ScreenshotNeo

BlogHow-to

How to Build a Screenshot Script for Browser Automation

Build repeatable browser screenshots with Playwright, Puppeteer, or Cypress. Learn when to wait, capture elements or full pages, and save reliable CI artifacts.

By the ScreenshotNeo team29 September 202610 min read

How to Build a Screenshot Script for Browser Automation

A reliable browser screenshot script launches a browser, opens an isolated page, navigates to a URL, waits for the page’s meaningful ready state, captures the viewport, full page, or a specific element, then saves the image or passes its bytes onward. For repeatable results, set the viewport explicitly, wait for application state instead of relying on a fixed sleep, and close the browser in a finally block.

This guide uses Playwright for a complete JavaScript example, then covers equivalent Puppeteer and Cypress approaches, screenshot options, CI artifacts, failure diagnosis, and cost and performance tradeoffs.

1. Choose the capture shape and wait condition

Before writing the script, decide what the image is for. A viewport capture records what a user sees at the current scroll position. A full-page capture includes the scrollable document. An element capture isolates a particular component, such as a navigation bar or order summary. These are different outputs; full-page images can be very tall, while element images can fail if the target is hidden, detached, or outside a stable layout.

A repeatable capture moves from navigation to a meaningful ready state and then to the required image shape.
A repeatable capture moves from navigation to a meaningful ready state and then to the required image shape.
Goal Capture mode Typical use
Check the initial fold Viewport Visual regression for a fixed viewport
Review the entire document Full page Page review or archival
Inspect a component Locator or element Component snapshots, receipts, charts

Then identify a page-specific readiness condition: a heading visible, a loading indicator gone, or a known result count populated. “Navigation finished” does not necessarily mean the app finished rendering. Likewise, waiting for network idle can be unsuitable for pages that keep polling or stream data. Prefer an observable state that matches the content you intend to capture.

2. Build a reusable Playwright screenshot script

Install Playwright and its Chromium browser in your project:

npm install --save-dev playwright
npx playwright install chromium

Save this as capture.js. It accepts a URL, creates the output directory, waits for a useful page condition, and writes deterministic viewport, full-page, and element captures. The element capture is attempted only if the configured selector exists.

const { chromium } = require('playwright');
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const target = process.argv[2] || 'https://example.com';
  const outDir = path.resolve('artifacts');
  await fs.mkdir(outDir, { recursive: true });

  const browser = await chromium.launch();
  try {
    const context = await browser.newContext({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1,
    });
    const page = await context.newPage();
    const response = await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 });
    if (response && !response.ok()) {
      throw new Error(`Navigation returned HTTP ${response.status()}`);
    }

    // Replace this with a locator meaningful for your application.
    await page.locator('body').waitFor({ state: 'visible', timeout: 15000 });
    await page.screenshot({ path: path.join(outDir, 'page.png') });
    await page.screenshot({ path: path.join(outDir, 'page-full.png'), fullPage: true });

    const targetElement = page.locator('header').first();
    if (await targetElement.count()) {
      await targetElement.screenshot({ path: path.join(outDir, 'header.png') });
    }
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with node capture.js https://example.com. The finally block closes Chromium even after navigation or capture errors. For a real application, replace the generic body wait with a semantic locator, such as page.getByRole('heading', { name: 'Dashboard' }).waitFor(). A strong readiness condition should indicate that the content under test is present, not merely that some element exists.

Use a stable state rather than a sleep

Fixed delays are easy to write but unreliable: too short on a slow run, wasteful on a fast one. Wait for the exact data or UI state you need. If animations make comparisons unstable, disable or await them according to the framework’s screenshot controls. Mask timestamps, user-specific values, and other volatile regions where the framework supports it, or render deterministic test data.

3. Capture with Playwright options

Playwright supports screenshots from a page and from a locator. Full-page capture includes the full scrollable page. A locator screenshot captures the element’s bounding area. For example:

await page.screenshot({ path: 'artifacts/full.png', fullPage: true });
await page.locator('[data-testid="order-summary"]').screenshot({
  path: 'artifacts/order-summary.png'
});

For processing or uploading, omit path and retain the returned buffer:

const pngBytes = await page.screenshot({ fullPage: true });
// Example handoff: pass pngBytes to your storage or image-processing client.

Useful documented controls include clip for a rectangular region, mask and maskColor for covering matching locators, omitBackground for transparency, quality for lossy formats, scale for CSS-size or device-scale output, and animation controls. Check the [Playwright screenshot API](https://playwright.dev/docs/screenshots) and [Page API](https://playwright.dev/docs/api/class-page) for current details and valid combinations.

PNG is a sensible default for pixel comparison because it is lossless. JPEG or WebP may be smaller when exact pixel matching is not the goal. Quality settings apply to lossy formats; keep dimensions and device scale fixed when comparing results. A retina scale produces more pixels and larger files, which affects artifact storage and transfer time.

4. Puppeteer alternative

Puppeteer follows the same lifecycle: launch, create a page, navigate, wait for a meaningful state, capture, close. Install it and its browser according to the [official Puppeteer guide](https://pptr.dev/guides/installation):

npm install puppeteer
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900 });
    const response = await page.goto('https://example.com', {
      waitUntil: 'domcontentloaded',
      timeout: 30000,
    });
    if (response && !response.ok()) {
      throw new Error(`Navigation returned HTTP ${response.status()}`);
    }
    await page.waitForSelector('body');
    await page.screenshot({ path: 'artifacts/example.png', fullPage: true });
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Puppeteer’s Page.screenshot() returns a Uint8Array unless an encoding is requested; it can also write directly to a path. See the [Puppeteer Page API](https://pptr.dev/api/puppeteer.page.screenshot) for options. Create the output directory before capture if it may not exist. Keep the browser open only as long as necessary, and await screenshot completion before reusing or closing resources.

5. Cypress alternative for test-run screenshots

Cypress is useful when the screenshot belongs to an existing end-to-end test. The following example waits for a specific checkout component and captures the full page while blacking out an email field:

it('captures the checkout state', () => {
  cy.visit('/checkout');
  cy.get('[data-testid="order-summary"]').should('be.visible');
  cy.screenshot('checkout', {
    capture: 'fullPage',
    blackout: ['[data-testid="email"]'],
    overwrite: true,
  });
});

Cypress supports viewport, full-page, runner, and element screenshots. Relevant options include clipping, blackout selectors, padding, overwrite behavior, and animation or timer controls. Cypress saves screenshots under cypress/screenshots by default. During cypress run, it captures failure screenshots unless screenshotOnRunFailure is disabled. See the [Cypress screenshot API](https://docs.cypress.io/api/commands/screenshot) and [Cypress test configuration guide](https://docs.cypress.io/app/guides/screenshots-and-videos).

6. Make screenshots repeatable in CI

A screenshot useful for debugging or regression review needs a predictable environment as well as a script. Use this checklist:

  • Set the viewport and device scale factor explicitly.
  • Use fixed test data and a stable account or fixture.
  • Wait for a meaningful locator or application signal.
  • Disable or settle animations when visual stability matters.
  • Mask personal data, timestamps, tokens, and dynamic regions.
  • Use deterministic file names and create the destination directory.
  • Upload the screenshot directory as a CI artifact, including failure images.
  • Keep screenshots from failed runs long enough to diagnose intermittent issues.

For parallel tests, include a test identifier or worker identifier in file names so captures do not overwrite one another. Enable overwrite only when replacement is intended. If the goal is a visual diff, keep browser version, viewport, fonts, locale, and test data controlled; otherwise an environment change can look like an application change.

7. Troubleshooting common failures

Symptom Likely cause Fix
Capture is blank or incomplete Script took the image before the app rendered its content. Wait for an app-specific locator or data-ready state; inspect navigation errors and console output.
Navigation times out The page is slow, blocked, or never reaches the chosen lifecycle state; network-idle waits can hang on persistent connections. Use a suitable navigation condition such as DOM content loaded, then wait separately for the required UI. Set a considered timeout and report the URL on failure.
Element screenshot says target is not visible Selector matched a hidden, zero-size, detached, or not-yet-rendered node. Use a specific selector, wait for visibility, and confirm the element exists in the current page state.
Images or lazy content are missing Below-the-fold resources have not loaded or the capture happened too early. Use a full-page capture and wait for image/content readiness required by the page; verify lazy-loaded content appears after scrolling if the app requires it.
Images differ between runs Animations, timestamps, random data, fonts, viewport, or asynchronous content vary. Stabilize fixtures and dimensions, settle or disable animation, and mask dynamic regions.
File write fails Output directory does not exist or CI user lacks write permission. Create the directory first, use a resolved path, and verify the CI workspace is writable.
Browser fails to launch in CI Browser binaries or required runtime dependencies are absent, or the container configuration differs. Install the framework’s browser build in the CI image and follow the framework’s official CI setup guidance.
Screenshot file is unexpectedly huge Full-page dimensions, retina scale, or lossless format produce many pixels. Capture only the needed area, reduce scale or dimensions, or choose a lossy format when acceptable.

8. Performance, reliability, and cost

The browser startup is often avoidable overhead when one process captures many pages: reuse a browser process, but give each job an isolated context and close pages and contexts after use. This balances startup cost with separation of cookies and local storage. For a single capture, the simpler launch-and-close lifecycle is easier to reason about. Limit parallel captures to the capacity of the runner; each browser page consumes resources, and oversized full-page images increase memory, encoding, and upload work.

Choose the narrowest capture that answers the question. An element image is usually easier to store and compare than an entire long document. Use PNG where pixel fidelity matters, and a compressed format where bandwidth or retention matters more. Keep CI artifact retention aligned with debugging needs and storage policy. Browser automation itself generally has no per-screenshot API fee, but it still consumes compute time, storage, and CI minutes. Hosted runners or browser services may charge under their own pricing; consult their current terms rather than assuming a universal rate.

Reliability comes from explicit waits, controlled inputs, bounded timeouts, and cleanup. A capture failure should fail the job when the screenshot is a required test output. For optional diagnostics, catch the capture error, preserve the original test failure, and record enough context to investigate. Avoid retrying blindly: retries can hide race conditions and make results harder to interpret.

9. Or skip the browser setup

If you need a screenshot of a public URL without maintaining a browser runtime, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF; see the [ScreenshotNeo docs](https://screenshotneo.com/docs/) for parameters and response details.

ScreenshotNeo can remove common consent banners, popups, and chat widgets before capture.
ScreenshotNeo can remove common consent banners, popups, and chat widgets before capture.
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 request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. 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 a month with no card; paid plans start at $5 for 3,000. All features are available on every plan. Try [ScreenshotNeo free](https://screenshotneo.com/account/sign-up/).

10. FAQ

Can a screenshot script capture a page that requires login?

Yes, browser automation can use an authenticated test context or sign-in flow. Keep credentials in CI secrets, use test accounts, and avoid exposing private data in retained artifacts. A URL screenshot API may not share your browser session unless it supports the required authentication configuration.

Should I save the image to disk or use bytes?

Save to disk when CI should retain an artifact or a human needs to inspect it. Keep bytes when the next step uploads, analyzes, or transforms the image without an intermediate file.

How do I capture just part of the viewport?

Use a locator screenshot for a semantic element, or a rectangular clip when you need a precise region. Prefer a locator when layout movement could make fixed coordinates brittle.

Which framework should I use?

Use the framework already running your tests when the image is test evidence. Playwright and Puppeteer support standalone browser scripts; Cypress screenshots fit naturally into Cypress test runs. Choose based on runtime, browser needs, capture controls, and existing CI workflow.