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.

Use a TestNG ITestListener and capture the browser in onTestFailure(ITestResult), before teardown closes the WebDriver. Save the image under a path that your build publishes, then add a relative link or attachment using the reporting library your project actually uses. TestNG detects the failure; Selenium captures the image; the report integration connects the two.

What the solution must do

A reliable failure screenshot has four separate responsibilities:

  1. Identify the exact WebDriver instance that ran the failed invocation.
  2. Capture it from onTestFailure while the session is still alive.
  3. Write a uniquely named file to a report-accessible directory.
  4. Attach that file, or emit a relative link, through your report tool.

TestNG’s listener callback does not automatically know where your driver is stored, and TestNG’s default HTML output does not provide one universal image-attachment API. Those are project-specific decisions.

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

Prerequisites and project decisions

  • Java, Selenium WebDriver, and TestNG dependencies that match the versions used by your build.
  • A driver lifecycle that lets the listener retrieve the driver for the failed test.
  • A published artifact directory, such as target/surefire-reports or build/reports/tests.
  • A report library (if you need embedded images rather than links).

Do not hide the driver in an unrelated global singleton when tests run in parallel. Use a base test accessor, dependency injection, or a ThreadLocal<WebDriver> whose value is bound to the current invocation.

Implement the failure listener

Listener code

The following class is intentionally explicit about the parts that vary by project. Replace driverFor, screenshotPathFor, and attachToReport with your own implementations.

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.testng.ITestListener;
import org.testng.ITestResult;

public final class FailureScreenshotListener implements ITestListener {
  @Override
  public void onTestFailure(ITestResult result) {
    WebDriver driver = driverFor(result); // project-specific lookup
    if (driver == null) {
      System.err.println("No WebDriver available for " + result.getName());
      return;
    }

    try {
      File temporary = ((TakesScreenshot) driver)
          .getScreenshotAs(OutputType.FILE);
      Path destination = screenshotPathFor(result);
      Files.createDirectories(destination.getParent());
      Files.copy(temporary.toPath(), destination,
          StandardCopyOption.REPLACE_EXISTING);
      attachToReport(result, destination);
    } catch (WebDriverException | IOException captureError) {
      // Keep the assertion failure as the primary result.
      System.err.println("Could not capture failure screenshot: "
          + captureError.getMessage());
    }
  }

  private WebDriver driverFor(ITestResult result) {
    // Return the driver owned by this test invocation.
    throw new UnsupportedOperationException("Implement driver lookup");
  }

  private Path screenshotPathFor(ITestResult result) {
    String className = result.getTestClass().getName();
    String method = result.getMethod().getMethodName();
    String invocation = Integer.toString(result.getMethod().getCurrentInvocationCount());
    String safe = (className + "-" + method + "-" + invocation)
        .replaceAll("[^A-Za-z0-9._-]", "_");
    return Path.of("target", "surefire-reports", "screenshots", safe + ".png");
  }

  private void attachToReport(ITestResult result, Path image) {
    // Call the attachment API of your report library, or log a relative link.
    System.out.println("Failure screenshot: " + image);
  }
}

Selenium’s TakesScreenshot interface can return several representations. OutputType.FILE is convenient for copying; use OutputType.BYTES or OutputType.BASE64 when your reporter accepts in-memory data. A driver or element implementation may throw WebDriverException, and an implementation that does not support screenshots may throw UnsupportedOperationException.

Use a thread-local driver safely

public final class DriverStore {
  private static final ThreadLocal<WebDriver> CURRENT = new ThreadLocal<>();

  public static void set(WebDriver driver) { CURRENT.set(driver); }
  public static WebDriver get() { return CURRENT.get(); }
  public static void clear() { CURRENT.remove(); }
}

Create the driver and call DriverStore.set(driver) in setup, retrieve DriverStore.get() from the listener, and clear it only after the listener has had a chance to capture. If your framework stores drivers on a base class, have driverFor resolve the test instance from result.getInstance() instead.

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

Make the file visible in the report

Relative link in a plain HTML report

When the report is static HTML, place images beneath the report directory and write a relative anchor such as <a href="screenshots/LoginTest-signIn-0.png">Screenshot</a>. The build must publish both the HTML file and the screenshots directory; publishing only the HTML produces broken links.

Third-party reporters

Extent-style, Allure-style, and other reporters expose different attachment calls and lifecycle objects. Invoke that library’s method from attachToReport, passing either the copied path, bytes, or Base64 string it documents. Do not assume that TestNG’s Reporter.log embeds an image; it can record text or a link, while image rendering remains a reporter concern.

TestNG’s generated output

TestNG writes its generated reports to the configured output directory and exposes an index.html entry point plus XML output. Treat those files as destinations for your build artifacts, not as proof of a built-in, universal screenshot attachment feature.

Register the listener

Suite-wide XML registration

<suite name="UI suite">
  <listeners>
    <listener class-name="com.example.FailureScreenshotListener"/>
  </listeners>
  <test name="Chrome tests">
    <classes>
      <class name="com.example.LoginTest"/>
    </classes>
  </test>
</suite>

Annotation registration

import org.testng.annotations.Listeners;

@Listeners(FailureScreenshotListener.class)
public class LoginTest {
  // tests
}

The annotation applies at suite scope for the tests it covers, so use it deliberately. XML is preferable when you want an environment-specific listener without changing test classes.

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.

Driver lifetime, retries, and parallel execution

Capture before teardown

If an @AfterMethod or suite teardown calls quit() before the failure callback, the session is gone and no screenshot can be taken. Arrange teardown ordering so the listener runs while the driver remains available. A defensive listener should still catch capture errors and preserve the original assertion.

Generate collision-proof names

Method names alone collide across classes, data-provider rows, retries, and workers. Include class, method, invocation number, and a safe unique suffix. Sanitize slashes, spaces, and other path characters before writing.

Separate concurrent sessions

TestNG’s parallel modes do not define your driver association. Your storage design does. Keep one driver per invocation or thread, never mutate a shared driver, and ensure two workers cannot overwrite the same destination.

Decide what retries mean

A retry analyzer can execute the same method several times. Decide whether each failed attempt gets an image or only the final failure; include the attempt in the filename if you retain all of them. onTestFailure is not a catch-all for timeouts, skips, or failures allowed by a success-percentage rule. Add handling for onTestFailedButWithinSuccessPercentage, onTestSkipped, or onTestFailedWithTimeout only when those outcomes matter to your report.

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

Common failures and fixes

Symptom Likely cause Fix
No image is created The listener was never registered, or the driver lookup returned null. Verify XML/annotation registration and log the test instance, thread, and driver identity.
NoSuchSessionException or a closed-session error Teardown quit the browser first. Move capture earlier in the lifecycle; do not clear the driver store before the callback.
ClassCastException The driver does not implement TakesScreenshot. Use a screenshot-capable WebDriver implementation or record that capture is unsupported.
Files exist locally but links are broken in CI The artifact collector published HTML but not the image directory, or the relative path is wrong. Publish the complete report tree and test the link from the CI artifact root.
Images overwrite one another Filename contains only the method name. Add class, invocation/retry identity, and a unique suffix.
Only some failures have screenshots Those outcomes use timeout/skip callbacks, or the reporter flushes before attachment. Handle the relevant callbacks and flush the reporter after attachment.
Screenshot shows only a viewport WebDriver screenshot semantics are implementation-dependent for page, frame, window, or display scope. Do not promise full-page capture unless your browser/driver combination explicitly supports the method you use.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When Selenide is a better fit

Projects already using Selenide may not need this listener for ordinary Selenide checks: its documentation says screenshots are taken automatically when those checks fail, normally under build/reports/tests. You can change the directory with Configuration.reportsFolder. Selenide also documents a TestNG ScreenShooter listener for broader success and failure behavior, including non-Selenide assertions. Confirm the behavior against the Selenide version and report integration in your build before treating it as a drop-in replacement.

Approach Setup ownership Attachment behavior Best fit
Custom Selenium listener You own driver lookup, paths, lifecycle, and reporter calls. Whatever your report library supports. Plain Selenium projects and custom pipelines.
Selenide automation Selenide manages common failure capture; you configure folders/listeners. Integrated with its documented reporting conventions. Teams already using Selenide checks.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One request returns PNG, JPEG, WebP, or PDF; it accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP tools let Claude, Cursor, or another MCP client call take_screenshot, get_page_info, and capture_pdf.

For a direct capture, 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)
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}`);

Every plan includes the same feature set: full-page and selector capture, device and retina controls, dark mode, custom CSS/JavaScript, waits, request blocking, headers/cookies, timezone and geolocation, resizing, chosen-TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to start.

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

Operational checklist

  • Register the listener in the intended scope.
  • Prove that the failed invocation resolves to the correct driver.
  • Capture before quit() and preserve the original throwable.
  • Create directories and use collision-resistant filenames.
  • Attach with the actual reporter API or a relative link.
  • Publish images alongside HTML/XML reports in CI.
  • Test retries, data providers, parallel workers, skips, and timeouts separately.

Frequently Asked Questions

Can I capture a screenshot from an ITestListener after the test method returns?

Yes, if the WebDriver session is still open. If teardown has already called quit(), the listener cannot recover that browser state.

Does onTestFailure capture skipped tests or timeouts?

Not necessarily. TestNG exposes separate callbacks for skips, timeouts, and failures within a success-percentage allowance; implement those callbacks when your reporting policy requires images.

Is a Selenium screenshot always a full-page image?

No. The result depends on the conformant WebDriver implementation and the method used; it may represent a page, window, frame, or display.

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.

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