ScreenshotNeo

BlogGuides

Playwright Script Examples: Browser Automation, Testing, Network Mocking, and Screenshots

Runnable Playwright examples for navigation, locators, assertions, API mocking, debugging, and reliable browser automation.

By the ScreenshotNeo team1 October 20269 min read

Playwright scripts combine browser setup, navigation, user-like interaction, and a check of the resulting page state. The examples below cover both the Playwright Library and the Playwright Test runner, with patterns for reliable locators, asynchronous pages, network mocking, screenshots, debugging, and CI use.

Install the test runner with:

npm install -D @playwright/test
npx playwright install

For a standalone browser script, install the library:

npm install playwright
npx playwright install

1. A complete Playwright browser script

This standalone JavaScript program launches Chromium, opens a page, uses an accessible locator, verifies the destination, saves a screenshot, and always closes the browser.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage({
    viewport: { width: 1440, height: 900 },
  });

  try {
    await page.goto('https://example.com', {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });

    await page.getByRole('link', { name: 'More information' }).click();
    await page.waitForURL('**/iana.org/help/example-domains');
    await page.screenshot({ path: 'example.png', fullPage: true });

    console.log('Final URL:', page.url());
  } finally {
    await browser.close();
  }
})();

Playwright supports Chromium, Firefox, and WebKit. Replace chromium with firefox or webkit when you need another browser engine. The browser lifecycle shown here follows the official Browser API pattern (Browser API).

2. The same flow as a test-runner test

The test runner provides fixtures such as page, reporting, retries, parallel execution, and web-first assertions. A useful test checks an observable outcome after the action.

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

test('sign-in form accepts credentials', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('User Name').fill('John');
  await page.getByLabel('Password').fill('secret-password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByText('Welcome, John!')).toBeVisible();
});

The credentials are illustrative documentation values. Use test accounts or injected secrets in a real project. Playwright’s web-first assertions retry while checking the condition; the documented default assertion timeout is five seconds (Assertions).

Run the test with:

npx playwright test
npx playwright test tests/auth.spec.ts --project=chromium
npx playwright test --headed

3. Locators that survive UI changes

Prefer locators that describe how a user experiences the interface. They are evaluated when the action runs, which helps when a framework rerenders the DOM.

Preferred locator Example Use when
Role and accessible name getByRole('button', { name: 'Save' }) Buttons, links, headings, checkboxes, dialogs, and other semantic controls
Label getByLabel('Email') Form controls with associated labels
Text getByText('Order complete') Visible status or content
Placeholder getByPlaceholder('Search products') Inputs where a stable placeholder is part of the contract
Test ID getByTestId('cart-count') An explicit testing contract is more stable than visible copy
CSS or XPath locator('[data-state="open"]') Only when a user-facing locator or explicit test ID cannot express the target
await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByRole('heading', { name: /results/i })).toBeVisible();

Avoid long CSS and XPath chains tied to nesting or generated class names. They break when the DOM structure changes even though the user-visible behavior is unchanged. If several elements match, narrow the locator with filter, hasText, or a parent region:

const card = page.getByRole('article').filter({ hasText: 'Product 1' });
await card.getByRole('button', { name: 'Add to cart' }).click();

4. Clicking, filling, checking, and selecting

await page.getByLabel('Email').fill('developer@example.test');
await page.getByLabel('Subscribe to updates').check();
await page.getByLabel('Country').selectOption('US');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByTestId('status')).toHaveText('Submitted');

Actions wait for the target to become actionable. Keep the action followed by an assertion of the expected result instead of inserting a fixed sleep.

5. Waiting for navigation and asynchronous UI

Use a condition that represents the state your user or test actually needs:

await page.getByRole('button', { name: 'Load report' }).click();
await expect(page.getByRole('heading', { name: 'Monthly report' })).toBeVisible();
await expect(page.getByTestId('report-status')).toHaveText('Ready');

For a URL change, wait for the URL pattern. For an element that is created later, wait with a locator assertion:

await Promise.all([
  page.waitForURL('**/checkout'),
  page.getByRole('button', { name: 'Checkout' }).click(),
]);

await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible();

Use page.waitForLoadState('domcontentloaded') or networkidle only when that load state is meaningful for the page. A page can continue polling or streaming forever, so network idle is not a universal signal that the UI is ready.

6. Intercepting and mocking API requests

Routes can inspect, fulfill, modify, or abort HTTP and HTTPS requests, including XHR and fetch. Mocking makes a test deterministic; it does not provide coverage of the live service.

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

test('renders mocked products', async ({ page }) => {
  await page.route('**/api/products', route => route.fulfill({
    status: 200,
    contentType: 'application/json',
    json: [
      { id: 1, name: 'Product 1' },
      { id: 2, name: 'Product 2' },
    ],
  }));

  await page.goto('https://example.com/products');
  await expect(page.getByText('Product 1')).toBeVisible();
});

Modify a real response when you need most of the live payload:

await page.route('**/api/profile', async route => {
  const response = await route.fetch();
  const body = await response.json();
  await route.fulfill({
    response,
    json: { ...body, plan: 'test' },
  });
});

Abort unneeded resources to reduce noise in a focused test:

await page.route('**/*', route => {
  const type = route.request().resourceType();
  if (type === 'image' || type === 'font') return route.abort();
  return route.continue();
});

Register routes before goto so the first request is intercepted. Keep route patterns specific; a broad pattern can accidentally mock analytics, scripts, or the API calls your test is meant to exercise (Network).

7. Screenshots, PDFs, and element captures

await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
await page.locator('#invoice').screenshot({ path: 'invoice.png' });
await page.pdf({ path: 'invoice.pdf', format: 'A4', printBackground: true });

Full-page screenshots can be expensive on very long pages. If a page lazily loads images while scrolling, scroll it first and then capture:

await page.evaluate(async () => {
  await new Promise(resolve => {
    let y = 0;
    const step = 600;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        resolve();
      }
    }, 100);
  });
});
await page.screenshot({ path: 'lazy-loaded.png', fullPage: true });

8. Browser context options

Create a context to control settings shared by its pages:

const context = await browser.newContext({
  viewport: { width: 1280, height: 800 },
  deviceScaleFactor: 2,
  locale: 'en-US',
  timezoneId: 'America/New_York',
  colorScheme: 'dark',
  userAgent: 'PlaywrightExample/1.0',
  extraHTTPHeaders: {
    'X-Test-Run': 'example',
  },
});

await context.addCookies([
  {
    name: 'session',
    value: 'test-session',
    domain: 'example.com',
    path: '/',
  },
]);

const page = await context.newPage();

Use isolated contexts for independent users or test cases. Keep credentials in environment variables or CI secrets. Do not commit real cookies, bearer tokens, or passwords.

9. Forms, uploads, downloads, dialogs, and popups

await page.getByLabel('Attachment').setInputFiles('fixtures/report.csv');

const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Export CSV' }).click();
const download = await downloadPromise;
await download.saveAs('artifacts/report.csv');

page.on('dialog', dialog => dialog.accept());

const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open details' }).click();
const popup = await popupPromise;
await expect(popup).toHaveTitle(/Details/);

Start waiting for an event before the action that triggers it. This avoids races where the event happens before the listener is attached.

10. Debugging failed scripts

  1. Run headed: npx playwright test --headed.
  2. Open UI Mode: npx playwright test --ui.
  3. Pause with the Inspector: await page.pause().
  4. Capture a trace on failure in your Playwright configuration.
  5. Review the HTML Reporter after the run.

UI Mode and Inspector let you step through actions, inspect locators, and view DOM snapshots, logs, and network activity. The HTML Reporter helps inspect an individual failure (UI Mode).

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

export default defineConfig({
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  reporter: [['html', { outputFolder: 'playwright-report' }]],
});

11. Reliability and performance checklist

  • Use user-facing locators or explicit test IDs.
  • Assert the final state after each important action.
  • Prefer condition-based waits to fixed sleeps.
  • Use one browser process with isolated contexts when running many tests.
  • Mock unstable third-party APIs, while keeping separate integration coverage for the real service.
  • Reuse authenticated storage state for a test suite instead of logging in through the UI every time.
  • Set explicit navigation and assertion timeouts that match your application.
  • Capture traces, screenshots, and videos on failure rather than for every passing test.
  • Keep browser binaries and the Playwright package updated together.
  • Run Chromium, Firefox, and WebKit when browser-engine differences matter.

Parallel workers can reduce wall-clock time but may expose shared database, account, or file conflicts. Give each worker isolated data, or serialize tests that mutate the same resource.

12. Common errors and fixes

Symptom Likely cause Fix
Executable doesn't exist Browser binaries were not installed Run npx playwright install; install only the needed browser in CI if desired.
Locator matches multiple elements The locator is too broad Use an accessible name, filter, a parent region, or a test ID.
Timeout waiting for a locator Wrong state, selector, frame, or page Inspect with UI Mode, verify the URL, wait for the correct frame, and assert a visible condition.
Click intercepted An overlay or animation covers the target Wait for the overlay to disappear, close the dialog, or fix the test fixture. Avoid forcing clicks unless that is the behavior under test.
Navigation race The listener was attached after the click Start waitForURL or the event promise before the action, often with Promise.all.
Mock did not apply Route registered after the request, or pattern does not match Register before goto and log route.request().url() to verify the pattern.
Test passes locally but fails in CI Different browser, viewport, timing, locale, or data Pin configuration, collect a trace on retry, and remove dependence on shared state or fixed sleeps.
Blank or incomplete screenshot Fonts, images, lazy content, or app hydration are unfinished Wait for a meaningful UI assertion, load lazy content, and verify network responses needed by the page.

13. Or skip the browser setup

If your goal is a clean screenshot or PDF rather than a browser test, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets can be removed. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

See the ScreenshotNeo API documentation for all options, including full-page and element capture, dark mode, device presets, retina scale, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, authentication, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, bulk capture, usage, and the OpenAPI specification.

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,
)
r.raise_for_status()
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 failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', buffer);

ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools, so Claude, Cursor, and other MCP clients can capture pages. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

14. Frequently asked questions

Should I use Playwright Library or Playwright Test?

Use the Library for a focused script or service with its own lifecycle. Use the test runner when you need fixtures, assertions, retries, projects, parallelism, and reports.

It can help while debugging, but it is a fragile synchronization method. Prefer a locator assertion, URL wait, response wait, or another condition that represents the required state.

How do I test a page inside an iframe?

Use page.frameLocator('iframe').getByRole(...) or obtain the frame and then locate inside it. Confirm that the frame URL and loading behavior match your test.

Can Playwright run without a visible browser?

Yes. The default test and library examples run headless. Use headed mode or UI Mode when diagnosing a failure.

How should I keep screenshot output stable?

Fix the viewport, browser engine, locale, timezone, color scheme, test data, and fonts. Wait for the page’s meaningful ready state before capturing.