Capture the image in ITestListener.onTestFailure(ITestResult), while the WebDriver session is still alive, then let @AfterMethod call quit(). Selenium returns an OutputType.FILE temporary file, so copy it immediately to a directory owned by your build or CI system. The listener below also handles timeout failures, parallel-safe names, missing drivers and capture errors without hiding the original test failure.
The lifecycle you need
For a normal TestNG test failure, the useful order is:
- The test method throws an assertion or another exception.
- TestNG invokes
onTestFailureon registeredITestListenerimplementations. - The listener obtains the test instance’s live driver and calls
getScreenshotAs. - The listener copies Selenium’s temporary file to durable test artifacts.
@AfterMethod(alwaysRun = true)performs teardown and callsdriver.quit().
If quit() happens first, there is no browser session from which to capture. Do not close the driver in the failure listener before calling getScreenshotAs.
Expose the driver to the listener
A listener receives an ITestResult, not a typed field from your test class. A small project-owned interface gives the listener a safe way to obtain the driver without reflection.
Recommended Free Tools
#1 Best Overall
public interface HasDriver {
WebDriver getDriver();
}
Every test class that wants failure screenshots implements this interface. If your project uses a base class, that base class can implement it once and return its protected driver field.
Implement the failure listener
The implementation below creates the destination directory, generates a filesystem-safe name containing the class, method, timestamp and thread, copies the temporary file, and deliberately treats capture errors as secondary diagnostics.
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.ITestListener;
import org.testng.ITestResult;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public final class FailureScreenshotListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
capture(result);
}
// TestNG versions that expose this callback invoke it for a timeout failure.
@Override
public void onTestFailedWithTimeout(ITestResult result) {
capture(result);
}
private void capture(ITestResult result) {
Object instance = result.getInstance();
if (!(instance instanceof HasDriver)) {
return;
}
WebDriver driver = ((HasDriver) instance).getDriver();
if (!(driver instanceof TakesScreenshot)) {
return;
}
String className = result.getTestClass().getName().replaceAll("[^a-zA-Z0-9._-]", "_");
String methodName = result.getMethod().getMethodName().replaceAll("[^a-zA-Z0-9._-]", "_");
String fileName = className + "-" + methodName + "-" +
System.currentTimeMillis() + "-thread-" + Thread.currentThread().getId() + ".png";
Path target = Path.of("test-artifacts", "screenshots", fileName);
try {
Files.createDirectories(target.getParent());
File temporary = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
Files.copy(temporary.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException | RuntimeException captureError) {
// Log captureError, but do not replace the original test failure.
System.err.println("Could not save failure screenshot: " + captureError.getMessage());
}
}
}
TakesScreenshot.getScreenshotAs is the Selenium API for capturing an image. A driver implementation may throw UnsupportedOperationException when screenshots are not supported, which is why the capture call is guarded and wrapped.
Why the temporary-file copy matters
OutputType.FILE does not designate your final artifact location. Selenium documents that the returned file is temporary and that users must make a copy. Copy it in the same callback, before the JVM exits or any cleanup process removes temporary files. Saving under test-artifacts/screenshots also gives Maven, Gradle or CI artifact collection a predictable path.
Rank #2
Register the listener
Annotation registration
Apply @Listeners to a test class:
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(FailureScreenshotListener.class)
public class CheckoutTest implements HasDriver {
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
}
@Override
public WebDriver getDriver() {
return driver;
}
@Test
public void checkoutShowsConfirmation() {
driver.get("https://example.test/checkout");
// assertions...
}
@AfterMethod(alwaysRun = true)
public void tearDown() {
if (driver != null) {
driver.quit();
driver = null;
}
}
}
Initialize the driver before the test and make teardown null-safe. Setting the field to null after quit() prevents later code from mistaking a closed session for a live one.
XML registration
For suite-wide registration, add the listener to testng.xml:
<listeners>
<listener class-name="example.FailureScreenshotListener"/>
</listeners>
XML registration is useful when you want the same listener for many classes without adding an annotation to each one.
Timeouts, teardown and custom runners
TestNG documents onTestFailure as being invoked each time a test fails. Timeout failures can have a separate callback in newer TestNG releases; TestNG 7.9.0 lists onTestFailedWithTimeout separately. Implement that method when your version exposes it and delegate to the same private capture method, as shown above. If your project’s TestNG version does not contain that callback, remove the method (and its @Override) so the listener compiles.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #3
In the standard lifecycle, @AfterMethod runs after the failure callback. A custom runner can change that ordering. If your runner shuts down the driver before listener processing, move shutdown to a later suite or test cleanup hook, or keep the session in a framework-owned registry until listeners finish. The invariant is simple: getScreenshotAs must run before quit().
Make captures reliable in CI and parallel runs
Use names that cannot collide
Parallel methods can share a class and method name. Include a timestamp and thread identifier, and sanitize class and method names before using them as path components. If retries can occur within the same millisecond, add a UUID or an atomic sequence to the filename.
Keep diagnostics from masking failures
A full disk, unwritable workspace, crashed browser or unsupported driver can make capture fail. Catch IOException and runtime capture exceptions, log them, and leave the original assertion or exception as TestNG’s failure result. A screenshot is evidence, not a reason to turn a test failure into an unrelated listener error.
Account for driver ownership
With one driver per test instance, result.getInstance() and HasDriver are sufficient. With a ThreadLocal<WebDriver> design, have getDriver() return the current thread’s driver. Do not use a single static driver in parallel execution unless your framework deliberately serializes access.
Rank #4
Collect the directory
Configure your build server to archive test-artifacts/screenshots even when tests fail. Otherwise the listener may save a correct file that disappears when the CI workspace is discarded.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No image is created | The class is not implementing HasDriver, the listener is not registered, or the driver field is null. |
Check annotation or testng.xml registration, implement getDriver(), and initialize the driver in @BeforeMethod. |
| “No such session” or similar error | Teardown called quit() before the listener captured. |
Move shutdown after listener processing and never call quit() inside the capture method. |
| Capture reports unsupported operation | The active WebDriver implementation does not implement screenshot capture. | Use a driver that implements TakesScreenshot; keep the guard so the original test failure is preserved. |
| Files overwrite each other | Names contain only the test method. | Add class, timestamp, thread ID and, for aggressive retries, a UUID or sequence. |
| Timeouts have no screenshot | The TestNG version routes timeouts to a separate callback. | Implement onTestFailedWithTimeout when available and delegate to the same capture routine. |
| Screenshot exists locally but not in CI | The artifact directory is not archived or the workspace is cleaned. | Publish test-artifacts/screenshots as a CI artifact and verify write permissions. |
| The page is blank or half-rendered | The browser failed or timed out before a usable document was displayed. | Log the capture exception and preserve the failure; a listener cannot recover a browser process that has already crashed. |
Performance and operational trade-offs
A screenshot adds a file transfer and disk write to every failed test, but successful tests incur no capture work. Keep the destination on fast local workspace storage and archive artifacts after the run rather than writing directly to a remote network share. If failures are extremely frequent, rotate or compress artifacts outside the listener so the callback remains short.
Capture timing also determines what you see. The callback records the browser state at failure, before teardown clears cookies, navigates away or closes the window. If a test fails while a modal, alert or new window is active, the driver may reject the command; log that exception and retain the original failure instead of attempting disruptive recovery in the listener.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server if you need images outside a live Selenium test. One request returns PNG, JPEG, WebP or PDF. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
Use the API documentation at https://screenshotneo.com/docs/ for authentication and options. A minimal cURL request is:
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same call in Python:
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)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Beyond a basic shot, the service supports full-page captures with lazy images, CSS-selector element captures, dark mode, device presets and custom viewports, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, clicks, selector or network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000 and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to start with 1,000 screenshots a month and no card.
Frequently Asked Questions
What should the listener do when the browser has already crashed?
Record the capture exception and leave the original TestNG failure unchanged. There may be no recoverable browser session, so the missing image is secondary to preserving the actual test error.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteCan one listener support tests that do not use a browser?
Yes. The HasDriver and TakesScreenshot checks simply return without capturing, allowing the same suite listener to cover non-browser tests.
Quick Recap
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.

