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.

Use Cucumber-JVM’s @AfterStep hook, obtain PNG bytes from the same Selenium WebDriver used by your step definitions, and attach those bytes to the current Scenario. The hook runs after every step that actually executes, including passing and failing steps. If a step fails, Cucumber skips later steps—and their hooks—so no hook can capture a step that never ran.

The complete pattern

Keep the browser lifecycle in your existing test context, then inject that context into a glue class. The class must be in a package scanned by the Cucumber TestNG runner. This example assumes a context object exposes a driver() method.

package steps;

import io.cucumber.java.AfterStep;
import io.cucumber.java.Scenario;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;

public class ScreenshotHooks {
    private final WebDriver driver;

    public ScreenshotHooks(TestContext context) {
        this.driver = context.driver();
    }

    @AfterStep
    public void captureAfterStep(Scenario scenario) {
        byte[] png = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.BYTES);
        scenario.attach(png, "image/png", "after-step");
    }
}

TestContext is deliberately project-specific. Replace it with your dependency-injection object, driver manager, or other context type. The important requirements are that the hook receives the same driver instance as the step definitions and that the driver is still valid when the hook executes.

What each line does

  • @AfterStep tells Cucumber-JVM to invoke the method after an executed step.
  • Scenario scenario gives the hook the current scenario, including its report attachment API.
  • TakesScreenshot is Selenium’s screenshot interface. The cast is appropriate for drivers that implement it, including the normal browser drivers.
  • getScreenshotAs(OutputType.BYTES) returns PNG bytes without creating a temporary file.
  • scenario.attach(...) embeds the bytes in the report. The media type must be supplied; image/png tells the formatter how to render the attachment.

How to wire the hook into a TestNG project

  1. Put ScreenshotHooks in the glue package configured by your Cucumber runner.
  2. Make the hook’s constructor match your dependency-injection setup. If your project uses a shared driver manager, retrieve the driver there instead of constructing a second browser.
  3. Ensure the driver is created before a scenario starts and is not quit until all step hooks for that scenario have completed.
  4. Run scenarios through your existing Cucumber TestNG runner. The hook mechanism is the same whether scenarios are launched through TestNG or JUnit; TestNG does not require a different annotation.

Do not add a separate driver to the hook. A second browser can show a blank page, the wrong tab, or a different authentication state. The hook should observe the exact browser session that performed the step.

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

What “after every step” actually means

Scenario event Hook behavior Screenshot result
Step passes @AfterStep runs Passing-step image is attached
Step fails The hook runs for that failed step Failure-state image is attached
Later step after a failure Cucumber skips the step and its hooks No image, because nothing executed
Step skipped for another reason No executed step hook No image for the skipped step

Step hooks have invoke-around semantics: they surround each step that Cucumber executes. Therefore, a scenario with ten successful steps normally receives ten attachments; a scenario that fails on step four receives images for the first four executed steps, not for steps five through ten.

Capturing only failures instead of every step

Attaching every image is useful when you need a visual trail, but it can make reports large. If your policy is failure-only capture, guard the same hook with the scenario status:

@AfterStep
public void captureFailedStep(Scenario scenario) {
    if (!scenario.isFailed()) {
        return;
    }

    byte[] png = ((TakesScreenshot) driver)
            .getScreenshotAs(OutputType.BYTES);
    scenario.attach(png, "image/png", "failed-step");
}

Use this condition only when failure-only evidence is the intended behavior. It does not recover screenshots for steps skipped after an earlier failure.

Naming and organizing attachments

A stable name such as after-step is valid and keeps the hook simple. If your formatter preserves attachment names, a name containing a step index or sanitized step text makes a long report easier to scan. Keep names free of secrets: never include passwords, access tokens, session cookies, or personally identifying data in a step title or generated label.

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

The attachment is binary report data, not a file path. Your chosen Cucumber formatter determines where it is displayed or stored. Verify that the formatter used by your TestNG build supports embedded media; a formatter that emits only plain text may not render images even though Scenario.attach was called.

Driver lifecycle and parallel TestNG execution

The hook itself is stateless apart from its driver reference, but parallel scenarios make driver ownership critical. Each concurrently running scenario must resolve to its own isolated driver (commonly through a scenario-scoped context or a ThreadLocal manager). Never let two TestNG threads share a mutable browser instance.

  • Create the driver before the first step for that scenario.
  • Resolve the same instance in step definitions and ScreenshotHooks.
  • Quit it in an after-scenario cleanup hook, after all after-step hooks have finished.
  • Clear thread-local or context state during cleanup so the next scenario cannot reuse a closed driver.

If a dependency-injection framework constructs glue classes, make its scope compatible with your driver scope. A singleton hook that captures a driver once is unsafe when scenarios run in parallel or when the driver is replaced between scenarios.

Making the capture robust

Check screenshot support

Most Selenium browser drivers implement TakesScreenshot, but the interface is not guaranteed for every custom or remote implementation. If the cast fails, use a driver implementation that supports screenshots or handle the unsupported case explicitly rather than hiding the error.

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

Capture before teardown

If a cleanup hook calls driver.quit() before @AfterStep runs, the screenshot call will fail. Keep teardown at scenario scope and confirm hook ordering in your project when multiple hooks are present.

Keep the hook lightweight

PNG bytes are held in memory briefly and then passed to the report formatter. Avoid writing an additional file unless another system requires one. If report size becomes a problem, adopt failure-only capture or a project-specific retention policy instead of silently dropping attachments.

Use explicit media types

Always pass image/png (or the actual type if your driver returns another format). Omitting the media type or passing a generic value can prevent report viewers from displaying the image correctly.

Common failures and fixes

Symptom Likely cause Fix
ClassCastException at TakesScreenshot The active driver does not implement the interface. Use a screenshot-capable Selenium driver or add an explicit capability check and diagnostic.
Null or closed-driver error The hook gets a different context, or teardown ran first. Inject the scenario’s driver and move quit() to after all step hooks.
No images in the report Glue package is not scanned, the hook is not running, or the formatter does not render embedded media. Confirm the runner’s glue setting, add a log breakpoint in the hook, and verify formatter support.
Only some steps have images A prior step failed and later steps were skipped. Inspect the first failing step; skipped steps cannot execute @AfterStep.
Wrong browser state The hook created or retrieved a second driver. Share the exact driver instance used by step definitions.
Parallel runs show mixed screenshots Drivers or context are shared between TestNG threads. Use scenario-scoped or thread-isolated driver storage and clear it during teardown.
Report becomes unwieldy An image is attached after every successful step. Switch to the scenario.isFailed() guard or reduce scenario step count.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need a screenshot of a public page rather than a live Cucumber browser session, ScreenshotNeo provides a single HTTP capture request. It accepts cookie and 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 result with 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.

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

For Java-based test tooling, call the same endpoint from a process step or helper. The API also supports full-page captures with lazy images, CSS-selector element shots, dark mode, device presets, custom viewport and retina scale, PDF output, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, 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 are accepted to ease migration.

cURL

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

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)

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}`);

See the ScreenshotNeo documentation for request options and response handling. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account to try it.

Practical verification checklist

  • Run one scenario with at least two passing steps and confirm two image attachments.
  • Force a failure in the second step and confirm the first and failed steps have images while later steps are skipped.
  • Run scenarios in parallel and inspect that each attachment matches its own browser.
  • Close the driver only after step hooks complete.
  • Check the generated report with the formatter used in CI, not only an IDE preview.

Frequently Asked Questions

Can an @AfterStep hook change the browser before the next step?

Yes. It is an executable hook, so it can perform project-specific actions, but changing page state in a screenshot hook can make the next step observe a different application state. Keep capture hooks read-only unless that behavior is intentional.

Does Scenario.attach save a PNG file on the test machine?

It attaches bytes to the Cucumber report pipeline. Whether a separate file is written depends on the formatter and report configuration.

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

Can the same hook be used with remote Selenium Grid?

Yes, provided the remote driver supports TakesScreenshot and the hook accesses that same remote session.

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.