ScreenshotNeo

BlogGuides

How to Test Modern Web Applications with Playwright

Set up Playwright, write resilient end-to-end tests, cover the browsers your users need, and debug failures locally and in CI.

By the ScreenshotNeo team4 October 202610 min read

Playwright Test lets you test a modern web application by driving real browser engines through user-visible workflows, then checking that the page reaches the expected state. It includes a test runner, assertions, fixtures for setup and isolation, parallel execution, and debugging and reporting tools. A practical starting point is a small set of isolated tests using accessible locators and web-first assertions, then an intentional browser matrix and reproducible CI run. Playwright installation and overview

1. Install Playwright and run a starter test

Playwright Test runs on Windows, Linux, and macOS and supports Chromium, Firefox, and WebKit, with mobile device emulation. Use the official initializer in an existing or new Node.js project. It asks whether to use TypeScript or JavaScript, where tests belong, whether to add a GitHub Actions workflow, and whether to install browser binaries. Installation guide

# npm
npm init playwright@latest

# yarn
# yarn create playwright

# pnpm
# pnpm create playwright

For the examples below, assume the initializer created playwright.config.ts and a tests/ directory. A minimal test navigates to a page and waits for its title to match:

import { test, expect } from '@playwright/test';

test('home page has the expected title', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000/');
  await expect(page).toHaveTitle(/Storefront/);
});

Save it as tests/home.spec.ts. The runner supplies the page fixture and manages its browser context and page lifecycle for the test. Start your application separately, then run:

npx playwright test
npx playwright show-report

Tests run headless by default. Useful local commands include npx playwright test --headed to see the browser, npx playwright test --project=chromium to select a configured project, npx playwright test tests/home.spec.ts to run one file, and npx playwright test --ui to explore tests interactively. With yarn use yarn playwright; with pnpm use pnpm exec playwright. The HTML report provides outcome filters, errors, attachments, and test details. Running tests and reports

2. Write tests around user behavior

Test what a user can see and do: navigate, submit a form, add an item, or receive a confirmation. Prefer locators based on accessible roles and names, labels, and visible text. They are less coupled to incidental markup such as CSS classes or deep DOM structure. Use a test ID when the team deliberately defines it as an automation contract. Playwright describes locators as central to its auto-waiting and retry behavior. Playwright best practices · Locators

import { test, expect } from '@playwright/test';

test('customer can add an item to the cart', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000/products/mug');
  await page.getByRole('button', { name: 'Add to cart' }).click();
  await expect(page.getByRole('status')).toContainText('Added to cart');
  await expect(page.getByRole('link', { name: /cart \(1\)/i })).toBeVisible();
});

Locators resolve against the current page state. Before actions such as clicking, Playwright checks actionability conditions, including whether the target is uniquely identified, visible, stable, enabled, and able to receive events. If a locator matches multiple elements, make it more specific by scoping or filtering rather than selecting an arbitrary match.

// Scope to the product row, then find its action.
const product = page.getByRole('listitem').filter({ hasText: 'Ceramic mug' });
await product.getByRole('button', { name: 'Add to cart' }).click();

// Use a test ID when it is the intentional test contract.
await page.getByTestId('checkout-submit').click();

Use web-first assertions such as toBeVisible(), toHaveText(), and toHaveURL(). These wait and retry until the expected state appears or the assertion times out. Avoid a one-time read followed by a plain assertion when the page updates asynchronously:

// Waits for the page to reach the expected state.
await expect(page.getByText('Welcome back')).toBeVisible();

// Avoid treating a transient immediate read as the eventual result.
// expect(await page.getByText('Welcome back').isVisible()).toBe(true);

Auto-waiting does not make an unpredictable test reliable by itself. Tests can still race with changing test data, shared accounts, uncontrolled network dependencies, or other tests that modify the same state. Make those inputs explicit and isolate state.

3. Use fixtures for setup and isolation

Playwright Test prepares only the fixtures a test requests and tears them down afterward. The built-in page fixture provides an isolated page; context gives access to the browser context for work such as permissions or storage state. This per-test isolation helps prevent cookies, storage, and page state from leaking between tests. Playwright fixtures

Put repeated setup in a fixture or hook when it improves clarity, and keep each test’s data predictable. A custom fixture can encapsulate a reusable signed-in state or page object; avoid a shared mutable account if parallel tests can change it.

import { test as base, expect } from '@playwright/test';

 type Fixtures = { dashboardUrl: string };

export const test = base.extend<Fixtures>({
  dashboardUrl: async ({}, use) => {
    await use('http://127.0.0.1:3000/dashboard');
  },
});

export { expect };

test('dashboard renders', async ({ page, dashboardUrl }) => {
  await page.goto(dashboardUrl);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

For larger suites, consider a dedicated test-data setup that creates uniquely identifiable records and removes or resets them safely. Keep authentication setup separate from assertions about authorization: reuse an authenticated state only for tests whose purpose is not to verify login itself.

4. Configure browser and device coverage deliberately

Projects let one suite run with different browsers, device profiles, base URLs, and other settings. Pick the matrix from the environments your application promises to support. Chromium, Firefox, and WebKit cover distinct browser engines; branded Chrome and Microsoft Edge are separate options, and Playwright can emulate mobile device settings. More projects mean more executions and CI time, so begin with a representative set and add coverage where it answers a real compatibility question. Browser projects and browser binaries · Test projects

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
  ],
});

Run just one project with npx playwright test --project=webkit. A project can also separate staging from production, or logged-in from logged-out tests, provided the environment and test data are appropriate. Avoid treating emulation as a substitute for every real-device check when the behavior depends on actual hardware or platform integration.

Playwright’s open-source Chromium build is not the same thing as branded Google Chrome. Browser binaries are tied to the Playwright package version. After updating Playwright, install the corresponding browser revisions again:

npm install -D @playwright/test@latest
npx playwright install

In a fresh Linux CI environment, install operating-system dependencies as well with npx playwright install --with-deps. If the suite only needs Chromium, installing only that browser can reduce browser downloads and disk use. Browser installation guidance

5. Run locally and debug failures

Use headed mode when the visible interaction matters, UI mode for watch-and-rerun feedback, and the Inspector to step through actions and inspect locators. For example:

npx playwright test --ui
npx playwright test tests/cart.spec.ts --debug
npx playwright test --project=webkit --headed

When a test fails intermittently, first find the failing step and determine whether the cause is a locator mismatch, an unexpected application state, slow or failed network work, shared test data, or resource contention. Prefer fixing the cause over adding arbitrary sleeps or simply increasing timeouts.

For CI failures, Playwright recommends traces for investigation. A trace includes a timeline, DOM snapshots around actions, and network request details. The common configuration trace: 'on-first-retry' captures a trace when a failed test is retried; tracing every run can impose substantial performance cost. For a local investigation, use npx playwright test --trace on and open the report with npx playwright show-report. You can also open a saved trace with npx playwright show-trace path/to/trace.zip. Playwright debugging guidance · Trace Viewer

Screenshots and video can provide visual context, but traces are usually more useful for locating the action and state transition that failed. Be mindful that traces and reports may contain page content, URLs, or network details from the test environment; handle artifacts according to your team’s data practices.

6. Make CI repeatable, then scale it

A CI job needs the locked Node dependencies, the browser binaries and system dependencies for its environment, and the test command. A basic sequence is:

npm ci
npx playwright install --with-deps
npx playwright test

Playwright recommends starting with one worker in CI for stability and reproducibility. Once tests are isolated and the runner has enough resources, increase workers or distribute the suite into shards across separate jobs. Sharding uses --shard=x/y; it is easiest to balance when tests can be distributed at test level, for example with fullyParallel: true. Save the HTML report and traces as CI artifacts so a failure can be inspected after the job ends. Provider configuration changes over time, so adapt the current official CI example to your CI service. Continuous Integration · Sharding

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  workers: process.env.CI ? 1 : undefined,
  retries: process.env.CI ? 1 : 0,
  reporter: process.env.CI ? 'html' : 'list',
  use: { trace: 'on-first-retry' },
});

After measuring the suite and CI capacity, set a higher worker count if the tests and application tolerate concurrent load. For example, npx playwright test --workers=4 limits a run to four workers. To shard across three jobs, run npx playwright test --shard=1/3, --shard=2/3, and --shard=3/3 in separate jobs. Sharding shortens elapsed time only when jobs run concurrently and the work is distributed reasonably.

7. Troubleshooting common Playwright failures

Symptom Likely cause Fix
Browser executable missing Package was installed or updated without installing its matching browser binaries. Run npx playwright install; in Linux CI use npx playwright install --with-deps. Keep package and browser revisions aligned.
Click times out The locator matches no element, multiple elements, or an element that is hidden, disabled, moving, or covered. Inspect the error and trace snapshot. Use a user-facing locator, scope it to the relevant region, and wait for the actual application state.
Assertion times out The expected state never became true, or the test checks the wrong state or page. Confirm navigation and test data, inspect the trace and network requests, then assert the state a user should observe.
Works locally but fails in CI Environment configuration, missing OS dependencies, different base URL, slower resources, or parallel interference. Reproduce with CI-like settings, install dependencies, verify environment variables, retain a trace on retry, and begin with one worker.
Test passes alone but fails in the suite Shared accounts or records, leaked external state, or execution order assumptions. Give tests independent data, use isolated fixtures, and remove order dependencies.
Only one browser project fails Browser-specific behavior or a project configuration / binary mismatch. Run that project alone, confirm its installed browser revision, and inspect the browser’s trace and console/network evidence.
Unexpectedly long suite Too many projects, slow setup, excessive tracing, or resource contention from workers. Choose a purposeful browser matrix, trace on retry, and increase workers or shard only after checking resources and test isolation.

8. Performance, reliability, and cost decisions

  • Browser matrix: each additional browser or device project repeats work. Cover the engines and profiles that correspond to supported users, then add projects to answer specific compatibility questions.
  • Parallel workers: more workers can reduce elapsed time when CPU, memory, and the application under test can handle the load. They can expose shared-state bugs or cause resource contention. One worker is Playwright’s recommended CI starting point.
  • Sharding: use separate CI jobs when a suite’s runtime justifies the added parallel CI capacity. Balance depends on test distribution; it is not an automatic speed guarantee.
  • Diagnostics: retain traces on retry or failure rather than tracing every passing run by default. Keep reports and traces long enough to investigate failures, while considering their contents and storage.
  • Repeatability: pin dependencies with a lockfile, install browser binaries for the installed Playwright version, control test data, and avoid tests that depend on order or uncontrolled third-party services.

Playwright itself is an open-source framework; your practical execution cost comes from the machines, CI minutes, and artifact retention used to run and inspect your suite. The dossier does not establish a universal runtime or cost benchmark, so measure against your own app and CI environment.

Or skip the browser setup

Playwright is the right tool for exercising interactive application behavior. When you need a clean screenshot of a page for a visual reference, documentation, or a simple capture workflow, ScreenshotNeo offers a website screenshot API and MCP server. It accepts one GET request for an image or PDF; see the ScreenshotNeo API documentation.

# 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}`);
  • Cookie banners are accepted like a visitor and removed along with supported consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools 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 screenshots. Every feature is on every plan.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Can I use Playwright with JavaScript instead of TypeScript?

Yes. The initializer supports either. TypeScript is not required; use JavaScript test files and the same Playwright Test APIs.

Does a passing Playwright test prove every browser is supported?

No. It proves the configured projects passed for that run. Configure the browser and device projects that reflect the compatibility commitments you need to validate.

Should every end-to-end test run on every pull request?

That depends on suite duration and CI capacity. Keep a useful fast feedback set and run broader browser coverage where your workflow can accommodate it; the right split is specific to the project.

When should I use a screenshot API instead of a browser test?

Use a browser test to verify behavior and interactions. Use a screenshot API when the deliverable is a captured page image or PDF rather than a test assertion about application behavior.