How to Use Playwright for Testing
Install Playwright, write reliable browser tests, run them across browsers, debug failures, and scale execution in CI.
Direct answer: Install Playwright Test, download the browser binaries, write tests with test, expect, and the built-in page fixture, then run them with npx playwright test. Configure projects for Chromium, Firefox, WebKit, branded browsers, or device profiles. Use locators and web-first assertions, isolate test data for parallel workers, and enable traces on the first retry in CI.
1. Install Playwright
Playwright Test is the first-party runner recommended in the Playwright migration guidance. It includes fixtures, parallel execution, reporters, and trace tooling. The examples below use TypeScript, but the same runner supports JavaScript.
Create a new project
mkdir playwright-tests
cd playwright-tests
npm init -y
npm init playwright@latest
The setup wizard creates a configuration file, an example test, and a test directory. Select TypeScript or JavaScript, and choose whether to add a GitHub Actions workflow.
Install browsers
npx playwright install
Install only the browser you need when saving CI download time and disk space:
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
On Linux, install operating-system dependencies with:
npx playwright install --with-deps chromium
Playwright browser binaries are tied to the Playwright package version. After updating Playwright, run the browser installation command again.
See the official browser guide and installation documentation for version-specific details.
2. Write your first Playwright test
Create tests/home.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage has the expected title and heading', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example Domain/);
await expect(page.getByRole('heading', { name: 'Example Domain' })).toBeVisible();
});
The { page } argument is a built-in fixture. The runner creates an isolated page for the test. getByRole returns a locator, and toHaveTitle and toBeVisible are web-first assertions that wait for the expected state instead of checking immediately.
Use locators that describe user intent
test('user can submit a search', async ({ page }) => {
await page.goto('https://example.com/search');
await page.getByRole('textbox', { name: 'Search' }).fill('playwright');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByRole('heading', { name: /results/i })).toBeVisible();
});
Prefer role, label, text, and test-id locators. Avoid long CSS or XPath chains tied to layout details. A locator resolves against the current DOM when the action or assertion runs, which makes it more resilient to re-rendering.
Test navigation, forms, and network state
test('checkout redirects to confirmation', async ({ page }) => {
await page.goto('https://shop.example.test/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await expect(page).toHaveURL(/\/confirmation/);
await expect(page.getByText('Order received')).toBeVisible();
});
test('waits for an API response before checking results', async ({ page }) => {
await page.goto('https://app.example.test');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/results') && response.ok()
);
await page.getByRole('button', { name: 'Load results' }).click();
await responsePromise;
await expect(page.getByRole('list')).toBeVisible();
});
3. Configure projects and browsers
Projects let one suite run with different browser engines, viewports, permissions, locales, and device profiles. A typical configuration is:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [['html', { open: 'never' }]],
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
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 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
Run all projects or select one:
npx playwright test
npx playwright test --project=firefox
npx playwright test tests/home.spec.ts
npx playwright test -g "homepage"
Branded Chrome and Edge can be configured with the channel option when those browsers are installed. Device presets emulate viewport, user agent, touch support, and other settings; they do not replace testing on physical hardware.
4. Run tests during development
Headless and headed runs
# Fast routine run
npx playwright test
# Open a visible browser
npx playwright test --headed
# Run one test repeatedly while developing
npx playwright test tests/home.spec.ts --headed --project=chromium
Headless mode is appropriate for routine and CI runs. Headed mode helps when you need to see navigation, focus, dialogs, or animation behavior.
UI Mode
npx playwright test --ui
UI Mode lets you browse tests, run individual steps, watch changes, inspect locators, and open trace information interactively. It is useful when a failure is easier to understand by stepping through the test than by reading terminal output.
HTML reports
npx playwright show-report
The HTML report groups results by project and test and links to retained screenshots, videos, and traces.
5. Debug failures with traces
Configure trace: 'on-first-retry' for CI. This records diagnostic evidence only after a failure is retried, reducing artifact size and runtime overhead compared with tracing every passing test.
npx playwright test --trace=on
npx playwright show-trace test-results/example-test/trace.zip
Trace Viewer exposes actions, snapshots, network details, console output, and timing around the failing step. The lower-level browserContext.tracing API does not record Playwright Test assertions; the test-runner configuration captures a more complete test trace. See the Trace Viewer guide and tracing API reference.
6. Keep tests independent and parallel-safe
Test files run in parallel by default. Tests in one file run in declaration order unless you opt into parallel execution. Workers are separate processes with separate browser instances, so process globals cannot be used as shared state.
Use isolated data
import { test, expect } from '@playwright/test';
test('worker-specific account can sign in', async ({ page }, testInfo) => {
const email = `e2e-worker-${testInfo.workerIndex}@example.test`;
await page.goto('/signup');
await page.getByLabel('Email').fill(email);
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
});
Prefer creating and cleaning up data inside each test or fixture. If your application cannot support parallel writes safely, lower the worker count:
npx playwright test --workers=1
Use higher worker counts only when the CI machine and test environment can handle the browser processes and concurrent data operations.
7. Fixtures, authentication, and setup
Fixtures keep setup reusable while preserving isolation. For login-heavy suites, save authenticated browser state once and reuse it:
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('button', { name: 'Account' })).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts (relevant parts)
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
dependencies: ['setup'],
use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
},
],
Keep authentication files out of source control because they can contain session cookies and tokens.
8. Mock unstable dependencies when appropriate
End-to-end tests should exercise real integration points where that behavior is the subject of the test. For deterministic UI tests, route a third-party or slow endpoint:
test('shows an empty state', async ({ page }) => {
await page.route('**/api/messages', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ messages: [] }),
});
});
await page.goto('/inbox');
await expect(page.getByText('No messages yet')).toBeVisible();
});
Do not mock the same behavior in every test. Keep a small number of full-stack tests to verify that the UI and backend still agree.
9. Component testing
Playwright component testing runs components in a real browser against a small story gallery served by your development server. This exercises browser layout and interactions more realistically than a DOM-only test. The component-testing API and package support are version-sensitive; check the current component-testing guide and migration notes before adopting it. Experimental React and Vue packages have changed over time.
10. Python and Node.js examples
Use the language that matches your application and team. Playwright Test’s runner examples above are TypeScript/Node.js. Playwright also provides language libraries for Python, Java, and .NET.
Python
pip install pytest-playwright
playwright install
# test_example.py
from playwright.sync_api import Page, expect
def test_homepage(page: Page):
page.goto("https://example.com")
expect(page).to_have_title("Example Domain")
expect(page.get_by_role("heading", name="Example Domain")).to_be_visible()
pytest -q
Node.js without the Playwright Test runner
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();
cURL for a basic endpoint check
cURL does not execute JavaScript or replace a browser test, but it is useful for checking that an application endpoint responds before starting a suite:
curl -I https://example.com/health
11. Continuous integration checklist
- Install the exact package lockfile version.
- Run
npx playwright install --with-deps chromium(or the browsers your projects require). - Set secrets through the CI secret store, never in the repository.
- Use a bounded worker count that fits the runner.
- Enable
trace: 'on-first-retry'and retain the HTML report as an artifact. - Run a focused test or project first when diagnosing failures.
npm ci
npx playwright install --with-deps chromium
npx playwright test
12. Troubleshooting common errors
| Error or symptom | Cause | Fix |
|---|---|---|
Executable doesn't exist |
Browser binaries are missing or belong to an older Playwright version. | Run npx playwright install after installing or upgrading Playwright. |
Tests time out at page.goto |
The server is not running, the URL is wrong, DNS is unavailable, or the page is blocked in CI. | Check webServer, verify the URL from the CI machine, and inspect the trace or network logs. |
| Locator resolves to multiple elements | The locator is too broad. | Use an accessible role and name, a label, a test id, or .filter() to identify the intended element. |
| Element is not visible or actionable | The UI has not reached the expected state, an overlay covers it, or the locator targets the wrong element. | Use a web-first assertion, wait for the relevant response or state, and inspect the trace snapshot. |
| Flaky tests under parallel execution | Tests share accounts, records, ports, files, or mutable global state. | Create worker- or test-specific data, clean it up, or reduce workers while fixing the isolation issue. |
| Works headed, fails headless | Timing, viewport, animation, missing dependencies, or environment differences. | Compare projects and environment variables, disable nonessential animation, and inspect a trace from CI. |
| Authentication disappears between tests | Each test has an isolated context, or storage state was not configured. | Use a setup project and storageState, or perform login in a fixture. |
| Trace is missing assertions | The low-level tracing API was used instead of Playwright Test tracing. | Configure use.trace in playwright.config.ts. |
13. Performance, reliability, and cost decisions
- Browser coverage: Chromium, Firefox, and WebKit give broader engine coverage but require additional browser downloads and runtime. Start with the browsers that represent your users, then expand for release-critical flows.
- Workers: More workers shorten wall-clock time only when the machine, server, database, and test data can handle concurrency. Excess workers can increase contention and flakiness.
- Retries: Retries provide evidence for intermittent failures, but they can hide real defects. Keep retries limited and investigate tests that repeatedly pass only on retry.
- Tracing and video: Capture traces on the first retry and screenshots or videos on failure to balance diagnosis with artifact size.
- Waits: Prefer locator assertions, URL assertions, and response waits over arbitrary sleeps. Fixed delays make suites slower and still fail when environments vary.
- Cost: Playwright itself is installed through project tooling. Your main operating costs are CI minutes, browser storage, and any hosted test infrastructure.
14. Or skip the browser setup
If your goal is to capture pages for visual checks, documentation, previews, or regression artifacts rather than interact with a browser yourself, ScreenshotNeo provides a single screenshot API request. Its clean-capture flow accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, 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. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Read the ScreenshotNeo API documentation for all options.
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)
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}`);
It supports full-page and element captures, device presets and custom viewports, retina scale, dark mode, PDF output, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, geolocation, caching, signed links, async jobs, bulk capture, and a usage API. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.
15. FAQ
Should I use Playwright Test or another test runner?
Use Playwright Test when you want the official fixtures, projects, parallelism, reporters, retries, and trace integration. Use the language library with another runner when your existing test infrastructure requires it.
Do I need to test all three browser engines?
Not for every test. Choose projects based on your supported browsers and risk. Run a focused cross-browser set for critical flows, then expand coverage where engine-specific behavior matters.
Why do tests pass locally but fail in CI?
Common causes are missing browser dependencies, different environment variables, slower services, shared test data, and viewport differences. A CI trace from the first retry usually shows which state diverged.
Can Playwright test mobile browsers?
It can emulate documented device profiles, including viewport, user agent, and touch behavior. Emulation complements testing on real devices when hardware-specific behavior matters.
When should I use screenshots instead of browser tests?
Use Playwright for interactions, assertions, and workflows. Use a screenshot API when you need repeatable page images or PDFs without maintaining browser installation and capture code.


