How to Insert Screenshots into SpecRun and SpecFlow Reports
Capture screenshots in SpecRun or SpecFlow hooks, render them in a custom report template, and publish portable image links with CI.

To insert screenshots into a SpecRun or SpecFlow report, save each browser screenshot in the runner output directory, write its path to the test trace, and configure a custom Razor/CSHTML report template to turn that path into an image. Publish the generated HTML together with the image directory so relative links continue to work.
Saving a PNG alone does not attach it to a report. The report renderer must receive a path or marker and convert it into an <img> element or an image link.
How the native workflow works
The reliable pattern has four stages:

- Capture the browser in an
[AfterStep]or[AfterScenario]hook. - Write the file below the test runner’s output directory, using a unique name.
- Emit the path through console or trace output as a
file:///URL or a marker such asSCREENSHOTXX path XXSCREENSHOT. - Select a custom Razor/CSHTML template in the
.srprofilefile and replace the trace token with a relative image link.
Keep the report and media files together. A report copied without its PNG files will contain broken images.
Capture a screenshot in a SpecFlow hook
The following example uses Selenium’s ITakesScreenshot. The exact driver and hook namespaces vary by Selenium and SpecFlow version, but the important behavior is stable: capture after a step, save into the runner work directory, and print a path that the report template can recognize.
using System;
using System.IO;
using OpenQA.Selenium;
using TechTalk.SpecFlow;
using NUnit.Framework;
[Binding]
public sealed class ScreenshotHooks
{
private readonly IWebDriver driver;
public ScreenshotHooks(IWebDriver driver)
{
this.driver = driver;
}
[AfterStep]
public void SaveScreenshotAfterStep()
{
var takesScreenshot = (ITakesScreenshot)driver;
var screenshot = takesScreenshot.GetScreenshot();
var outputDirectory = TestContext.CurrentContext.WorkDirectory;
Directory.CreateDirectory(outputDirectory);
var fileName = $"step-{DateTime.UtcNow:yyyyMMdd-HHmmssfff}-{Guid.NewGuid():N}.png";
var path = Path.Combine(outputDirectory, fileName);
screenshot.SaveAsFile(path, ScreenshotImageFormat.Png);
// Use a file URL, or use a marker that your custom template replaces.
var fileUrl = $"file:///{path.Replace('\\', '/')}";
Console.WriteLine(fileUrl);
}
}
AfterStep creates a screenshot for every step. If that produces too many files, use AfterScenario instead, or capture only when a step fails. A failure-only hook needs access to the scenario result supplied by your test framework; the output path and template process remain the same.
Use collision-resistant paths
Parallel workers can execute the same scenario at the same time. Do not use only a fixed name such as screenshot.png. Include a GUID, a timestamp with milliseconds, a worker identifier, or the scenario name after sanitizing it. A useful layout is:
artifacts/
screenshots/
worker-01/
checkout-7f7e...-step-03.png
SpecRun.html
Make the directory before saving, and keep the path inside the artifact directory that your CI job publishes.
Emit a path the report can recognize
There are two common trace conventions:
| Convention | When to use it | Template work |
|---|---|---|
file:///absolute/path/image.png |
Simple integrations that scan trace output for file URLs | Convert the absolute path to a relative path before rendering |
SCREENSHOTXX path XXSCREENSHOT |
Projects that want an unambiguous custom token | Parse the marker and emit an image element |
Absolute file URLs are convenient while diagnosing a build, but relative links are better for a report that will be downloaded or moved. The template should resolve the file under the report’s media directory and HTML-encode any value before inserting it.
Configure a custom SpecRun report template
SpecRun (also known as SpecFlow+ Runner in later documentation) selects report templates through the .srprofile file. A minimal profile section looks like this:
<Report>
<Template name="CustomReport.cshtml"
outputName="SpecRun.html"
existingFileHandlingStrategy="Overwrite" />
</Report>
Place the template where the runner expects it, and match the XML namespace and profile schema for the installed runner version. Template models and trace property names differ between versions, so start by copying the version’s default template and make the smallest possible change.
Replace a screenshot token with an image
The exact Razor object containing formatted trace output depends on the runner template. The following is a pattern, rather than a drop-in file. It shows the transformation you need to make after the default template has produced trace HTML:
@{
// Replace this with the trace property exposed by your runner template.
var traceHtml = Model.FormattedTrace;
var marker = "SCREENSHOTXX";
var endMarker = "XXSCREENSHOT";
while (traceHtml.Contains(marker) && traceHtml.Contains(endMarker))
{
var start = traceHtml.IndexOf(marker, StringComparison.Ordinal);
var valueStart = start + marker.Length;
var end = traceHtml.IndexOf(endMarker, valueStart, StringComparison.Ordinal);
if (end < 0) break;
var rawPath = traceHtml.Substring(valueStart, end - valueStart).Trim();
var safeFileName = Path.GetFileName(rawPath);
var relativePath = "screenshots/" + safeFileName;
var image = $"<img loading='lazy' width='50%' src='{Html.Encode(relativePath)}' alt='Step screenshot' />";
traceHtml = traceHtml.Remove(start, end + endMarker.Length - start)
.Insert(start, image);
}
}
@Html.Raw(traceHtml)
Use the runner’s HTML encoder rather than concatenating untrusted trace text. If your trace contains an absolute path, map it to the file’s relative location under the report directory. Do not allow arbitrary path segments such as ../ to escape that directory.
Make the report portable in CI
- Choose one artifact root for the HTML report and screenshots.
- Save every image below that root.
- Configure the template to emit relative URLs such as
screenshots/name.png. - Publish the whole root, not just the HTML file.
- Copy the artifact to a clean directory and open the report as a final check.
Some CI viewers serve artifacts from a web origin, while others open HTML from the local filesystem. Relative image links work in both cases more often than machine-specific absolute paths. If your viewer blocks local file access, serve the artifact directory through the CI viewer or a simple static server.
Capture only what you need
Full-step capture is useful for debugging but can create thousands of files in a large suite. Consider these policies:
- Every step: best for exploratory debugging and short suites.
- Every scenario: one final-state image with much lower storage use.
- Failures only: lowest overhead for routine CI. Capture in an
[AfterScenario]hook when the scenario has failed. - Selected steps: add a tag or binding attribute and capture only checkout, payment, or other high-value flows.
PNG is usually the safest choice for text and UI detail. If your reporting pipeline supports it, JPEG reduces size for photographic pages but can blur small text. Avoid embedding every image as base64 unless you specifically need a single self-contained HTML file; base64 makes the document larger and can slow report loading.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot file exists, but report shows no image | The trace path is not emitted or the template does not parse it | Inspect raw trace output, verify the marker spelling, and test the replacement against one known path |
| Report shows a text URL | The default template renders trace text without custom replacement | Select the custom CSHTML template in .srprofile and replace the token with an anchor or <img> |
| Broken images after downloading artifacts | Only HTML was published, or links point to an absolute build path | Publish the media directory and emit relative links |
| Parallel tests overwrite images | Fixed filenames are shared by workers | Add a GUID, worker ID, scenario ID, or timestamp to every filename |
| Images appear in the wrong scenario | Shared state or a reused filename associates a later trace with an earlier file | Generate the path inside the hook and keep it local to that scenario’s output |
| HTML becomes extremely large | Too many full-size PNGs or base64 embedding | Capture on failure, resize where acceptable, link files instead of embedding, and prune old artifacts |
| Template compilation error | Template API or model changed between runner versions | Start from the installed runner’s default template and change only the trace rendering block |
| Screenshot is blank | Capture happened before navigation or asynchronous content finished | Wait for the page’s ready condition, a selector, or the application-specific network idle state before capture |
| File access denied | Output directory is read-only or the file is still being written | Use the runner work directory, ensure it exists, and close or finish the screenshot write before logging the path |
ExtentReports and ReportPortal alternatives
If the project already uses ExtentReports, its APIs can attach a file path to a test or log, including AddScreenCaptureFromPath and MediaEntityBuilder.CreateScreenCaptureFromPath. Base64 variants are also available. This is a different reporting pipeline from native SpecRun template customization, so choose one reporting owner rather than trying to make both render the same trace token.

ReportPortal can centralize SpecFlow+ Runner results and supports .srprofile settings, including parallel-run configuration. It is an optional integration, not a requirement for screenshots in the native HTML report. Check current runner compatibility, licensing, and support before adopting a new reporting layer.
Or skip the browser setup
If your goal is a clean screenshot for a report rather than browser-driver control, ScreenshotNeo provides a one-call screenshot API. The response can be PNG, JPEG, WebP, or PDF, and the API supports full-page capture, element selectors, custom CSS and JavaScript, waits, device presets, dark mode, cookies, headers, geolocation, caching, and more.
Use the API key and target URL in one of these runnable examples. See the ScreenshotNeo documentation for the complete option list and response headers.
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(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await fs.promises.writeFile('shot.webp', buffer);
For SpecRun, save the returned bytes under the same artifact directory used by your report and print the resulting relative path in the trace. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents take screenshots, inspect page information, and capture PDFs. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and use the resulting image files as report artifacts.
Performance, reliability, and cost notes
- Screenshot time depends on browser startup, navigation, page resources, waits, and image encoding. The research available for this workflow does not provide a numeric benchmark, so measure your own suite.
- Capturing after every step increases disk usage and report rendering work. Failure-only capture usually gives the best CI trade-off.
- Keep screenshots as external files when reports are large. This keeps HTML parsing manageable and allows artifact retention policies to remove media independently.
- Use deterministic output roots and unique names so retries do not corrupt earlier evidence.
- For ScreenshotNeo, choose a cache TTL when repeated URLs are acceptable, and inspect
X-Page-VerdictandX-Billedto distinguish clean captures from failures and cache hits.
Checklist
- Screenshot is captured in an
[AfterStep]or[AfterScenario]hook. - File is saved below the published artifact root.
- Filename is unique under parallel execution.
- Path is emitted as a file URL or recognized marker.
- Custom Razor/CSHTML template renders the path as a relative image URL.
- HTML and screenshots are published together.
- Copied artifacts open successfully from a clean directory.
- Paths are sanitized and HTML-encoded.
FAQ
Can I make a screenshot clickable?
Yes. Render an anchor around the image, using the same relative path for both the anchor target and the image source. This lets a reader open the original-size file.
Do I need a screenshot for every step?
No. Capture every step for diagnosis, or capture only failed scenarios and selected tagged steps to reduce storage and report size.
Why does saving the file not attach it automatically?
SpecRun reports are generated from runner data and templates. The template must receive the path through trace output and turn it into HTML.
Should I use base64 or a file path?
Use file paths for portable artifact directories and smaller HTML. Use base64 only when a single self-contained document is a firm requirement.
Is SpecFlow+ Runner required for this pattern?
The native profile and template details are runner-specific. Confirm the current product name, version, compatibility, licensing, and support before starting a new implementation.


