Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The fastest way to write a useful Playwright script is to build one complete user flow: install Playwright and its browser binaries, open a page, locate controls by the way a user sees them, perform an action, assert the visible result, and close the browser. This guide uses JavaScript with Node.js and shows both a standalone script and a Playwright Test version, plus debugging, locator, waiting, isolation, and failure-recovery practices.

What you will build

The example below opens a browser, visits a page, clicks a link, verifies that the destination is visible, and closes the browser. The URL and link name are deliberately simple; replace them with controls and outcomes from your own application. A test is valuable only when it can fail if the user-facing behavior is broken.

  • Standalone script: you control browser startup and cleanup yourself.
  • Test runner: Playwright Test manages fixtures, contexts, assertions, reporting, and lifecycle.

Playwright can launch Chromium, Firefox, and WebKit from a Node.js script. The examples use Chromium unless a command says otherwise.

Install Playwright and its browsers

Start a Node.js project

  1. Install a current Node.js release and create a directory for the automation.
  2. Run npm init -y inside that directory.
  3. Install the library with npm install playwright.
  4. Download the browser binaries with npx playwright install. Without this step, the package may be installed while the executable needed to launch a browser is missing.

For a test suite rather than a one-off script, install the runner with npm init playwright@latest. Follow the prompts for JavaScript or TypeScript, test-folder location, and whether to add a continuous-integration workflow. The generated project includes a configuration file and an example test.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Write a minimal standalone JavaScript script

Create smoke.js

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

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

    await page.getByRole('link', { name: 'More information' }).click();
    await page.getByRole('heading', { name: /iana/i }).waitFor();

    console.log('The destination heading is visible.');
  } finally {
    await browser.close();
  }
})();

Run it with node smoke.js. The try/finally ensures the browser closes even when navigation, clicking, or verification throws an error. In your application, replace the illustrative URL, accessible link name, and heading with the real user journey.

Use a visible assertion instead of a log

A log confirms only that execution reached that line. A web-first assertion waits and retries until the expected condition is true, or fails with a useful timeout. Add the assertion library and use it in a script like this:

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

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com');
    await page.getByRole('link', { name: 'More information' }).click();
    await expect(page.getByRole('heading', { name: /iana/i })).toBeVisible();
  } finally {
    await browser.close();
  }
})();

Do not replace this with expect(await locator.isVisible()).toBe(true). That expression checks once, immediately, and can race a UI that is still rendering.

Choose locators that survive UI changes

Locators express how a user identifies an element. Prefer, in this order, a role and accessible name, a label, visible text, or a deliberate test id that represents a stable contract.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Role and accessible name

await page.getByRole('button', { name: 'Save changes' }).click();
await page.getByRole('textbox', { name: 'Email address' }).fill('dev@example.test');

Labels and text

await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
await expect(page.getByText('Profile updated')).toBeVisible();

Test ids for deliberate contracts

await page.getByTestId('cart-submit').click();

Use a test id when the accessible wording is not stable or when a component has no useful semantic role. Avoid generated CSS classes, long XPath expressions, and selectors tied to deep DOM structure. Those selectors describe implementation details rather than the behavior an end user depends on.

Chain and filter to narrow a component

const row = page.getByRole('listitem').filter({ hasText: 'Ada Lovelace' });
await row.getByRole('button', { name: 'Remove' }).click();

Locators auto-wait and retry actionability checks. A click therefore waits for the element to be attached, visible, enabled, and ready to receive the action.

Navigation, actions, and assertions in a realistic flow

Keep setup, action, and outcome distinct so a failure tells you what broke.

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

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const context = await browser.newContext({
      viewport: { width: 1440, height: 900 },
      locale: 'en-US'
    });
    const page = await context.newPage();

    await page.goto('https://your-app.example/login', {
      waitUntil: 'domcontentloaded',
      timeout: 30_000
    });
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
    await page.getByRole('button', { name: 'Sign in' }).click();

    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await page.getByRole('link', { name: 'Settings' }).click();
    await expect(page).toHaveURL(//settings$/);
    await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
  } finally {
    await browser.close();
  }
})();

Keep credentials in environment variables or a test secret store, not in source control. If a test changes server data, create the required data during setup and remove it afterward so another run cannot inherit a hidden dependency.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Turn the flow into a Playwright Test

The runner is usually the better choice for a maintainable end-to-end suite because it creates isolated browser contexts, provides fixtures and web-first assertions, and produces reports and traces.

Create a test file

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

test('user can open settings', async ({ page }) => {
  await page.goto('https://your-app.example/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await page.getByRole('link', { name: 'Settings' }).click();
  await expect(page).toHaveURL(//settings$/);
});

Run the suite with npx playwright test. Use npx playwright test --headed to watch a browser, npx playwright test --debug to open the inspector, and npx playwright show-report to inspect the HTML report after a run. A test file should describe a user outcome, not merely a sequence of DOM operations.

Standalone script versus runner

Approach Best fit Lifecycle and diagnostics
Library script One-off automation, data tasks, or a small smoke check You launch and close browsers; add your own assertions, retries, and reporting.
Playwright Test Repeatable end-to-end regression tests Fixtures provide isolated contexts; the runner supplies retries, reports, traces, and parallel execution controls.

Generate a first draft with Codegen

Run npx playwright codegen playwright.dev to open a browser and the Playwright inspector. Interact with the page; the inspector records actions and proposes locators, prioritizing roles, text, and test ids. If several elements match, it refines the locator.

Generated code is a starting point, not a finished test. Delete accidental clicks, replace selectors that depend on unstable markup, move credentials into environment configuration, and add an assertion for the business outcome. A recording that ends after a click does not prove that the click worked.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Waiting without making tests flaky

Use locator and navigation waits

Playwright automatically waits for locator actions and web-first assertions. Use explicit waits only for a condition that Playwright cannot observe directly, such as a known application event.

await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByRole('status')).toHaveText('Updated');

Avoid arbitrary sleeps such as waitForTimeout(5000). They slow passing runs and still fail when a slower environment needs more time.

Wait for a specific selector or URL

await page.waitForURL('**/checkout');
await expect(page.getByRole('heading', { name: 'Order complete' })).toBeVisible();

Choose the narrowest observable condition that proves the user action completed. Waiting for a generic network-idle state can be misleading on pages with analytics, streaming, or long-polling requests.

Isolation, authentication, and test data

  • Give each test its own browser context so cookies, local storage, and permissions cannot leak between tests.
  • Keep authentication setup explicit. A reusable signed-in state can save time, but refresh it when permissions or account data change.
  • Use dedicated accounts and deterministic records. Tests that depend on another test’s mutations are order-sensitive and difficult to diagnose.
  • Test what a user can see or do. Do not make the assertion depend on private implementation state when a visible outcome is available.

For a standalone script, create a fresh context with browser.newContext() rather than reusing a context across unrelated flows. For the runner, use fixtures and project configuration to define the intended browser, base URL, storage state, and retries.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Debug common failures

Symptom Likely cause Fix
browserType.launch cannot find an executable Browser binaries were not downloaded. Run npx playwright install; in a restricted CI image, install the browsers during image setup.
Locator resolves to several elements The locator is too broad. Add the role’s accessible name, chain from a component, or filter by stable text or test id.
Timeout while clicking The element is hidden, disabled, covered, detached, or never rendered. Check the trace or inspector, use a user-facing locator, and assert the prerequisite state before clicking.
Assertion is intermittently false An immediate boolean check races rendering or an asynchronous update. Use a web-first assertion such as toBeVisible, toHaveText, or toHaveURL.
Works locally but fails in CI Different viewport, browser, timezone, permissions, data, or timing. Set required context options explicitly, use deterministic test data, run headed or with traces, and avoid fixed sleeps.
Test passes alone but fails in a suite Shared cookies, storage, server records, or test order. Use a new context per test and clean up or uniquely namespace created data.
Codegen output breaks after a redesign Generated selectors relied on incidental markup. Replace them with roles, labels, stable text, or an intentional test id.

Headed debugging and evidence from a failed run

Switch to headed mode when you need to see overlays, focus, viewport behavior, or a redirect: launch with headless: false in a library script or run npx playwright test --headed. The inspector can pause at an action and inspect locator suggestions. The HTML report and trace viewer show steps, screenshots, console messages, and network context for runner failures. Use those artifacts to identify the first incorrect assumption rather than increasing every timeout.

Python option

Playwright also provides synchronous and asynchronous Python APIs. Install it with pip install playwright, then download browsers with playwright install. For end-to-end tests, the official pytest plugin is the usual route.

from playwright.sync_api import sync_playwright, expect

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    page.get_by_role("link", name="More information").click()
    expect(page.get_by_role("heading", name="IANA-managed Reserved Domains")).to_be_visible()
    browser.close()

Run pytest-based tests with pytest. The same principles apply: user-facing locators, web-first assertions, explicit data, and isolated contexts.

Or skip the browser setup

If your goal is a rendered screenshot rather than interaction or an assertion, ScreenshotNeo can return the image through one HTTP request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for output formats and options. 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.

Final review checklist

  • The script installs both the package and browser binaries.
  • Every important action uses a role, label, text, or intentional test id.
  • Assertions verify a visible, user-relevant outcome and retry while the UI settles.
  • Contexts, credentials, and test data are isolated.
  • The flow is useful in headless CI and debuggable with headed mode, the inspector, reports, or traces.
  • Cleanup runs even when a step fails.

Frequently Asked Questions

Can Playwright write the entire test for me?

Codegen records interactions and proposes locators, but you must remove accidental actions, stabilize selectors, add assertions, and make authentication and data setup explicit.

Which browser should I launch first?

Start with Chromium for a quick smoke flow, then add Firefox and WebKit projects when your compatibility requirements call for them.

Should I use JavaScript or Python?

Use the language your application and team can maintain. The locator, assertion, isolation, and debugging practices are the same; Python also has synchronous and asynchronous APIs and a pytest plugin.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.