ScreenshotNeo

BlogGuides

Puppeteer Testing: How to Automate Browser Tests

Build reliable browser tests with Puppeteer: install a compatible browser, interact with pages, assert outcomes, and handle CI failures.

By the ScreenshotNeo team4 October 20268 min read

Puppeteer automates browser actions from JavaScript. A browser test typically launches or connects to a browser, opens your app, interacts with controls, checks the result with assertions from a test runner, and closes the browser. Puppeteer supplies browser control; your test runner supplies test organization and assertions.

For a straightforward local or CI setup, install puppeteer, which downloads a compatible Chrome during installation. Use locators for normal interactions because they wait for elements to appear and reach an appropriate state. See the official Getting Started guide and Page interactions guide.

1. Install Puppeteer and prepare your app

Start your web application separately, then install Puppeteer in the test project:

npm init -y
npm install --save-dev puppeteer

The puppeteer package downloads a compatible Chrome browser as part of installation. Choose puppeteer-core if your environment provisions the browser itself and you want to specify its executable path. The core package does not download a browser.

npm install --save-dev puppeteer-core

With puppeteer-core, your launch configuration must point at an installed compatible browser, for example:

const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH });

Use Node.js 22.12 or newer according to Puppeteer’s current system requirements. If you use TypeScript, the documented minimum is 5.0.1. Check the live system requirements for supported operating systems, architectures, and Linux dependencies; these can change.

2. Write a complete browser test

This runnable example uses Node’s built-in test runner and assertions. It assumes the app is already available at http://localhost:3000 and has a form with an accessible email field, a submit button, and a success message after submission. Change those selectors and expected text to match the app’s user-visible behavior.

// browser.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import puppeteer from 'puppeteer';

test('visitor can submit the signup form', async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });

    await page.locator('input[name="email"]').fill('dev@example.com');
    await page.locator('button[type="submit"]').click();

    const message = await page.locator('[role="status"]').wait();
    assert.equal(await message.evaluate(element => element.textContent.trim()), 'Thanks for signing up');
  } finally {
    await browser.close();
  }
});

Run it with:

node --test browser.test.js

The app server is a prerequisite, not something the browser test starts automatically. In a larger project, use the setup and teardown hooks from your existing runner to start the app, launch a shared browser where appropriate, and close resources after the suite.

3. Select elements and synchronize on behavior

Puppeteer locators are the recommended starting point for selecting and acting on page elements. They wait for an element to exist and be in an appropriate state for the requested action. Puppeteer supports CSS selectors by default and also documents text, XPath, accessibility attribute, and Shadow DOM selector syntax.

  • Prefer a selector that reflects a stable user-facing control, such as a label, role, or test attribute your application intentionally maintains.
  • Use fill() for form values, and click() for buttons and links.
  • Wait for a result that proves the user action worked, such as a status message or changed page content.
  • Avoid fixed sleeps as the normal synchronization strategy. They can waste time when the page is fast and still fail when it is slow.

page.waitForSelector() is a lower-level alternative when you need an explicit wait. It returns an element handle; you must still perform the action and manage the handle, and it does not provide locator-style action retries. Use it when its lower-level control is useful rather than as a blanket replacement for locators.

4. Choose the browser and headless mode

Puppeteer pairs releases with browser versions to reduce protocol mismatches. Its supported-browser table documents the browser versions associated with each Puppeteer release. Puppeteer guarantees its Chrome for Testing binaries; an arbitrary system Chrome may not match the expected version. Check the live supported browsers table when pinning or diagnosing a version.

Mode When to use it Tradeoff
Regular headless (default) Typical automated runs, including CI No visible browser window
Headful: { headless: false } Debugging a failure by watching the browser Requires a display environment or suitable virtual display in CI
Headless shell: { headless: 'shell' } Automation where the separate Chrome headless shell is suitable May be faster for some automation, but does not fully match regular Chrome behavior

For a visible debugging run, change the launch call to puppeteer.launch({ headless: false }). When selecting headless shell for speed, confirm that the behavior you care about matches the target browser. The distinction is described in Puppeteer’s headless modes guide.

5. Configure browser installation and CI

Make browser ownership explicit so local development and CI use the same approach:

  1. Using puppeteer: allow its install process to download the paired browser, and cache the browser download directory in CI if your build system supports it.
  2. Using puppeteer-core: install a compatible browser in the CI image or job and set executablePath to that binary.
  3. Pin dependencies: commit the lockfile and use the same dependency installation method in CI as locally.
  4. Check download policy: if downloads are skipped or restricted, make sure the expected browser is installed separately.
  5. Review platform needs: verify OS packages, architecture, and archive tools against the official requirements for the runner image.

Puppeteer configuration supports choosing a default browser, executable path, browser cache directory, and whether downloads are skipped. The configuration guide documents the corresponding environment variable overrides. For managed browser binaries, the @puppeteer/browsers CLI and API can install and manage browsers. See the configuration guide before changing cache or download settings.

6. Keep tests reliable and maintainable

  • Assert visible outcomes: verify the state a user would observe instead of relying only on internal implementation details.
  • Use action-aware waiting: locator actions wait for readiness; then wait for a meaningful result when the action triggers asynchronous work.
  • Clean up on failure: close the browser in finally or the test runner’s teardown hook so an assertion failure does not leave browser processes running.
  • Keep test data controlled: reset or isolate accounts and records that could make repeated runs depend on prior runs.
  • Debug in headful mode: reproduce the failing path with headless: false when seeing the page will clarify whether the issue is setup, timing, or application behavior.
  • Keep runtime and browser choices reproducible: pin package versions through the lockfile and use the browser version paired with Puppeteer.

These practices reduce avoidable sources of nondeterminism, but no browser automation setup can make an application or its external services deterministic by itself.

7. Puppeteer and test runners

Puppeteer is the browser-control layer. A test runner usually provides test discovery, suite structure, setup and teardown, reporting, and assertion conventions. This example uses Node’s built-in runner; teams may instead use a JavaScript test framework or an integration such as the community jest-puppeteer project mentioned in Puppeteer’s FAQ. Check the integration’s own compatibility guidance before adopting it.

If your requirements include browser automation from several programming languages or broader orchestration, Puppeteer’s FAQ points to Selenium as a broader project in those areas. This is a scope distinction, not a benchmark or a complete framework comparison. See the Puppeteer FAQ.

8. Remote browser connections

For an advanced architecture, a browser-compatible Puppeteer build can connect to a separately running browser through a WebSocket endpoint. This is different from launching a local browser: the remote environment owns the browser process, browser version, and connectivity. Use the browser-compatible entry point and follow Puppeteer’s connection guidance for the browser endpoint you control. Ordinary Node-based UI tests do not need this architecture.

9. Troubleshooting common failures

Symptom Likely cause What to check
Browser executable not found Browser download was skipped, cache is missing, or puppeteer-core has no configured browser Confirm the install policy and cache; install a compatible browser or set executablePath.
Browser fails to launch in Linux CI Missing system libraries or unsupported runner image Compare the runner OS and architecture with the live system requirements and install the documented dependencies.
Protocol or browser compatibility error The installed browser does not match the Puppeteer release Use the paired Chrome for Testing version or select a matching Puppeteer release from the support table.
Test times out waiting for a control Wrong selector, app not ready, navigation failed, or control is inside a different frame/shadow root Confirm the app URL and selector in a visible browser; inspect the relevant frame or selector syntax and wait for the user-visible state.
Click appears to do nothing Click target is obscured, disabled, or the app has not reached the expected state Check the rendered page, confirm the control can be acted on, and assert a result after the click.
Works locally, fails in CI Different Node version, browser binary, cache, OS dependencies, environment variables, or external service timing Compare runtime and browser versions, installation settings, executable path, environment, and network dependencies.
Browser process remains after a failed test Cleanup is skipped on an exception Put browser closure in finally or a runner teardown hook.

10. Performance, reliability, and cost

Puppeteer itself is a library rather than a per-screenshot hosted service, so this setup has no ScreenshotNeo-style per-capture plan charge. Your practical costs are the compute and time used by your own browser environment, plus the engineering and maintenance needed to provision compatible browser binaries and keep CI stable. This guide makes no benchmark claim: runtime depends on the app, browser, runner resources, network, and test design.

For faster feedback, keep each test focused on a user flow, avoid unnecessary fixed delays, reuse setup thoughtfully within the runner, and cache browser downloads where appropriate. For reliability, keep versions aligned, close processes, and distinguish browser provisioning failures from application failures in CI logs.

Or skip the browser setup

If your goal is to capture a page rather than test interactions, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Its cookie/consent handling accepts banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. AI agents can use its MCP server tools to take screenshots, inspect page information, and capture PDFs.

See the ScreenshotNeo API documentation. Example cURL request:

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo includes 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. Sign up for free and get 1,000 screenshots a month with no card.

FAQ

Does Puppeteer include assertions?

Puppeteer controls the browser. Use assertions from your test runner or assertion library to check whether the app behaved as expected.

Can I use Puppeteer without downloading Chrome?

Yes. Use puppeteer-core and provide a compatible browser through your environment and launch configuration.

Should I use headless shell for every CI test?

No. Choose it only after checking that its behavior is suitable for the browser behavior your test needs to cover.

Can Puppeteer take screenshots?

Yes. Puppeteer can capture browser pages as part of its browser-control API. If you need a standalone screenshot API call instead of managing a browser test environment, ScreenshotNeo provides that service.