Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture the screenshot while Selenium’s WebDriver is still running, save the filename or base64 data on the individual test result, and change the HTMLTestRunner template to emit an <img> element for that result. This ordering is the part that must not change: once driver.quit() has run, there is no browser session left to capture.

There is no single screenshot-attachment API shared by every package named HTMLTestRunner. The original HTMLTestRunner distribution, forks, and newer packages use different result classes and templates, so verify the package and version installed in your environment before copying a template example.

What the implementation has to do

A working attachment has four separate parts. Treating them separately makes the code portable between HTMLTestRunner variants.

  1. Capture: call Selenium’s screenshot method before the browser is closed.
  2. Associate: store the resulting path or encoded image on the test/result record for the exact case that produced it.
  3. Render: add an image element to the report template at the point where that test’s details are printed.
  4. Package: keep linked image files at the relative paths used by the report, or embed the bytes so the HTML is self-contained.

Selenium’s Python WebDriver API documents save_screenshot(path) and get_screenshot_as_file(path) for PNG files, and get_screenshot_as_base64() for encoded data. The documentation specifically notes that base64 output is useful for embedding screenshots in HTML: Selenium Python WebDriver API.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

First identify your HTMLTestRunner package

Before editing a template, determine which distribution your test command imports. The package name shown by pip, the Python import path, the result class, and the template variable names can all differ.

Check the installed distribution

python -m pip show htmltestrunner
python -m pip show htmltestrunner-lit
python -c "import HtmlTestRunner, inspect; print(HtmlTestRunner.__file__)"

Use the command that matches the import used by your suite. Open the installed package’s report template and result implementation, then locate the loop that writes each test case. That loop is where the screenshot value must be available. The oldani/HtmlTestRunner report template is a useful example of a template to inspect, not a universal contract.

Do not copy a helper across packages

htmltestrunner-lit 1.0.5 documents an attach_screenshot helper for that package. That helper is not evidence that the original package or another fork exposes the same method. If your installed result class has no attachment method, keep the capture helper below and add the value to the result object or template context used by your distribution.

Capture a screenshot from a unittest case

The following case captures every test in teardown, records a stable relative filename, and closes the browser only after the capture attempt. Capturing every test is the most compatible starting point because teardown usually does not yet expose a portable, completed failure record.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path
import re
import unittest
from selenium import webdriver

SCREENSHOT_DIR = Path('test-report-assets')


def safe_name(value):
    value = re.sub(r'[^A-Za-z0-9_.-]+', '_', value)
    return value.strip('_') or 'test'


class BrowserCase(unittest.TestCase):
    def setUp(self):
        SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
        self.driver = webdriver.Chrome()
        self.driver.set_window_size(1440, 1000)
        self.screenshot_path = None
        self.screenshot_data_uri = None

    def tearDown(self):
        # The WebDriver must still be alive here.
        filename = safe_name(self.id()) + '.png'
        absolute_path = SCREENSHOT_DIR / filename
        try:
            written = self.driver.save_screenshot(str(absolute_path))
            if written:
                # HTMLTestRunner must receive a path relative to the report file.
                self.screenshot_path = str(Path(SCREENSHOT_DIR.name) / filename)
        finally:
            self.driver.quit()

    def test_home_page(self):
        self.driver.get('https://example.com')
        self.assertIn('Example Domain', self.driver.title)

save_screenshot returns a Boolean according to Selenium’s API. Keep the value and attach the path only when it is true; otherwise a report can contain an image tag that points to a file that was never written. The directory is created before the browser starts, so a permissions or missing-directory error does not occur at capture time.

Use a base64 data URI instead of a file

Replace the file-writing portion with this form when you want one portable HTML file:

import base64

encoded = self.driver.get_screenshot_as_base64()
self.screenshot_data_uri = 'data:image/png;base64,' + encoded

Do not store both forms in a large suite unless you have a reason: embedded data increases the HTML size for every image, while linked files keep the report smaller but require the asset directory to travel with it.

Attach the value to the matching test result

HTMLTestRunner implementations usually transform a unittest.TestCase into an internal result record before rendering. Your goal is to copy test.screenshot_path or test.screenshot_data_uri into that record under a field that the template can read. The exact hook is package-specific, so inspect the result class rather than assuming a method name.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep the association per test

  • Use self.id(), which includes the module, class, and method, as the filename seed.
  • Sanitize the identifier for the operating system, as in safe_name.
  • If a test can take several screenshots, add a checkpoint name or an incrementing suffix instead of overwriting the first file.
  • When tests run in parallel, add a worker or process identifier to avoid two browsers writing the same path.

Choose when to capture failures

For a package-neutral implementation, capture in teardown and let the report renderer decide whether to display the image for a passed or failed case. A teardown method may run before the result object has received the final failure entry, and private attributes such as _outcome have changed between Python versions. If you need failure-only files, use a result hook supported by your installed runner, or override the case’s execution flow so the exception is known before teardown closes the driver. Do not rely on a private outcome layout without testing it against the Python version used in CI.

A practical compromise is to capture every case, render images only for failures, and delete assets belonging to passed cases after the report has been generated. That preserves correct timing while keeping the delivered artifact small.

Render the screenshot in the report template

Find the template section that prints one test’s detail row. Add an image only when the record has a non-empty screenshot value. The HTML emitted should be equivalent to one of these two patterns:

<!-- linked-file variant; src is relative to the report HTML -->
<a href="test-report-assets/test_module_BrowserCase_test_home_page.png">
  <img src="test-report-assets/test_module_BrowserCase_test_home_page.png"
       alt="Screenshot for test_home_page" loading="lazy">
</a>

<!-- embedded variant -->
<img src="data:image/png;base64,..." alt="Screenshot for test_home_page" loading="lazy">

Replace the literal filename with the template variable used by your runner. Escape the test name before placing it in an HTML attribute, and omit the entire block when the value is empty. Keep the image under the test’s own detail section, not in a suite-level footer, so a multi-case report cannot display the wrong browser state.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Linked files versus embedded data

Choice Advantages Costs and failure modes
Linked PNG files Smaller HTML; images can be opened separately and cached by a browser. The report and asset directory must be distributed together. Moving only the HTML, renaming the directory, or changing the working directory can produce broken images.
Base64 embedded images The HTML carries the screenshot bytes and can be copied as one file without path problems. Every image enlarges the HTML. Large suites can become slow to open or difficult to store as a CI artifact.

Whichever option you choose, open the generated report from the same location in which a reviewer will receive it. A path that works on the test machine is not proof that the archived artifact is portable.

Failure-only reports without closing the browser too soon

The ordering is the common source of blank or missing failure images:

  1. Start the browser in setUp.
  2. Run the test and allow the runner to record its outcome.
  3. Capture while the driver is alive. If your runner supplies a supported failure hook, capture there; otherwise capture in teardown for every case.
  4. Store the path or data on that test’s result record.
  5. Render only the records marked failed or errored when the template is built.
  6. Quit the driver after the capture attempt and after any browser logs you also need.

Capturing after quit() cannot work. Capturing a single global filename is equally unsafe: the next test overwrites it, and every row can appear to point at the last browser state.

Common errors and fixes

The report has no image element

The template was not changed, or the template variable is named differently from the field you populated. Inspect the installed template and add a conditional image block inside the per-test loop. Confirm that the result record actually contains a non-empty path or data URI before rendering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An image icon appears, but the file is missing

Usually the src is absolute, uses the wrong working directory, or points to a directory that was not copied with the report. Generate a path relative to the report HTML, copy the asset directory into the same CI artifact, and test after moving the complete artifact to a clean folder.

Every test shows the same screenshot

A shared filename or suite-level variable is being reused. Include the full test identifier and a unique checkpoint suffix in each name, then attach that value to the individual result rather than to a global object.

Screenshots are blank or capture the previous page

Capture only after navigation and required UI actions have completed. If the page loads asynchronously, wait for the application’s stable element before calling Selenium’s screenshot method. Do not quit or recreate the driver between the action that fails and the capture.

Failure detection works locally but not in CI

Private unittest internals and runner result hooks can vary by Python and package version. Prefer a documented result hook for your installed HTMLTestRunner; otherwise capture all cases and filter at render time. Log the resolved output directory and the Boolean returned by save_screenshot so a CI artifact can distinguish a capture failure from a template problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The helper in an online example is undefined

That example may target a fork such as htmltestrunner-lit, whose documented API is specific to its own package. Compare the installed distribution and version, then adapt the underlying Selenium call and your runner’s result/template interfaces instead of importing the helper unchanged.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Report size, runtime, and reliability choices

  • Runtime: a PNG capture is an additional browser operation for each selected test. Capturing only failed cases reduces work, but requires a supported failure hook; capturing all and filtering is simpler and more portable.
  • Storage: linked files scale better for large suites. Embedded base64 keeps sharing simple but makes the HTML grow with every screenshot.
  • Determinism: set a known viewport, use stable filenames, and wait for the same application condition before capture so visual differences represent test outcomes rather than timing.
  • Security: screenshots can contain credentials, personal data, or tokens displayed in the browser. Restrict report artifacts and remove sensitive images before publishing them.
  • Verification: include a post-run check that the expected asset exists (or that the data URI is non-empty) and open a representative failed and passed case in a browser.

Or skip the browser setup

If you need a rendered page image rather than a screenshot tied to a live Selenium test, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF; it can accept consent banners before capture and remove more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots, and each response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers.

For the complete parameter list and response details, see the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    'https://api.screenshotneo.com/v1/shot',
    params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
    timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

You can request full-page output, a CSS-selected element, a device preset or custom viewport, dark mode, retina scale, lazy-image loading, custom CSS and JavaScript, clicks, waits, blocked ads or resource types, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, a chosen cache TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, and PDF paper, margin, orientation, and page-range options. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Every feature is available on every plan; 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Checklist before sharing the report

  • The screenshot is captured before driver.quit().
  • Each case has its own stable, collision-resistant filename or data URI.
  • The result object used by your installed HTMLTestRunner contains that value.
  • The template emits an image inside the matching test section and omits empty values.
  • Linked assets travel with the HTML, or the report uses embedded base64 data.
  • A failed case and a passed case were opened from the final archived location.
  • Sensitive browser content has been removed or access to the report is restricted.

Frequently Asked Questions

How can I tell which HTMLTestRunner template is actually rendering my report?

Print the imported module path and inspect the installed package directory, then compare the template named by that package with the HTML file produced by your test command. A fork can use different field names even when the import looks similar.

Can one report use both linked and embedded screenshots?

Yes. Store a path for ordinary cases and a data URI for cases that must remain portable, then have the template choose the non-empty value. Keep the conditional logic in one per-test block so the association remains unambiguous.

Why is capturing every test sometimes safer than capturing failures only?

The final failure entry may not exist while teardown is running, and private unittest outcome attributes vary by version. Capturing while the driver is alive and filtering during report rendering avoids depending on those private details.

The Bottom Line

Capture before quitting WebDriver, attach the value to the individual result, and make the exact HTMLTestRunner template render it. Use linked PNGs for smaller, scalable artifacts or base64 for a self-contained report, and verify the package-specific result and template APIs before deploying the pattern.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.