ScreenshotNeo

BlogHow-to

How to Capture Screenshots and Save Test Results with Selenium WebDriver in Node.js

Capture Selenium screenshots as PNG files, preserve runner results, avoid filename collisions, and keep artifacts reliable in local and CI runs.

By the ScreenshotNeo team30 September 20268 min read

How to Capture Screenshots and Save Test Results with Selenium WebDriver in Node.js

Direct answer: Selenium WebDriver’s JavaScript binding returns a screenshot from await driver.takeScreenshot() as a Base64-encoded PNG string. Save it with Node’s Base64 encoding option, for example await writeFile('artifacts/home.png', encodedPng, 'base64'). Test results are separate: Selenium captures browser evidence, while your test runner produces pass/fail data. Keep those concerns separate, give every test a unique artifact name, await every write, and upload the artifact directory in CI.

This guide shows a complete Node.js implementation, element screenshots, failure hooks, JSON result files, synchronization, cleanup, troubleshooting, and CI considerations. The Selenium JavaScript binding currently documents Node.js 22 or newer; check the official API documentation before pinning versions.

1. Install Selenium and choose a browser

Create a project and install the Selenium binding:

mkdir selenium-artifacts
cd selenium-artifacts
npm init -y
npm install selenium-webdriver

You also need a browser and a compatible driver. Selenium Manager can resolve drivers in many current setups, but your CI image still needs the browser installed. The example below uses Chrome. Use Browser.FIREFOX or another supported browser when your test matrix requires it.

2. Capture and save a page screenshot

takeScreenshot() resolves to a Base64 PNG. Passing 'base64' to the filesystem API decodes the string into image bytes. Without that encoding, Node writes the Base64 characters as text and the resulting file is not a valid PNG.

const { Builder, Browser } = require('selenium-webdriver');
const { writeFile, mkdir } = require('node:fs/promises');
const path = require('node:path');

async function saveScreenshot(driver, filePath) {
  const base64Png = await driver.takeScreenshot();
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, base64Png, 'base64');
}

(async () => {
  const driver = await new Builder().forBrowser(Browser.CHROME).build();
  try {
    await driver.get('https://example.com');
    await saveScreenshot(driver, path.join('artifacts', 'example.png'));
  } finally {
    await driver.quit();
  }
})();

The same API is available with ES modules:

import { Builder, Browser } from 'selenium-webdriver';
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';

const driver = await new Builder().forBrowser(Browser.CHROME).build();
try {
  await driver.get('https://example.com');
  const png = await driver.takeScreenshot();
  const output = path.join('artifacts', 'example.png');
  await mkdir(path.dirname(output), { recursive: true });
  await writeFile(output, png, 'base64');
} finally {
  await driver.quit();
}

Selenium’s JavaScript API describes the result as a Base64-encoded PNG. Its window and tab documentation also demonstrates writing the returned value to a file.

3. Understand what Selenium captures

The generic method is a best effort. Selenium prefers, in order, the entire page, the current window, the visible portion of the current frame, or the whole display containing the browser. The exact result depends on the browser, driver, window state, and operating system. Do not assume that every driver produces a full-page image.

A Selenium test produces a screenshot artifact while the runner produces structured results.
A Selenium test produces a screenshot artifact while the runner produces structured results.

Capture only after the state you need is present. An explicit wait is more reliable than an arbitrary sleep:

const { By, until } = require('selenium-webdriver');

await driver.get('https://example.com/dashboard');
await driver.wait(until.elementLocated(By.css('main.dashboard')), 15000);
await driver.wait(until.elementIsVisible(
  driver.findElement(By.css('main.dashboard'))
), 15000);
await saveScreenshot(driver, 'artifacts/dashboard.png');

If an application renders content after an API call, wait for a visible state that proves the data is ready. A screenshot taken immediately after navigation may contain a loading shell.

4. Capture an element for focused failure evidence

For a component-level artifact, locate an element and call element.takeScreenshot(true):

const { By } = require('selenium-webdriver');

const chart = await driver.findElement(By.css('[data-testid="sales-chart"]'));
const chartPng = await chart.takeScreenshot(true);
await mkdir('artifacts', { recursive: true });
await writeFile('artifacts/sales-chart.png', chartPng, 'base64');

Element screenshots are useful when the full page is noisy or when a failed assertion concerns one widget. The element must exist and be renderable; hidden, detached, or off-screen elements can produce driver-specific errors.

5. Save screenshots from a test failure

Failure capture belongs in your runner’s hook, because the runner knows the test title and outcome. Selenium itself does not create test reports. The pattern below is framework-neutral: pass the active driver and test metadata to a helper, then rethrow the original error.

const { writeFile, mkdir } = require('node:fs/promises');
const path = require('node:path');

function safeName(value) {
  return value
    .normalize('NFKD')
    .replace(/[^a-zA-Z0-9._-]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 140) || 'unnamed-test';
}

async function captureFailure(driver, testTitle, workerId = 'worker-0') {
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
  const fileName = `${safeName(testTitle)}-${safeName(workerId)}-${stamp}.png`;
  const filePath = path.join('artifacts', 'screenshots', fileName);
  await saveScreenshot(driver, filePath);
  return filePath;
}

async function runTestWithEvidence(driver, testTitle, testBody) {
  try {
    await testBody();
    return { title: testTitle, status: 'passed' };
  } catch (error) {
    let screenshotPath;
    try {
      screenshotPath = await captureFailure(driver, testTitle);
    } catch (captureError) {
      console.error('Screenshot capture failed:', captureError);
    }
    throw Object.assign(error, { screenshotPath });
  }
}

Preserve the original exception. If screenshot capture fails, log that secondary problem without replacing the assertion error that explains the test failure.

6. Persist structured test results

A screenshot is an image artifact; a result file is structured data. Use your runner’s reporter when a standard format is sufficient. For example, Mocha provides a JSON reporter; consult the runner documentation for the version you install.

When downstream tooling needs a project-specific shape, write an intentional summary:

const { writeFile, mkdir } = require('node:fs/promises');

const resultSummary = {
  runId: process.env.CI_JOB_ID || `local-${Date.now()}`,
  title: 'dashboard loads sales chart',
  status: 'failed',
  durationMs: 1842,
  browser: 'chrome',
  timestamp: new Date().toISOString(),
  error: {
    message: 'Expected chart heading to be visible',
    stack: '...'
  },
  screenshotPath: 'artifacts/screenshots/dashboard-loads-sales-chart-worker-0.png'
};

await mkdir('artifacts/results', { recursive: true });
await writeFile(
  'artifacts/results/dashboard-loads-sales-chart.json',
  JSON.stringify(resultSummary, null, 2),
  'utf8'
);

Useful fields include a build or run identifier, full test title, outcome, duration supplied by the runner, error message and stack, browser capabilities, timestamp, and screenshot path. These are application choices, not fields Selenium automatically supplies.

7. Avoid collisions and unsafe writes

Use one file per test, worker, and attempt. A title alone is not unique when a suite retries or runs in parallel. Include a sanitized title plus a timestamp, retry number, or worker identifier. Node documents that overlapping writeFile calls on the same file are unsafe; always await a write before starting another write to that path. For a single aggregate report, collect summaries in memory and write once after the run.

Do not use a shared append file from parallel workers without deliberate coordination. Separate worker directories are simpler:

artifacts/
  screenshots/
    checkout-adds-card-worker-1.png
  results/
    worker-1.json

At the end of the run, merge worker JSON files in a separate process if your CI consumer requires one report.

8. Cleanup and CI artifact handling

Always quit the driver in a finally block, including when navigation, assertions, or screenshot writing throws. Capture before driver.quit(); after quitting, browser commands are invalid.

let driver;
try {
  driver = await new Builder().forBrowser(Browser.CHROME).build();
  await runSuite(driver);
} finally {
  if (driver) await driver.quit();
}

In CI, configure the platform’s artifact upload step to collect artifacts/. Retention periods, upload syntax, and whether artifacts survive a failed job are CI-platform settings; Selenium does not define them. Upload on failure as well as success so screenshots remain available for diagnosis.

9. Common errors and fixes

Error or symptom Cause Fix
PNG cannot be opened Base64 text was written as UTF-8 Pass 'base64' to writeFile or writeFileSync.
ENOENT when saving Parent directory does not exist Run mkdir(path.dirname(file), { recursive: true }) first.
Screenshot is blank or shows a loader Capture ran before the page reached the expected state Wait for a specific element or condition after navigation.
Only the viewport is captured Driver/browser does not provide generic full-page capture Verify behavior in the target environment and use a browser-specific full-page strategy when required.
Element screenshot fails Element is hidden, detached, or not yet located Wait for location and visibility; reacquire stale elements.
Original assertion is missing Failure-capture code replaced the original exception Catch capture errors separately and rethrow or preserve the original error.
Parallel tests overwrite files Names are not unique Include sanitized title, worker, retry, and timestamp components.
Driver cannot start Browser/driver is absent or incompatible Install the browser in the runtime and check Selenium Manager or explicit driver configuration.

10. Performance, reliability, and cost notes

  • Capture only when evidence is useful. A PNG adds I/O and storage to every test, so many suites capture on failure and a small set of checkpoints.
  • Use explicit waits tied to application state. Fixed delays slow passing tests and still fail when environments are slower than expected.
  • Keep screenshots and JSON on local disk during a run, then upload once. Repeated network uploads inside each test increase failure points.
  • Choose a stable viewport and browser configuration so visual differences are meaningful.
  • For parallel runs, isolate browser instances and artifact paths per worker.
  • Screenshot cost is normally your CI storage and execution time; Selenium does not charge per capture. Your CI provider controls storage limits and retention.

11. Or skip the browser setup

If you need URL screenshots rather than browser-driven interaction, ScreenshotNeo provides a GET API and an MCP server. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status.

A screenshot service can remove common overlays before capturing a page.
A screenshot service can remove common overlays before capturing a page.

See the ScreenshotNeo API documentation for all options. A one-call capture looks like this:

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

ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDFs, custom CSS and JavaScript, clicks, waits, request blocking, headers and cookies, timezone and geolocation, transparency, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture, usage reporting, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.

12. FAQ

Does Selenium return a PNG Buffer?

No. The JavaScript binding returns a Base64-encoded PNG string. Decode it when writing the file.

Can Selenium guarantee a full-page screenshot?

No. The generic API makes a best effort and the scope depends on browser and driver support. Verify the behavior you need in your target environment.

Does Selenium generate JUnit or JSON test results?

No. Your selected runner and reporter own result formats. You can also serialize a custom summary with Node’s filesystem APIs.

Should every test save a screenshot?

Usually capture failures and a few diagnostic checkpoints. Saving every image increases runtime and artifact storage.

Why await filesystem writes?

Awaiting prevents later operations from racing the write. Node warns that overlapping writes to the same file are unsafe.