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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Capture the browser before the WebDriver session closes, map the test result to Pass, Fail, or Skip, and attach the image to the matching ExtentReports test or log entry. A failure-only callback is not enough when a run can finish in several statuses. Your listener or teardown code must decide which outcomes receive screenshots, save each file to a stable path, attach it with the correct ExtentReports API, and flush the report after all tests have logged.

The examples below target Java with TestNG and the ExtentReports Java APIs documented for v4. Adapt package names and lifecycle hooks to the versions used by your project. ExtentSparkReporter v5 changes reporter configuration, but the status and media concepts should be checked against the exact dependency version you install.

Choose a screenshot policy for every status

ExtentReports records outcomes on the test and log model. Common statuses include Pass, Fail, and Skip, and the status hierarchy can influence the overall result. Decide the policy before writing the listener:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Failures only: capture when a test fails; do not create misleading images for successful or skipped tests.
  • Pass and fail: retain evidence of both the final successful page and the failure state.
  • Every status: capture pass, fail, and skip only when a browser session exists for the skipped case.

A skipped TestNG method may never start WebDriver. Check for a live driver before calling Selenium’s screenshot API. If no session exists, log the skip without an image rather than turning the skip into a reporting error.

Keep one Extent test object for one framework test

Create or retrieve one Extent test entry for the currently executing TestNG result. Store that object where the completion callback can find it (for example, a thread-local holder when tests run in parallel). Do not create a second test in onTestFailure or log the same result from both a listener and teardown.

import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;

public final class ExtentManager {
    private static final ExtentReports EXTENT;

    static {
        ExtentSparkReporter spark = new ExtentSparkReporter("target/extent-report.html");
        EXTENT = new ExtentReports();
        EXTENT.attachReporter(spark);
    }

    private ExtentManager() { }

    public static ExtentReports reports() {
        return EXTENT;
    }

    public static ExtentTest createTest(String name) {
        return EXTENT.createTest(name);
    }
}

Reporter setup differs between ExtentReports generations. Keep the setup that matches your dependency; the important lifecycle rule is that flush() runs once after the run’s logging is complete.

Capture a file while WebDriver is still alive

Selenium’s Java driver exposes a screenshot facility through TakesScreenshot. The following helper creates a unique PNG in a directory that will travel with the HTML report. Verify the screenshot call and imports against the Selenium version in your build.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.util.UUID;

public final class ScreenshotFiles {
    private ScreenshotFiles() { }

    public static String save(WebDriver driver, String testName) throws IOException {
        Path directory = Path.of("target", "extent-screenshots");
        Files.createDirectories(directory);
        String safeName = testName.replaceAll("[^a-zA-Z0-9._-]", "_");
        Path destination = directory.resolve(
            safeName + "-" + Instant.now().toEpochMilli() + "-" + UUID.randomUUID() + ".png");
        Path temporary = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE).toPath();
        Files.copy(temporary, destination, StandardCopyOption.REPLACE_EXISTING);
        return destination.toString();
    }
}

Use a unique name in parallel suites. A relative path is convenient when the report and image directory are moved together; an absolute path can work locally but often breaks on a CI artifact viewer. Before publishing artifacts, open the generated report from the same directory layout and confirm that each image loads.

Attach the image at test level or log level

Test-level attachment

Use this when the image describes the final state of the whole test. The Java API is:

test.addScreenCaptureFromPath(path);

This stores a reference to the file. It does not embed the image in the report. ExtentReports documentation explains that file-based reporters reference the saved image with an HTML <img> element, so keep the screenshot files beside the report when sharing it.

Log-level attachment

Use a log-level media entity when the image belongs to a particular event, such as the assertion that failed:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import com.aventstack.extentreports.MediaEntityBuilder;

test.fail("Checkout assertion failed",
    MediaEntityBuilder.createScreenCaptureFromPath(path).build());

Replace fail with the appropriate status method for the outcome you are recording. This keeps the text and image together in the event timeline.

Path versus Base64

ExtentReports also documents Base64 screenshot APIs for tests and log events. Embedded data travels with the report payload, which is useful when a single artifact must be self-contained, but large images make the report larger. File paths keep the report lighter but require the referenced files to remain accessible. Choose one approach consistently for your CI artifact strategy.

TestNG listener that handles pass, fail, and skip

The official TestNG adapter supplies listener implementations, while a custom listener is useful when screenshot rules differ by status. The example below is a policy-driven custom listener. It assumes your test setup puts the active driver and Extent test into a thread-local context.

import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.MediaEntityBuilder;
import org.openqa.selenium.WebDriver;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class ReportingListener implements ITestListener {
    private static final ThreadLocal<ExtentTest> CURRENT_TEST = new ThreadLocal<>();
    private static final ThreadLocal<WebDriver> CURRENT_DRIVER = new ThreadLocal<>();

    public static void startTest(String name, WebDriver driver) {
        CURRENT_TEST.set(ExtentManager.createTest(name));
        CURRENT_DRIVER.set(driver);
    }

    public static void clearTest() {
        CURRENT_DRIVER.remove();
        CURRENT_TEST.remove();
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        finish(result, "passed", true);
    }

    @Override
    public void onTestFailure(ITestResult result) {
        finish(result, "failed", true);
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        // Set this to true only if your skip policy requires a screenshot.
        finish(result, "skipped", false);
    }

    private void finish(ITestResult result, String outcome, boolean capture) {
        ExtentTest test = CURRENT_TEST.get();
        if (test == null) return;

        try {
            String message = result.getThrowable() == null
                ? "Test " + outcome
                : result.getThrowable().toString();
            WebDriver driver = CURRENT_DRIVER.get();

            if (capture && driver != null) {
                String path = ScreenshotFiles.save(driver, result.getName());
                if ("failed".equals(outcome)) {
                    test.fail(message,
                        MediaEntityBuilder.createScreenCaptureFromPath(path).build());
                } else if ("passed".equals(outcome)) {
                    test.pass(message,
                        MediaEntityBuilder.createScreenCaptureFromPath(path).build());
                } else {
                    test.skip(message,
                        MediaEntityBuilder.createScreenCaptureFromPath(path).build());
                }
            } else if ("failed".equals(outcome)) {
                test.fail(message);
            } else if ("passed".equals(outcome)) {
                test.pass(message);
            } else {
                test.skip(message);
            }
        } catch (Exception reportingError) {
            // Do not hide the original test result because screenshot reporting failed.
            test.warning("Could not capture screenshot: " + reportingError);
        }
    }

    @Override
    public void onFinish(ITestContext context) {
        ExtentManager.reports().flush();
    }
}

The listener is a pattern rather than a universal drop-in: your fixture must call startTest after creating the driver, and clear the thread-local values after the test. If the adapter already creates tests and logs results, use its objects instead of creating another entry. In parallel execution, never store a mutable driver or test in a single static field shared by all threads.

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.

Register the listener and control fixture order

Register the listener through your TestNG configuration (for example, the suite XML or the listener annotation supported by your setup). Ensure the order is:

  1. Create the Extent test entry and start WebDriver.
  2. Run the test method.
  3. Let the completion callback capture the browser before the driver is quit.
  4. Quit WebDriver and clear thread-local state.
  5. Flush the report once the suite has finished.

If a configuration method quits the driver before onTestFailure or onTestSuccess, the callback cannot capture the final page. Move the quit operation to a point after listener processing, or capture in a teardown hook that still has access to the same driver and then let the listener only attach the already-created file.

Common failures and precise fixes

Image icon appears, but the screenshot is missing

The report has a path reference, not embedded bytes. Copy the screenshot directory with the HTML report and preserve its relative layout. Also check that the process writing the report has permission to read the image.

Skipped tests cause a NullPointerException

A skipped method may have no browser. Branch on a non-null driver and log skip without media when no session exists. If your policy requires evidence, capture from a driver created in a dedicated setup step rather than assuming one exists.

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.

Only failures are visible

Most failure callbacks do not define pass or skip behavior. Implement separate success and skipped handlers, or centralize all three outcomes in a completion method as shown above.

The same test appears twice

Both the adapter and a custom listener are probably creating entries. Pick one owner for test creation and one owner for final status logging. Retrieve the existing Extent test instead of calling createTest again.

Report status is wrong after multiple log calls

ExtentReports applies status precedence across logs. Do not log a failure and then mark the same test as passed in teardown. Map the framework’s final result once and use the corresponding Extent status method.

Screenshot capture fails after teardown

The driver has already been quit, or the browser crashed. Capture in the completion callback while the session is alive, guard the call, and record a warning when the image cannot be obtained. Preserve the original pass, fail, or skip result.

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

Parallel tests overwrite each other’s files

Use test name, timestamp, and a UUID (or another collision-resistant value) in the filename. Keep driver and Extent test references thread-local.

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

Performance, reliability, and artifact choices

  • Full-page or high-resolution screenshots increase disk usage and report load time; capture only the viewport or outcome that answers the debugging question when practical.
  • Writing files to a local workspace is usually simpler than sending image bytes through every log call. Archive the image directory with the report.
  • Base64 avoids broken relative links when a report is moved alone, but the embedded report becomes larger.
  • Flush once at suite completion rather than after every test, unless a crash-resilience requirement justifies the extra I/O.
  • When a browser crashes, treat the screenshot as unavailable evidence; do not change a framework failure into a reporting failure.

Or skip the browser setup

If you need a clean image of a URL rather than a browser session inside your test, ScreenshotNeo provides a website screenshot API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing state in X-Page-Verdict and X-Billed headers.

One GET request returns PNG, JPEG, WebP, or PDF. The API supports full-page captures with lazy images, CSS-selector element shots, device presets and custom viewports, dark mode, retina scale, custom CSS and JavaScript, clicks, waits, blocked resources, cookies, headers, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk requests for up to 100 URLs, usage data, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

See the ScreenshotNeo documentation for parameters and response handling.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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)
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}`);

The Free plan includes 1,000 shots each month without a card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account to try it.

Verification checklist before publishing a report

  • Each TestNG result maps to exactly one Extent test entry.
  • Pass, fail, and skip policies are explicit.
  • Skipped tests without a live driver do not attempt capture.
  • Images use unique, report-accessible paths.
  • Path-based reports ship with their image directory, or Base64 is used intentionally.
  • The driver remains alive until capture completes.
  • flush() runs once after all logging.
  • The generated report is opened from the same artifact layout used by readers.

Frequently Asked Questions

Should a skipped TestNG test always have a screenshot?

No. A skipped method may never create a WebDriver session. Capture only when your policy requires it and a live driver is available; otherwise record the skip without media.

Can I attach one screenshot to both the test and a log?

You can, but doing so duplicates the reference and may clutter the report. Attach at test level for a final-state image or at log level when it explains one event.

Why does moving the HTML report break screenshots?

Path-based ExtentReports attachments point to files on disk rather than embedding image data. Move the screenshot directory with the report or use a documented Base64 API.

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

Where should report flushing happen in a parallel TestNG suite?

Flush after the suite finishes and all listener callbacks have logged. Do not flush independently from each test thread.

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.