
Playwright
- 54 installs
- 2 repo stars
- Updated August 3, 2026
- fandhe-ai/agent-reference-skills
Helps with ai & agent building tasks.
About
playwright is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- playwright
- AI & Agent Building
- AI-coding skill
Playwright by the numbers
- 54 all-time installs (skills.sh)
- Ranked #6,946 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fandhe-ai/agent-reference-skills --skill playwrightAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | fandhe-ai/agent-reference-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Playwright E2E テストフレームワーク リファレンス
Playwright の全 API ドキュメントを網羅したスキル。 ユーザーのタスクに応じて適切な README.md を読み、そこから個別ファイルへ辿ること。
ディレクトリ構成
skills/playwright/
SKILL.md
references/
getting-started/
README.md
ci.md
running-and-debugging.md
writing-tests.md
test-runner/
README.md
annotations.md
configuration.md
fixtures.md
global-setup.md
parallelism.md
projects.md
reporters.md
retries.md
timeouts.md
web-server.md
core-concepts/
README.md
actions.md
assertions.md
auto-waiting.md
emulation.md
frames.md
isolation.md
locators.md
pages.md
guides/
README.md
accessibility.md
api-testing.md
aria-snapshots.md
authentication.md
best-practices.md
dialogs-downloads.md
events.md
mock-browser-apis.md
network.md
parameterize.md
pom.md
visual-testing.md
tooling/
README.md
cli.md
codegen.md
test-agents.md
trace-viewer.md
ui-mode.md
advanced/
README.md
browsers.md
chrome-extensions.md
component-testing.md
evaluate.md
service-workers.md
typescript.md
api/
README.md
assertions.md
browser-context.md
locator.md
page.md
request.md
samples/
README.md
accessibility-testing.md
api-testing.md
authentication.md
basic-test.md
codegen.md
configuration.md
custom-fixtures.md
device-emulation.md
dialog-handling.md
locators.md
network-mocking.md
page-object-model.md
parameterized-tests.md
visual-testing.md
scripts/
README.md
install.md
cli.md
generate.md
debug.md
ci.md探索手順
タスクからカテゴリを引き、カテゴリの README.md で目的のページを特定する:
1. 下記マッピング表でタスクに対応するカテゴリを探す 2. そのカテゴリの references/{category}/README.md を参照して目的のページを特定する 3. 該当ページの .md を Read して詳細を確認する
タスク → カテゴリ マッピング
| タスク | カテゴリ | 参照 README |
|---|---|---|
| テスト作成の基本、最初のテスト、CI セットアップ、デバッグ実行 | getting-started | references/getting-started/README.md |
| playwright.config.ts、fixtures、annotations、parallelism、sharding、retries、timeouts | test-runner | references/test-runner/README.md |
| locators、assertions、actions、auto-waiting、emulation、frames、isolation | core-concepts | references/core-concepts/README.md |
| 認証、ネットワークモック、API テスト、ビジュアルテスト、POM、アクセシビリティ、ベストプラクティス | guides | references/guides/README.md |
| CLI コマンド、codegen、trace viewer、UI mode、test agents | tooling | references/tooling/README.md |
| TypeScript、ブラウザ管理、Chrome 拡張、コンポーネントテスト、evaluate、service workers | advanced | references/advanced/README.md |
| Page、Locator、BrowserContext、APIRequestContext、Assertions クラス API | api | references/api/README.md |
| 典型的な使い方を知りたい、実装サンプルを参照したい | samples | samples/README.md |
| インストール・CLI コマンド・CI 実行・デバッグコマンドを知りたい | scripts | scripts/README.md |
Browsers
Playwright downloads and manages browser binaries for Chromium, Firefox, and WebKit. Each Playwright release is pinned to specific browser versions to guarantee consistent behavior.
Installation
# Install all default browsers
npx playwright install
# Install a specific browser
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
# Install system dependencies (Linux)
npx playwright install-deps
# Install browser with system dependencies
npx playwright install --with-deps chromiumSupported Browsers
| Browser | Engine | Matches |
|---|---|---|
| Chromium | Blink | Google Chrome, Microsoft Edge |
| Firefox | Gecko | Recent Firefox Stable |
| WebKit | WebKit | Safari (latest main branch) |
Browser Channels
Channels allow testing against branded (stable/beta/dev/canary) browser builds instead of the bundled Chromium/Firefox/WebKit:
| Channel | Browser |
|---|---|
chrome | Google Chrome Stable |
msedge | Microsoft Edge Stable |
chrome-beta | Google Chrome Beta |
msedge-beta | Microsoft Edge Beta |
chrome-dev | Google Chrome Dev |
msedge-dev | Microsoft Edge Dev |
chrome-canary | Google Chrome Canary |
msedge-canary | Microsoft Edge Canary |
Install branded browsers:
npx playwright install msedge
npx playwright install chromeHeadless Shell
For CI environments that only need headless Chromium, install the lightweight headless shell:
# Install headless shell only (smaller download)
npx playwright install --with-deps --only-shell
# Install full Chromium without headless shell
npx playwright install --with-deps --no-shellConfiguring Browser Projects
Define multiple browser targets in playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
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'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
},
{
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
],
});Run a single project:
npx playwright test --project=firefoxBrowser Cache
Default cache locations by platform:
| Platform | Path |
|---|---|
| macOS | ~/Library/Caches/ms-playwright |
| Linux | ~/.cache/ms-playwright |
| Windows | %USERPROFILE%\AppData\Local\ms-playwright |
PLAYWRIGHT_BROWSERS_PATH
Override the cache directory:
# Custom location
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers npx playwright install
PLAYWRIGHT_BROWSERS_PATH=$HOME/pw-browsers npx playwright test
# Hermetic install (inside node_modules)
PLAYWRIGHT_BROWSERS_PATH=0 npx playwright installNetwork Configuration
# Proxy for downloads
HTTPS_PROXY=https://192.0.2.1 npx playwright install
# Custom certificate authority
export NODE_EXTRA_CA_CERTS="/path/to/cert.pem"
# Increase download timeout (milliseconds)
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT=120000 npx playwright install
# Custom download host
PLAYWRIGHT_DOWNLOAD_HOST=http://192.0.2.1 npx playwright install
# Per-browser custom host
PLAYWRIGHT_FIREFOX_DOWNLOAD_HOST=http://203.0.113.3 npx playwright installListing and Uninstalling
# List installed browsers
npx playwright install --list
# Uninstall browsers for current Playwright version
npx playwright uninstall
# Uninstall all Playwright browser versions
npx playwright uninstall --allVersion Management
# Check current version
npx playwright --version
# Update Playwright and re-install browsers
npm install -D @playwright/test@latest
npx playwright installChrome Extensions
Playwright supports testing Chrome extensions in Chromium only. Extensions require a persistent browser context and specific launch arguments.
Requirements
- Chromium only -- Firefox and WebKit do not support extensions.
- Persistent context -- Use
chromium.launchPersistentContext()instead of the standardbrowser.newContext(). - Channel `chromium` -- Set
channel: 'chromium'to enable headless extension support. Google Chrome and Microsoft Edge have removed command-line flags for side-loading extensions, so the Playwright-bundled Chromium must be used.
Launch Arguments
Two flags are required to load an unpacked extension:
| Flag | Purpose |
|---|---|
--disable-extensions-except=PATH | Disable all extensions except the target |
--load-extension=PATH | Load the unpacked extension from PATH |
Accessing the Service Worker
Extensions expose their background service worker through the browser context:
let [serviceWorker] = browserContext.serviceWorkers();
if (!serviceWorker) {
serviceWorker = await browserContext.waitForEvent('serviceworker');
}The extension ID is extracted from the service worker URL:
const extensionId = serviceWorker.url().split('/')[2];Test Fixture
Create a reusable fixture that launches Chromium with the extension loaded:
import { test as base, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';
export const test = base.extend<{
context: BrowserContext;
extensionId: string;
}>({
context: async ({}, use) => {
const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', {
channel: 'chromium',
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
let [serviceWorker] = context.serviceWorkers();
if (!serviceWorker) {
serviceWorker = await context.waitForEvent('serviceworker');
}
const extensionId = serviceWorker.url().split('/')[2];
await use(extensionId);
},
});
export const expect = test.expect;Testing Extension Pages
Page Content Modified by the Extension
Verify that the extension modifies page content:
test('extension modifies page', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.locator('body')).toHaveText('Changed by my-extension');
});Extension Popup
Navigate directly to the extension popup using chrome-extension:// protocol:
test('extension popup', async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await expect(page.locator('body')).toHaveText('my-extension popup');
});Headless and Headed Modes
With channel: 'chromium', extensions work in both headless and headed modes. To run headed (useful for debugging):
npx playwright test --headedThe default headless mode runs without a visible browser window, which is suitable for CI environments.
Component Testing
Playwright Test can mount and test individual UI components in a real browser. Components execute with real clicks, real layout, and visual regression support. This feature is experimental.
Supported Frameworks
| Framework | Package |
|---|---|
| React | @playwright/experimental-ct-react |
| Vue | @playwright/experimental-ct-vue |
| Svelte | @playwright/experimental-ct-svelte |
Setup
Installation
npm init playwright@latest -- --ctRequired Files
`playwright/index.html` -- the host page for mounted components:
<html lang="en">
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>`playwright/index.ts` -- entry point for applying styles, themes, and runtime dependencies. Import global CSS or register framework plugins here.
mount
The mount fixture renders a component into the browser and returns a Locator pointing to the mounted component.
React
import { test, expect } from '@playwright/experimental-ct-react';
import App from './App';
test('renders app', async ({ mount }) => {
const component = await mount(<App />);
await expect(component).toContainText('Learn React');
});Vue
import { test, expect } from '@playwright/experimental-ct-vue';
import App from './App.vue';
test('renders app', async ({ mount }) => {
const component = await mount(App, {
props: { msg: 'greetings' },
});
await expect(component).toContainText('greetings');
});Svelte
import { test, expect } from '@playwright/experimental-ct-svelte';
import App from './App.svelte';
test('renders app', async ({ mount }) => {
const component = await mount(App);
await expect(component).toBeVisible();
});Props
Pass props during mount:
// React
const component = await mount(<Component msg="greetings" />);
// Vue
const component = await mount(Component, { props: { msg: 'greetings' } });
// Svelte
const component = await mount(Component, { props: { msg: 'greetings' } });Callbacks and Events
// React
const component = await mount(<Component onClick={() => { /* ... */ }} />);
// Vue
const component = await mount(Component, {
on: { click() { /* ... */ } },
});
// Svelte
const component = await mount(Component, {
on: { click() { /* ... */ } },
});Children and Slots
// React -- pass JSX children
const component = await mount(<Component>Child content</Component>);
// Vue -- use slots
const component = await mount(Component, {
slots: { default: 'Slot content' },
});
// Svelte -- use slots
const component = await mount(Component, {
slots: { default: 'Slot content' },
});Hooks
Hooks run in the browser and configure the environment before or after mounting. Define them in playwright/index.ts.
beforeMount
Runs before the component mounts. Use it to wrap the component with providers (router, store, theme):
// playwright/index.tsx
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import { BrowserRouter } from 'react-router-dom';
export type HooksConfig = {
enableRouting?: boolean;
};
beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
if (hooksConfig?.enableRouting) {
return <BrowserRouter><App /></BrowserRouter>;
}
});Pass hooksConfig from the test:
test('products page with routing', async ({ mount }) => {
const component = await mount<HooksConfig>(<ProductsPage />, {
hooksConfig: { enableRouting: true },
});
await expect(component).toContainText('Products');
});afterMount
Runs after the component mounts. Use it for post-mount setup such as waiting for async initialization.
Lifecycle
unmount
Remove the component from the DOM:
const component = await mount(<Component />);
await component.unmount();update
Update props, callbacks, or slots on the already-mounted component:
const component = await mount(<Component msg="hello" />);
await component.update({
props: { msg: 'updated' },
on: { click() { /* ... */ } },
slots: { default: 'New Child' },
});Router Fixture
The experimental router fixture intercepts network requests at the component level. It supports raw route handlers and MSW (Mock Service Worker) handlers:
import { http, HttpResponse } from 'msw';
test('mocked API', async ({ mount, router }) => {
await router.use(
http.get('/data', async () => {
return HttpResponse.json({ value: 'mocked' });
})
);
const component = await mount(<DataDisplay />);
await expect(component).toContainText('mocked');
});Limitations
Complex objects cannot be passed as props
Only plain serializable values (strings, numbers, dates, plain objects) work. Class instances and complex objects fail:
// This will NOT work
const component = await mount(<ProcessViewer process={process} />);Synchronous callbacks are not supported
Callbacks live in Node.js and cannot return values synchronously to the browser component:
// This will NOT work
const component = await mount(<ColorPicker colorGetter={() => 'red'} />);Workaround: Test Stories
Create thin wrapper components ("stories") that convert complex objects to serializable data:
// input-media.story.tsx
export function InputMediaForTest(props: {
onMediaChange: (name: string) => void;
}) {
return <InputMedia onChange={(media) => props.onMediaChange(media.name)} />;
}
// input-media.spec.tsx
test('changes the image', async ({ mount }) => {
let mediaSelected: string | null = null;
const component = await mount(
<InputMediaForTest
onMediaChange={(mediaName) => {
mediaSelected = mediaName;
}}
/>
);
await component
.getByTestId('imageInput')
.setInputFiles('src/assets/logo.png');
await expect.poll(() => mediaSelected).toBe('logo.png');
});Under the Hood
Playwright uses Vite to bundle components and serves them from a local static web server. When mount() is called, the browser navigates to /playwright/index.html and renders the specified component. Vite configuration can be customized via the ctViteConfig property in playwright-ct.config.ts.
Evaluate and Handles
Playwright scripts run in a Node.js environment, separate from the browser page. The page.evaluate() API bridges these two worlds by executing JavaScript functions inside the browser page context.
page.evaluate
Execute a function in the browser and return the result to Node.js:
const href = await page.evaluate(() => document.location.href);Promises are automatically awaited:
const status = await page.evaluate(async () => {
const response = await fetch(location.href);
return response.status;
});Argument Passing
Variables from the Node.js scope are not accessible inside evaluate. Pass them explicitly as the second argument:
// WRONG -- `data` is not accessible inside evaluate
const data = 'some data';
const result = await page.evaluate(() => {
window.myApp.use(data); // ReferenceError
});
// CORRECT -- pass `data` explicitly
const data = 'some data';
const result = await page.evaluate((data) => {
window.myApp.use(data);
}, data);Supported Argument Types
Primitives, arrays, plain objects, and JSHandle instances can be passed:
// Primitive
await page.evaluate((num) => num * 2, 42);
// Array
await page.evaluate((array) => array.length, [1, 2, 3]);
// Object
await page.evaluate((obj) => obj.foo, { foo: 'bar' });Multiple arguments are passed as an object:
const x = 10;
const y = 20;
await page.evaluate(({ x, y }) => x + y, { x, y });evaluateHandle
Returns a JSHandle instead of a serialized value. Use this when you need a reference to an in-browser object rather than its serialized form:
const windowHandle = await page.evaluateHandle(() => window);Combine with evaluate to work with the referenced object:
const button = await page.evaluateHandle('window.button');
const text = await page.evaluate((btn) => btn.textContent, button);Pass multiple handles as an object:
const button1 = await page.evaluateHandle('window.button1');
const button2 = await page.evaluateHandle('window.button2');
const combined = await page.evaluate(
({ button1, button2 }) => button1.textContent + button2.textContent,
{ button1, button2 }
);addInitScript
Inject JavaScript that runs before any page script, on every page load or navigation. Useful for mocking browser APIs.
Inline Function
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
Math.random = () => 0.42;
});
});Inline Function with Arguments
test.beforeEach(async ({ page }) => {
const value = 42;
await page.addInitScript((value) => {
Math.random = () => value;
}, value);
});From a File
import { test, expect } from '@playwright/test';
import path from 'path';
test.beforeEach(async ({ page }) => {
await page.addInitScript({
path: path.resolve(__dirname, '../mocks/preload.js'),
});
});Context-Level Init Script
Apply to all pages in a browser context:
await browserContext.addInitScript(() => {
window.__testMode = true;
});JSHandle
A JSHandle represents a reference to a JavaScript object living in the browser. Handles prevent the referenced object from being garbage collected.
Creating Handles
const jsHandle = await page.evaluateHandle('window');
const arrayHandle = await page.evaluateHandle(() => {
window.myArray = [1];
return window.myArray;
});Using Handles as Arguments
const myArrayHandle = await page.evaluateHandle(() => {
window.myArray = [1];
return window.myArray;
});
// Read from the handle
const length = await page.evaluate((a) => a.length, myArrayHandle);
// Modify via the handle
await page.evaluate(
(arg) => arg.myArray.push(arg.newElement),
{ myArray: myArrayHandle, newElement: 2 }
);Disposing Handles
Handles are released automatically on page navigation. For long-lived pages, manually release handles to avoid memory leaks:
const handle = await page.evaluateHandle(() => ({ data: 'large' }));
// ... use the handle ...
await handle.dispose();ElementHandle (Deprecated)
ElementHandle is a JSHandle that references a DOM element. Its use is discouraged -- prefer Locator objects and web-first assertions instead.
// Discouraged -- ElementHandle
const elementHandle = await page.waitForSelector('#box');
const boundingBox = await elementHandle.boundingBox();
const classNames = await elementHandle.getAttribute('class');
// Preferred -- Locator
const box = page.locator('#box');
await expect(box).toBeVisible();
await expect(box).toHaveClass(/highlighted/);Why Locators Are Preferred
| Aspect | ElementHandle | Locator |
|---|---|---|
| Binding | Points to a specific DOM element | Captures the logic to find an element |
| Staleness | Goes stale if DOM changes | Re-queries the DOM on every action |
| Auto-waiting | No | Yes |
| Retry | No | Yes (with web-first assertions) |
| Recommendation | Avoid | Use for all new code |
ElementHandle fetches via page.waitForSelector() or frame.waitForSelector() wait for the element to be attached and visible, but the handle becomes stale if the element is later removed and re-added. Locators re-evaluate the selector on each interaction, making tests resilient to DOM changes.
Advanced
| Name | Description | Path |
|---|---|---|
| Browsers | Playwright downloads and manages browser binaries for Chromium, Firefox, and WebKit. | browsers.md |
| Chrome Extensions | Playwright supports testing Chrome extensions in Chromium only. | chrome-extensions.md |
| Component Testing | Playwright Test can mount and test individual UI components in a real browser. | component-testing.md |
| Evaluate and Handles | Playwright scripts run in a Node.js environment, separate from the browser page. | evaluate.md |
| Service Workers | Playwright provides APIs for interacting with service workers. | service-workers.md |
| TypeScript | Playwright Test has built-in TypeScript support. | typescript.md |
Service Workers
Playwright provides APIs for interacting with service workers. Service worker support is Chromium-only -- Firefox and WebKit do not expose service worker APIs.
Blocking Service Workers
To prevent service workers from interfering with tests, set serviceWorkers to 'block' in the configuration. This produces more predictable and performant tests:
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
serviceWorkers: 'block',
},
});The default value is 'allow', which lets service workers register and intercept requests normally.
Waiting for Service Worker Registration
Detect when a service worker activates using the browser context event:
const serviceWorkerPromise = context.waitForEvent('serviceworker');
await page.goto('/example-with-a-service-worker.html');
const serviceWorker = await serviceWorkerPromise;Before evaluating code in the worker, ensure it has fully activated:
await page.evaluate(async () => {
const registration = await window.navigator.serviceWorker.getRegistration();
if (registration.active?.state === 'activated') return;
await new Promise<void>((resolve) => {
window.navigator.serviceWorker.addEventListener(
'controllerchange',
() => resolve()
);
});
});Network Events
When a service worker makes network requests, those requests fire events on the browser context (not the page):
browserContext.on('request')browserContext.on('response')browserContext.on('requestfinished')browserContext.on('requestfailed')
Use request.serviceWorker() to identify requests originating from a service worker.
Conditional Routing
Route requests differently depending on whether they come from a service worker or a page:
await context.route('**', async (route) => {
if (route.request().serviceWorker()) {
// Mock responses for service worker requests
await route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'from sw',
});
} else {
// Let page requests pass through
await route.continue();
}
});Limitations
- Service workers are supported on Chromium-based browsers only.
- Requests for updated service worker main script code (the script URL itself) cannot be routed via Playwright's routing APIs.
TypeScript
Playwright Test has built-in TypeScript support. Tests written in .ts files are automatically transformed to JavaScript and executed without any additional configuration.
Type Checking Caveat
Playwright does not perform type checking. Tests run even when non-critical TypeScript compilation errors exist. To enforce type safety, run the TypeScript compiler separately:
// package.json scripts
{
"scripts": {
"test": "npx tsc -p tsconfig.json --noEmit && npx playwright test"
}
}For development, use watch mode alongside Playwright:
npx tsc -p tsconfig.json --noEmit -wSupported tsconfig Options
Playwright reads tsconfig.json by traversing the directory structure upward from the test file. Only these compilerOptions are recognized:
| Option | Purpose |
|---|---|
allowJs | Allow JavaScript files in the compilation |
baseUrl | Base directory for resolving module names |
paths | Map import paths to physical locations |
references | Project references for composite projects |
Other compiler options are ignored by Playwright's internal transformer.
Specifying tsconfig with --tsconfig
Point Playwright to a specific tsconfig.json via the CLI flag:
npx playwright test --tsconfig=tsconfig.test.jsonOr set it in playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
tsconfig: './tsconfig.test.json',
});Path Mapping
Configure path aliases in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@myhelper/*": ["packages/myhelper/*"]
}
}
}Use the alias in test files:
import { test, expect } from '@playwright/test';
import { username, password } from '@myhelper/credentials';
test('login', async ({ page }) => {
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
});Playwright resolves these paths at transform time, so no additional bundler configuration is needed.
Manual Compilation
When tests use advanced TypeScript features that Playwright's built-in transformer does not support, pre-compile before running tests.
Create a dedicated tests/tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "Node",
"sourceMap": true,
"outDir": "../tests-out"
}
}Configure package.json to compile then run:
{
"scripts": {
"pretest": "tsc --incremental -p tests/tsconfig.json",
"test": "playwright test -c tests-out"
}
}The --incremental flag speeds up subsequent compilations. The -c tests-out flag tells Playwright to look for tests in the compiled output directory.
Assertions
Playwright uses expect from @playwright/test which extends Jest-like assertions with auto-retrying, web-first matchers. Assertions that target Locator, Page, or APIResponse automatically retry until the condition is met or the timeout expires.
import { test, expect } from "@playwright/test";
test("example", async ({ page }) => {
await page.goto("https://example.com");
await expect(page).toHaveTitle(/Example/);
await expect(page.getByRole("heading")).toHaveText("Example Domain");
});LocatorAssertions
Auto-retrying assertions for Locator objects. Created via expect(locator).
State Assertions
| Method | Signature |
|---|---|
toBeAttached | (options?: { attached?: boolean; timeout?: number }) => Promise<void> |
toBeChecked | (options?: { checked?: boolean; indeterminate?: boolean; timeout?: number }) => Promise<void> |
toBeDisabled | (options?: { timeout?: number }) => Promise<void> |
toBeEditable | (options?: { editable?: boolean; timeout?: number }) => Promise<void> |
toBeEmpty | (options?: { timeout?: number }) => Promise<void> |
toBeEnabled | (options?: { enabled?: boolean; timeout?: number }) => Promise<void> |
toBeFocused | (options?: { timeout?: number }) => Promise<void> |
toBeHidden | (options?: { timeout?: number }) => Promise<void> |
toBeInViewport | (options?: { ratio?: number; timeout?: number }) => Promise<void> |
toBeVisible | (options?: { timeout?: number; visible?: boolean }) => Promise<void> |
const button = page.getByRole("button", { name: "Submit" });
await expect(button).toBeVisible();
await expect(button).toBeEnabled();
await expect(button).not.toBeDisabled();
await expect(page.getByRole("checkbox")).toBeChecked();
await expect(page.getByRole("checkbox")).toBeChecked({ checked: false }); // unchecked
await expect(page.getByRole("checkbox")).toBeChecked({ indeterminate: true });
await expect(page.getByRole("textbox")).toBeEditable();
await expect(page.getByRole("textbox")).toBeEmpty();
await expect(page.getByRole("textbox")).toBeFocused();
await expect(page.getByTestId("loading")).toBeHidden();
await expect(page.getByTestId("element")).toBeAttached();
await expect(page.getByTestId("element")).not.toBeAttached(); // detached from DOM
// At least 50% of element is in viewport
await expect(page.getByRole("banner")).toBeInViewport({ ratio: 0.5 });Content Assertions
| Method | Signature |
|---|---|
toContainText | `(expected: string \ |
toHaveText | `(expected: string \ |
toHaveValue | `(value: string \ |
toHaveValues | `(values: (string \ |
toHaveCount | (count: number, options?: { timeout?: number }) => Promise<void> |
// Exact text match (whitespace normalized)
await expect(page.getByRole("heading")).toHaveText("Welcome");
// Partial text match
await expect(page.getByRole("alert")).toContainText("success");
// Regex match
await expect(page.getByRole("heading")).toHaveText(/welcome/i);
// Case-insensitive match
await expect(page.getByRole("heading")).toHaveText("welcome", {
ignoreCase: true,
});
// Multiple elements (array of expected values)
await expect(page.getByRole("listitem")).toHaveText([
"First item",
"Second item",
"Third item",
]);
// Input value
await expect(page.getByLabel("Email")).toHaveValue("user@example.com");
await expect(page.getByLabel("Email")).toHaveValue(/.*@example\.com/);
// Multi-select values
await expect(page.getByLabel("Colors")).toHaveValues([/red/, /green/]);
// Element count
await expect(page.getByRole("listitem")).toHaveCount(3);Attribute and Property Assertions
| Method | Signature |
|---|---|
toHaveAttribute | `(name: string, value?: string \ |
toHaveClass | `(expected: string \ |
toContainClass | `(expected: string \ |
toHaveCSS | `(name: string, value: string \ |
toHaveId | `(id: string \ |
toHaveJSProperty | (name: string, value: Object, options?: { timeout?: number }) => Promise<void> |
// Attribute exists
await expect(page.getByRole("link")).toHaveAttribute("href");
// Attribute has specific value
await expect(page.getByRole("link")).toHaveAttribute("href", "/home");
await expect(page.getByRole("link")).toHaveAttribute("href", /\/home/);
// CSS class (exact full class string)
await expect(page.getByTestId("box")).toHaveClass("btn btn-primary");
// CSS class (contains specific class)
await expect(page.getByTestId("box")).toContainClass("btn-primary");
await expect(page.getByTestId("box")).toContainClass(["btn", "btn-primary"]);
// Multiple elements' classes
await expect(page.getByRole("listitem")).toHaveClass([
"item active",
"item",
"item",
]);
// CSS property
await expect(page.getByTestId("box")).toHaveCSS("color", "rgb(255, 0, 0)");
await expect(page.getByTestId("box")).toHaveCSS("display", /flex/);
// Element ID
await expect(page.getByRole("main")).toHaveId("content");
// JavaScript property on DOM element
await expect(page.getByRole("checkbox")).toHaveJSProperty("checked", true);Accessibility Assertions
| Method | Signature |
|---|---|
toHaveAccessibleDescription | `(description: string \ |
toHaveAccessibleErrorMessage | `(errorMessage: string \ |
toHaveAccessibleName | `(name: string \ |
toHaveRole | (role: AriaRole, options?: { timeout?: number }) => Promise<void> |
await expect(page.getByTestId("submit")).toHaveAccessibleName("Submit form");
await expect(page.getByTestId("submit")).toHaveAccessibleDescription(
"Submits the registration form",
);
await expect(page.getByLabel("Email")).toHaveAccessibleErrorMessage(
"Email is required",
);
await expect(page.getByTestId("nav")).toHaveRole("navigation");Visual Assertions
| Method | Signature |
|---|---|
toHaveScreenshot | `(name?: string \ |
toMatchAriaSnapshot | (expected?: string, options?: { name?: string; timeout?: number }) => Promise<void> |
// Screenshot comparison (auto-generates name from test title)
await expect(page.getByTestId("chart")).toHaveScreenshot();
// Named screenshot
await expect(page.getByTestId("chart")).toHaveScreenshot("chart-view.png");
// With tolerance
await expect(page.getByTestId("chart")).toHaveScreenshot({
maxDiffPixelRatio: 0.05,
mask: [page.getByTestId("timestamp")],
});
// ARIA snapshot
await expect(page.getByRole("list")).toMatchAriaSnapshot(`
- list:
- listitem: First
- listitem: Second
`);---
PageAssertions
Auto-retrying assertions for Page objects. Created via expect(page).
| Method | Signature |
|---|---|
toHaveTitle | `(titleOrRegExp: string \ |
toHaveURL | `(url: string \ |
toHaveScreenshot | `(name?: string \ |
not | PageAssertions |
// Title
await expect(page).toHaveTitle("Dashboard");
await expect(page).toHaveTitle(/Dashboard/);
// URL
await expect(page).toHaveURL("https://example.com/dashboard");
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page).toHaveURL((url) => url.pathname === "/dashboard");
// URL with ignoreCase
await expect(page).toHaveURL("/Dashboard", { ignoreCase: true });
// Negation
await expect(page).not.toHaveURL(/\/login/);
// Full page screenshot comparison
await expect(page).toHaveScreenshot("full-page.png", { fullPage: true });---
APIResponseAssertions
Auto-retrying assertions for APIResponse objects. Created via expect(apiResponse).
| Method | Signature |
|---|---|
toBeOK | () => Promise<void> |
not | APIResponseAssertions |
const response = await request.get("/api/health");
await expect(response).toBeOK(); // status 200-299
const badResponse = await request.get("/api/missing");
await expect(badResponse).not.toBeOK();---
GenericAssertions
Non-retrying assertions for any value. These work like standard Jest matchers. Created via expect(value) with a non-Playwright value.
Equality
| Method | Signature |
|---|---|
toBe | (expected: Object) => void |
toEqual | (expected: Object) => void |
toStrictEqual | (expected: Object) => void |
toMatchObject | `(expected: Object \ |
toBeCloseTo | (expected: number, numDigits?: number) => void |
Truthiness
| Method | Signature |
|---|---|
toBeTruthy | () => void |
toBeFalsy | () => void |
toBeDefined | () => void |
toBeUndefined | () => void |
toBeNull | () => void |
toBeNaN | () => void |
Comparison
| Method | Signature |
|---|---|
toBeGreaterThan | `(expected: number \ |
toBeGreaterThanOrEqual | `(expected: number \ |
toBeLessThan | `(expected: number \ |
toBeLessThanOrEqual | `(expected: number \ |
Collections and Strings
| Method | Signature |
|---|---|
toContain | `(expected: string \ |
toContainEqual | (expected: Object) => void |
toHaveLength | (expected: number) => void |
toHaveProperty | (keyPath: string, expected?: Object) => void |
toMatch | `(expected: RegExp \ |
Type and Instance
| Method | Signature |
|---|---|
toBeInstanceOf | (expected: Function) => void |
Errors
| Method | Signature |
|---|---|
toThrow | `(expected?: string \ |
toThrowError | `(expected?: string \ |
Asymmetric Matchers
| Method | Signature |
|---|---|
expect.any | (constructor: Function) => any |
expect.anything | () => any |
expect.arrayContaining | (expected: Array) => Array |
expect.arrayOf | (constructor: Function) => Array |
expect.closeTo | (expected: number, numDigits?: number) => number |
expect.objectContaining | (expected: Object) => Object |
expect.stringContaining | (expected: string) => string |
expect.stringMatching | `(expected: string \ |
// Equality
expect(1 + 1).toBe(2);
expect({ a: 1 }).toEqual({ a: 1 }); // deep equality
expect({ a: 1, b: undefined }).not.toStrictEqual({ a: 1 }); // strict: undefined !== missing
// Matching objects
expect({ a: 1, b: 2, c: 3 }).toMatchObject({ a: 1, b: 2 });
// Truthiness
expect("hello").toBeTruthy();
expect(null).toBeFalsy();
expect(undefined).toBeUndefined();
expect("value").toBeDefined();
expect(null).toBeNull();
expect(0 / 0).toBeNaN();
// Numbers
expect(10).toBeGreaterThan(5);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
// Collections
expect([1, 2, 3]).toContain(2);
expect([{ a: 1 }, { a: 2 }]).toContainEqual({ a: 1 });
expect([1, 2, 3]).toHaveLength(3);
expect("hello world").toContain("world");
expect("hello world").toMatch(/world$/);
// Properties
expect({ a: { b: 1 } }).toHaveProperty("a.b", 1);
expect({ a: 1 }).toHaveProperty("a");
// Errors
expect(() => { throw new Error("fail"); }).toThrow("fail");
expect(() => { throw new Error("fail"); }).toThrow(/fail/);
// Asymmetric matchers (useful inside toEqual/toMatchObject)
expect({ name: "Alice", age: 30 }).toEqual({
name: expect.any(String),
age: expect.any(Number),
});
expect({ items: [1, 2, 3] }).toEqual({
items: expect.arrayContaining([1, 3]),
});
expect({ message: "hello world" }).toEqual({
message: expect.stringContaining("world"),
});
expect({ data: { x: 1, y: 2, z: 3 } }).toEqual({
data: expect.objectContaining({ x: 1 }),
});---
SnapshotAssertions
Non-retrying assertions for comparing values against stored snapshots. Created via expect(value).
| Method | Signature |
|---|---|
toMatchSnapshot | `(name?: string \ |
toMatchSnapshot | `(options?: { maxDiffPixelRatio?: number; maxDiffPixels?: number; name?: string \ |
Note: For screenshot comparisons, preferexpect(page).toHaveScreenshot()orexpect(locator).toHaveScreenshot()which auto-retry.toMatchSnapshotis mainly for non-visual data.
// Snapshot of arbitrary data
expect(await page.title()).toMatchSnapshot("page-title.txt");
// Screenshot snapshot (prefer toHaveScreenshot instead)
expect(await page.screenshot()).toMatchSnapshot("screenshot.png");
// With options
expect(await page.screenshot()).toMatchSnapshot("screenshot.png", {
maxDiffPixelRatio: 0.05,
threshold: 0.2,
});---
Assertion Modifiers and Utilities
.not
Negates any assertion. Available on all assertion types.
await expect(page.getByRole("button")).not.toBeDisabled();
await expect(page).not.toHaveURL(/\/login/);
expect(value).not.toBe(0);expect.soft (Soft Assertions)
Soft assertions do not terminate the test on failure. The test continues running but is marked as failed. Useful for checking multiple conditions.
await expect.soft(page.getByTestId("status")).toHaveText("Success");
await expect.soft(page.getByTestId("name")).toHaveText("Alice");
await expect.soft(page.getByTestId("email")).toHaveText("alice@example.com");
// Optionally verify no soft assertion failures occurred
expect(test.info().errors).toHaveLength(0);expect.configure
Creates a pre-configured expect instance with custom defaults for timeout and soft.
// Custom timeout for slow operations
const slowExpect = expect.configure({ timeout: 10_000 });
await slowExpect(page.getByRole("status")).toHaveText("Complete");
// Pre-configured soft expect
const softExpect = expect.configure({ soft: true });
await softExpect(page.getByTestId("a")).toHaveText("A");
await softExpect(page.getByTestId("b")).toHaveText("B");
// Combine both
const slowSoftExpect = expect.configure({ timeout: 10_000, soft: true });expect.poll
Retries a function until the assertion passes. Useful for polling non-Playwright values (e.g., API status).
// Poll until API returns 200
await expect
.poll(
async () => {
const response = await page.request.get("/api/status");
return response.status();
},
{
timeout: 10_000,
intervals: [1_000, 2_000, 5_000],
message: "API did not return 200 in time",
},
)
.toBe(200);
// Poll a computed value
await expect
.poll(async () => {
const items = await page.getByRole("listitem").count();
return items;
})
.toBeGreaterThan(0);toPass
Retries an entire block of code until all assertions inside pass. Useful for retrying a sequence of operations.
await expect(async () => {
const response = await page.request.get("/api/data");
expect(response.status()).toBe(200);
const json = await response.json();
expect(json.items).toHaveLength(10);
}).toPass({
timeout: 60_000,
intervals: [1_000, 2_000, 5_000],
});Note: The default timeout fortoPassis0(no timeout). It does not inherit fromexpect.configure(). Always set an explicit timeout.
BrowserContext, Browser, and BrowserType
BrowserContext
A BrowserContext is an isolated browser session. Each context has its own cookies, storage, and permissions. Pages within a context share these, but contexts are isolated from each other. In @playwright/test, a fresh context is created for each test by default via the context fixture.
import { test, expect } from "@playwright/test";
test("example", async ({ context, page }) => {
// page already belongs to a fresh context
await page.goto("https://example.com");
// Open a second page in the same context (shares cookies/storage)
const page2 = await context.newPage();
await page2.goto("https://example.com/other");
});Methods
| Method | Signature |
|---|---|
addCookies | `(cookies: Array<{ name: string; value: string; url?: string; domain?: string; path?: string; expires?: number; httpOnly?: boolean; secure?: boolean; sameSite?: "Strict" \ |
addInitScript | `(script: Function \ |
browser | `() => Browser \ |
clearCookies | `(options?: { domain?: string \ |
clearPermissions | () => Promise<void> |
close | (options?: { reason?: string }) => Promise<void> |
cookies | `(urls?: string \ |
exposeBinding | (name: string, callback: Function, options?: { handle?: boolean }) => Promise<void> |
exposeFunction | (name: string, callback: Function) => Promise<void> |
grantPermissions | (permissions: string[], options?: { origin?: string }) => Promise<void> |
newCDPSession | `(page: Page \ |
newPage | () => Promise<Page> |
pages | () => Array<Page> |
removeAllListeners | `(type?: string, options?: { behavior?: "wait" \ |
route | `(url: string \ |
routeFromHAR | `(har: string, options?: { notFound?: "abort" \ |
routeWebSocket | `(url: string \ |
serviceWorkers | () => Array<Worker> |
setDefaultNavigationTimeout | (timeout: number) => void |
setDefaultTimeout | (timeout: number) => void |
setExtraHTTPHeaders | (headers: Record<string, string>) => Promise<void> |
setGeolocation | `(geolocation: { latitude: number; longitude: number; accuracy?: number } \ |
setOffline | (offline: boolean) => Promise<void> |
storageState | (options?: { indexedDB?: boolean; path?: string }) => Promise<{ cookies: Array<Cookie>; origins: Array<{ origin: string; localStorage: Array<{ name: string; value: string }> }> }> |
unroute | `(url: string \ |
unrouteAll | `(options?: { behavior?: "wait" \ |
waitForEvent | `(event: string, optionsOrPredicate?: Function \ |
// Cookies
await context.addCookies([
{ name: "session", value: "abc123", domain: ".example.com", path: "/" },
]);
const cookies = await context.cookies("https://example.com");
await context.clearCookies();
await context.clearCookies({ name: /^session/ });
// Permissions
await context.grantPermissions(["geolocation"], {
origin: "https://example.com",
});
await context.clearPermissions();
// Geolocation
await context.setGeolocation({ latitude: 48.8566, longitude: 2.3522 });
// Offline mode
await context.setOffline(true);
// Init script (runs in every new page in this context)
await context.addInitScript(() => {
window.__TEST_MODE__ = true;
});
// Route all API calls in the context
await context.route("**/api/**", async (route) => {
await route.fulfill({ status: 200, body: "{}" });
});
// Storage state (for reusing authentication)
const storageState = await context.storageState({ path: "auth.json" });
// Create a new page in this context
const page = await context.newPage();
// Wait for a new page event (popup)
const pagePromise = context.waitForEvent("page");
await page.getByRole("link", { name: "Open" }).click();
const newPage = await pagePromise;Properties
| Property | Type | Description |
|---|---|---|
clock | Clock | Clock for time manipulation |
request | APIRequestContext | API request context for direct HTTP calls |
tracing | Tracing | Tracing control |
// Use context-level API request
const response = await context.request.get("https://api.example.com/data");
// Tracing
await context.tracing.start({ screenshots: true, snapshots: true });
// ... run test actions ...
await context.tracing.stop({ path: "trace.zip" });Events
| Event | Callback Argument | Description |
|---|---|---|
close | BrowserContext | Context closed |
console | ConsoleMessage | Console message from any page |
dialog | Dialog | Dialog from any page |
page | Page | New page created in context |
request | Request | Request issued from any page |
requestfailed | Request | Request failed in any page |
requestfinished | Request | Request completed in any page |
response | Response | Response received in any page |
serviceworker | Worker | Service worker created |
weberror | WebError | Uncaught error from any page |
context.on("page", async (page) => {
console.log("New page opened:", page.url());
});
context.on("request", (request) => {
console.log(`>> ${request.method()} ${request.url()}`);
});---
Browser
The Browser is launched by BrowserType and can create multiple isolated BrowserContext instances. In @playwright/test, you typically use the browser fixture.
Methods
| Method | Signature |
|---|---|
browserType | () => BrowserType |
close | (options?: { reason?: string }) => Promise<void> |
contexts | () => BrowserContext[] |
isConnected | () => boolean |
newBrowserCDPSession | () => Promise<CDPSession> |
newContext | (options?: BrowserContextOptions) => Promise<BrowserContext> |
newPage | (options?: BrowserContextOptions) => Promise<Page> |
removeAllListeners | `(type?: string, options?: { behavior?: "wait" \ |
startTracing | (page?: Page, options?: { categories?: string[]; path?: string; screenshots?: boolean }) => Promise<void> |
stopTracing | () => Promise<Buffer> |
version | () => string |
Events
| Event | Callback Argument | Description |
|---|---|---|
disconnected | Browser | Browser disconnected |
import { test } from "@playwright/test";
test("multiple contexts", async ({ browser }) => {
// Create two isolated contexts
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// These pages do not share cookies or storage
await page1.goto("https://example.com");
await page2.goto("https://example.com");
await context1.close();
await context2.close();
});
// newContext with options
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
locale: "en-US",
timezoneId: "America/New_York",
geolocation: { latitude: 40.7128, longitude: -74.006 },
permissions: ["geolocation"],
colorScheme: "dark",
userAgent: "custom-agent",
httpCredentials: { username: "user", password: "pass" },
ignoreHTTPSErrors: true,
storageState: "auth.json",
recordVideo: { dir: "videos/", size: { width: 1280, height: 720 } },
});
// Shortcut: newPage creates a new context + page
const page = await browser.newPage({ locale: "fr-FR" });---
BrowserType
BrowserType provides methods to launch or connect to a browser. Access via chromium, firefox, or webkit from Playwright.
Methods
| Method | Signature |
|---|---|
connect | (wsEndpoint: string, options?: { exposeNetwork?: string; headers?: Record<string, string>; logger?: Logger; slowMo?: number; timeout?: number }) => Promise<Browser> |
connectOverCDP | (endpointURL: string, options?: { headers?: Record<string, string>; logger?: Logger; slowMo?: number; timeout?: number }) => Promise<Browser> |
executablePath | () => string |
launch | (options?: LaunchOptions) => Promise<Browser> |
launchPersistentContext | (userDataDir: string, options?: LaunchOptions & BrowserContextOptions) => Promise<BrowserContext> |
launchServer | (options?: LaunchOptions & { port?: number }) => Promise<BrowserServer> |
name | () => string |
Key Launch Options
| Option | Type | Description |
|---|---|---|
headless | boolean | Run in headless mode (default: true) |
channel | string | Browser channel ("chrome", "msedge", etc.) |
slowMo | number | Slow down operations by ms |
args | string[] | Additional browser arguments |
proxy | { server: string; bypass?: string; username?: string; password?: string } | Network proxy settings |
downloadsPath | string | Path for downloads |
chromiumSandbox | boolean | Enable Chromium sandbox |
executablePath | string | Path to a browser executable |
timeout | number | Launch timeout in ms (default: 30000) |
tracesDir | string | Directory for traces |
import { chromium, firefox, webkit } from "@playwright/test";
// Launch a browser directly (outside of test fixtures)
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
// Launch with a specific channel
const browser2 = await chromium.launch({ channel: "chrome" });
// Launch persistent context (retains user data between runs)
const context = await chromium.launchPersistentContext("/tmp/user-data", {
headless: false,
});
const page2 = context.pages()[0] || (await context.newPage());
// Connect to a remote browser
const browser3 = await chromium.connect("ws://localhost:3000");
// Get browser name and path
console.log(chromium.name()); // "chromium"
console.log(chromium.executablePath());Locator
The Locator class represents a way to find elements on a page at any moment. Locators are strict by default -- they throw if more than one element matches. They are created with page.locator(), page.getByRole(), and similar methods.
Locators auto-wait for elements to be actionable before performing actions. They auto-retry if the element is not ready.
import { test, expect } from "@playwright/test";
test("example", async ({ page }) => {
await page.goto("https://example.com");
const button = page.getByRole("button", { name: "Submit" });
await button.click();
await expect(button).toBeDisabled();
});Actions
| Method | Signature |
|---|---|
click | `(options?: { button?: "left" \ |
dblclick | `(options?: { button?: "left" \ |
check | (options?: { force?: boolean; noWaitAfter?: boolean; position?: { x: number; y: number }; timeout?: number; trial?: boolean }) => Promise<void> |
uncheck | (options?: { force?: boolean; noWaitAfter?: boolean; position?: { x: number; y: number }; timeout?: number; trial?: boolean }) => Promise<void> |
setChecked | (checked: boolean, options?: { force?: boolean; noWaitAfter?: boolean; position?: { x: number; y: number }; timeout?: number; trial?: boolean }) => Promise<void> |
fill | (value: string, options?: { force?: boolean; noWaitAfter?: boolean; timeout?: number }) => Promise<void> |
clear | (options?: { force?: boolean; noWaitAfter?: boolean; timeout?: number }) => Promise<void> |
selectOption | `(values: string \ |
setInputFiles | `(files: string \ |
hover | `(options?: { force?: boolean; modifiers?: ("Alt" \ |
tap | `(options?: { force?: boolean; modifiers?: ("Alt" \ |
focus | (options?: { timeout?: number }) => Promise<void> |
blur | (options?: { timeout?: number }) => Promise<void> |
press | (key: string, options?: { delay?: number; noWaitAfter?: boolean; timeout?: number }) => Promise<void> |
pressSequentially | (text: string, options?: { delay?: number; noWaitAfter?: boolean; timeout?: number }) => Promise<void> |
dragTo | (target: Locator, options?: { force?: boolean; noWaitAfter?: boolean; sourcePosition?: { x: number; y: number }; targetPosition?: { x: number; y: number }; timeout?: number; trial?: boolean }) => Promise<void> |
scrollIntoViewIfNeeded | (options?: { timeout?: number }) => Promise<void> |
dispatchEvent | (type: string, eventInit?: Serializable, options?: { timeout?: number }) => Promise<void> |
// Click with options
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("button").click({ button: "right" });
await page.getByRole("button").click({ modifiers: ["Shift"] });
await page.getByRole("button").click({ force: true }); // skip actionability checks
// Double click
await page.getByRole("cell").dblclick();
// Fill input fields (clears existing value first)
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Email").clear();
// Type character by character (simulates real typing)
await page.getByLabel("Search").pressSequentially("hello", { delay: 100 });
// Press keys
await page.getByLabel("Search").press("Enter");
await page.getByRole("textbox").press("Control+a");
// Checkbox
await page.getByRole("checkbox", { name: "Agree" }).check();
await page.getByRole("checkbox", { name: "Agree" }).uncheck();
await page.getByRole("checkbox").setChecked(true);
// Select option
await page.getByLabel("Country").selectOption("US");
await page.getByLabel("Country").selectOption({ label: "United States" });
await page.getByLabel("Colors").selectOption(["red", "green", "blue"]);
// File upload
await page.getByLabel("Upload").setInputFiles("./file.pdf");
await page.getByLabel("Upload").setInputFiles([
{ name: "test.txt", mimeType: "text/plain", buffer: Buffer.from("content") },
]);
await page.getByLabel("Upload").setInputFiles([]); // clear files
// Hover
await page.getByText("Hover me").hover();
// Drag and drop
await page.getByTestId("source").dragTo(page.getByTestId("target"));
// Focus and blur
await page.getByLabel("Name").focus();
await page.getByLabel("Name").blur();
// Dispatch custom event
await page.getByTestId("canvas").dispatchEvent("click");Content Retrieval
| Method | Signature |
|---|---|
textContent | `(options?: { timeout?: number }) => Promise<string \ |
innerText | (options?: { timeout?: number }) => Promise<string> |
innerHTML | (options?: { timeout?: number }) => Promise<string> |
inputValue | (options?: { timeout?: number }) => Promise<string> |
getAttribute | `(name: string, options?: { timeout?: number }) => Promise<string \ |
allTextContents | () => Promise<string[]> |
allInnerTexts | () => Promise<string[]> |
boundingBox | `(options?: { timeout?: number }) => Promise<{ x: number; y: number; width: number; height: number } \ |
ariaSnapshot | (options?: { timeout?: number }) => Promise<string> |
// Get text content
const text = await page.getByTestId("message").textContent();
const innerText = await page.getByTestId("message").innerText();
const html = await page.getByTestId("message").innerHTML();
// Get input value
const value = await page.getByLabel("Email").inputValue();
// Get attribute
const href = await page.getByRole("link", { name: "Home" }).getAttribute("href");
// Get all text contents from multiple elements
const allTexts = await page.getByRole("listitem").allTextContents();
// Get bounding box
const box = await page.getByRole("button").boundingBox();
if (box) {
console.log(`Button at (${box.x}, ${box.y}), size ${box.width}x${box.height}`);
}State Inspection
| Method | Signature |
|---|---|
isVisible | (options?: { timeout?: number }) => Promise<boolean> |
isHidden | (options?: { timeout?: number }) => Promise<boolean> |
isEnabled | (options?: { timeout?: number }) => Promise<boolean> |
isDisabled | (options?: { timeout?: number }) => Promise<boolean> |
isEditable | (options?: { timeout?: number }) => Promise<boolean> |
isChecked | (options?: { timeout?: number }) => Promise<boolean> |
// These do NOT auto-wait. Prefer assertions (expect) for waiting.
const visible = await page.getByRole("button").isVisible();
const enabled = await page.getByRole("button").isEnabled();
const checked = await page.getByRole("checkbox").isChecked();
// Preferred: use assertions which auto-retry
await expect(page.getByRole("button")).toBeVisible();
await expect(page.getByRole("button")).toBeEnabled();
await expect(page.getByRole("checkbox")).toBeChecked();Collection
| Method | Signature |
|---|---|
all | () => Promise<Locator[]> |
count | () => Promise<number> |
first | () => Locator |
last | () => Locator |
nth | (index: number) => Locator |
// Count elements
const count = await page.getByRole("listitem").count();
// Get first, last, nth
const first = page.getByRole("listitem").first();
const last = page.getByRole("listitem").last();
const third = page.getByRole("listitem").nth(2); // 0-indexed
// Iterate over all elements
const items = await page.getByRole("listitem").all();
for (const item of items) {
console.log(await item.textContent());
}Filtering and Composition
| Method | Signature |
|---|---|
filter | `(options?: { has?: Locator; hasNot?: Locator; hasNotText?: string \ |
and | (locator: Locator) => Locator |
or | (locator: Locator) => Locator |
getByRole | (role: AriaRole, options?: { ... }) => Locator |
getByText | `(text: string \ |
getByLabel | `(text: string \ |
getByPlaceholder | `(text: string \ |
getByAltText | `(text: string \ |
getByTitle | `(text: string \ |
getByTestId | `(testId: string \ |
// Filter by text
page.getByRole("listitem").filter({ hasText: "Product A" });
page.getByRole("listitem").filter({ hasNotText: "Out of stock" });
// Filter by child locator
page.getByRole("listitem").filter({
has: page.getByRole("button", { name: "Add to cart" }),
});
// Filter by absence of child
page.getByRole("listitem").filter({
hasNot: page.getByText("Sold out"),
});
// Combine with AND (both conditions must match)
const saveButton = page.getByRole("button").and(page.getByText("Save"));
// Combine with OR (either condition matches)
const actionButton = page
.getByRole("button", { name: "Save" })
.or(page.getByRole("button", { name: "Submit" }));
// Chained sub-locators (scoped search within parent)
const product = page.getByRole("listitem").filter({ hasText: "Widget" });
await product.getByRole("button", { name: "Add to cart" }).click();Sub-Locators
| Method | Signature |
|---|---|
locator | `(selectorOrLocator: string \ |
frameLocator | (selector: string) => FrameLocator |
contentFrame | () => FrameLocator |
// Find within a parent
const dialog = page.getByRole("dialog");
await dialog.locator("input[name='email']").fill("test@test.com");
await dialog.getByRole("button", { name: "Submit" }).click();
// Access iframe content
const frame = page.locator("iframe").contentFrame();
await frame.getByRole("button", { name: "Click" }).click();
// Alternative: frameLocator
const frame2 = page.frameLocator("#my-frame");
await frame2.getByText("Hello").click();Evaluation
| Method | Signature |
|---|---|
evaluate | `(pageFunction: Function \ |
evaluateAll | `(pageFunction: Function \ |
evaluateHandle | `(pageFunction: Function \ |
// Evaluate on a single element
const tagName = await page.getByTestId("element").evaluate(
(el) => el.tagName,
);
// Evaluate on all matched elements (does NOT auto-wait)
const texts = await page.getByRole("listitem").evaluateAll(
(elements) => elements.map((el) => el.textContent),
);Utilities
| Method | Signature |
|---|---|
waitFor | `(options?: { state?: "attached" \ |
screenshot | `(options?: { animations?: "disabled" \ |
highlight | () => Promise<void> |
describe | (description: string) => Locator |
description | `() => string \ |
toString | () => string |
// Wait for element to appear
await page.getByRole("status").waitFor({ state: "visible" });
// Wait for element to be removed
await page.getByRole("progressbar").waitFor({ state: "detached" });
// Screenshot of a specific element
await page.getByTestId("chart").screenshot({ path: "chart.png" });
// Highlight for debugging
await page.getByRole("button").highlight();
// Add description for debugging
const btn = page.getByRole("button").describe("The main submit button");
console.log(btn.description()); // "The main submit button"
// String representation
console.log(page.getByRole("button", { name: "Submit" }).toString());
// → locator('role=button[name="Submit"]')Page
The Page class represents a single tab or popup window in a browser. It provides methods to navigate, interact with elements, evaluate scripts, intercept network requests, and more. One BrowserContext can have multiple pages.
import { test, expect } from "@playwright/test";
test("example", async ({ page }) => {
await page.goto("https://example.com");
await expect(page).toHaveTitle(/Example/);
});Navigation
| Method | Signature |
|---|---|
goto | `(url: string, options?: { referer?: string; timeout?: number; waitUntil?: "load" \ |
goBack | `(options?: { timeout?: number; waitUntil?: "load" \ |
goForward | `(options?: { timeout?: number; waitUntil?: "load" \ |
reload | `(options?: { timeout?: number; waitUntil?: "load" \ |
// Navigate and wait for network idle
await page.goto("https://example.com", { waitUntil: "networkidle" });
// Go back/forward
await page.goBack();
await page.goForward();
// Reload
await page.reload();Element Location
| Method | Signature |
|---|---|
locator | `(selector: string, options?: { has?: Locator; hasNot?: Locator; hasNotText?: string \ |
getByRole | `(role: AriaRole, options?: { checked?: boolean; disabled?: boolean; exact?: boolean; expanded?: boolean; includeHidden?: boolean; level?: number; name?: string \ |
getByText | `(text: string \ |
getByLabel | `(text: string \ |
getByPlaceholder | `(text: string \ |
getByAltText | `(text: string \ |
getByTitle | `(text: string \ |
getByTestId | `(testId: string \ |
frameLocator | (selector: string) => FrameLocator |
frame | `(frameSelector: string \ |
frames | () => Array<Frame> |
mainFrame | () => Frame |
// Preferred: role-based locators
page.getByRole("button", { name: "Submit" });
page.getByRole("heading", { level: 1 });
page.getByRole("checkbox", { checked: true });
// Text-based
page.getByText("Welcome");
page.getByText(/welcome/i);
// Form-related
page.getByLabel("Email");
page.getByPlaceholder("Enter your email");
// Other semantic locators
page.getByAltText("Company logo");
page.getByTitle("Close dialog");
page.getByTestId("submit-button");
// CSS/XPath selector (use as fallback)
page.locator("css=div.container >> text=Hello");
page.locator("xpath=//button[@type='submit']");
// Filtering with has/hasText
page.locator("article", { hasText: "Playwright" });
page.locator("article", { has: page.getByRole("button") });
// Frame locators
const frame = page.frameLocator("#my-iframe");
await frame.getByRole("button").click();Evaluation
| Method | Signature |
|---|---|
evaluate | `(pageFunction: Function \ |
evaluateHandle | `(pageFunction: Function \ |
addInitScript | `(script: Function \ |
addScriptTag | (options?: { content?: string; path?: string; type?: string; url?: string }) => Promise<ElementHandle> |
addStyleTag | (options?: { content?: string; path?: string; url?: string }) => Promise<ElementHandle> |
exposeFunction | (name: string, callback: Function) => Promise<void> |
exposeBinding | (name: string, callback: Function, options?: { handle?: boolean }) => Promise<void> |
// Evaluate expression in browser context
const title = await page.evaluate(() => document.title);
// Pass arguments to evaluate
const text = await page.evaluate(
([selector]) => document.querySelector(selector)?.textContent,
[".my-class"],
);
// Add init script that runs before every navigation
await page.addInitScript(() => {
window.myGlobal = "test-value";
});
// Expose a Node.js function to the browser
await page.exposeFunction("sha256", (text: string) => {
return crypto.createHash("sha256").update(text).digest("hex");
});Waiting
| Method | Signature |
|---|---|
waitForEvent | `(event: string, optionsOrPredicate?: Function \ |
waitForFunction | `(pageFunction: Function \ |
waitForLoadState | `(state?: "load" \ |
waitForRequest | `(urlOrPredicate: string \ |
waitForResponse | `(urlOrPredicate: string \ |
waitForTimeout | (timeout: number) => Promise<void> |
waitForURL | `(url: string \ |
// Wait for navigation after a click
await page.getByRole("link", { name: "Dashboard" }).click();
await page.waitForURL("**/dashboard");
// Wait for a specific network request
const requestPromise = page.waitForRequest("**/api/users");
await page.getByRole("button", { name: "Load Users" }).click();
const request = await requestPromise;
// Wait for a specific network response
const responsePromise = page.waitForResponse(
(resp) => resp.url().includes("/api/data") && resp.status() === 200,
);
await page.getByRole("button", { name: "Fetch" }).click();
const response = await responsePromise;
// Wait for load state
await page.waitForLoadState("networkidle");
// Wait for a condition in the page
await page.waitForFunction(() => document.querySelector(".loaded") !== null);
// Wait for an event
const popup = await page.waitForEvent("popup");Routing
| Method | Signature |
|---|---|
route | `(url: string \ |
routeFromHAR | `(har: string, options?: { notFound?: "abort" \ |
unroute | `(url: string \ |
unrouteAll | `(options?: { behavior?: "wait" \ |
// Mock an API endpoint
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Alice" }]),
});
});
// Modify a request
await page.route("**/api/data", async (route, request) => {
const headers = { ...request.headers(), "x-custom": "value" };
await route.continue({ headers });
});
// Abort image requests
await page.route("**/*.{png,jpg,jpeg}", (route) => route.abort());
// Route only 3 times
await page.route("**/api/poll", (route) => route.fulfill({ body: "ok" }), {
times: 3,
});
// Route from HAR file
await page.routeFromHAR("./fixtures/api.har", { url: "**/api/**" });Content
| Method | Signature |
|---|---|
content | () => Promise<string> |
setContent | `(html: string, options?: { timeout?: number; waitUntil?: "load" \ |
dragAndDrop | (source: string, target: string, options?: { force?: boolean; noWaitAfter?: boolean; sourcePosition?: { x: number; y: number }; strict?: boolean; targetPosition?: { x: number; y: number }; timeout?: number; trial?: boolean }) => Promise<void> |
title | () => Promise<string> |
url | () => string |
// Get page content
const html = await page.content();
// Set page content directly
await page.setContent("<h1>Test</h1><p>Hello world</p>");
// Drag and drop
await page.dragAndDrop("#source", "#target");Screenshots and PDF
| Method | Signature |
|---|---|
screenshot | `(options?: { animations?: "disabled" \ |
pdf | `(options?: { displayHeaderFooter?: boolean; footerTemplate?: string; format?: string; headerTemplate?: string; height?: string \ |
// Full-page screenshot
await page.screenshot({ path: "screenshot.png", fullPage: true });
// Screenshot with mask
await page.screenshot({
path: "screenshot.png",
mask: [page.locator(".dynamic-content")],
maskColor: "#FF00FF",
});
// Generate PDF (Chromium only)
await page.pdf({ path: "page.pdf", format: "A4" });Configuration
| Method | Signature |
|---|---|
emulateMedia | `(options?: { colorScheme?: "light" \ |
setExtraHTTPHeaders | (headers: Record<string, string>) => Promise<void> |
setDefaultTimeout | (timeout: number) => void |
setDefaultNavigationTimeout | (timeout: number) => void |
setViewportSize | (viewportSize: { width: number; height: number }) => Promise<void> |
viewportSize | `() => { width: number; height: number } \ |
// Emulate dark mode
await page.emulateMedia({ colorScheme: "dark" });
// Emulate print media
await page.emulateMedia({ media: "print" });
// Set viewport
await page.setViewportSize({ width: 1280, height: 720 });
// Set custom headers for all requests
await page.setExtraHTTPHeaders({ Authorization: "Bearer token123" });
// Configure timeouts
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(30000);Handler Management
| Method | Signature |
|---|---|
addLocatorHandler | (locator: Locator, handler: (locator: Locator) => Promise<void>, options?: { noWaitAfter?: boolean; times?: number }) => Promise<void> |
removeLocatorHandler | (locator: Locator) => Promise<void> |
// Dismiss a cookie banner whenever it appears
await page.addLocatorHandler(
page.getByRole("button", { name: "Accept Cookies" }),
async (button) => {
await button.click();
},
);
// Remove the handler later
await page.removeLocatorHandler(
page.getByRole("button", { name: "Accept Cookies" }),
);State and Lifecycle
| Method | Signature |
|---|---|
close | (options?: { reason?: string; runBeforeUnload?: boolean }) => Promise<void> |
isClosed | () => boolean |
bringToFront | () => Promise<void> |
pause | () => Promise<void> |
opener | `() => Promise<Page \ |
context | () => BrowserContext |
requestGC | () => Promise<void> |
// Close page
await page.close();
// Check if closed
if (!page.isClosed()) {
await page.bringToFront();
}
// Pause for debugging (opens Playwright Inspector)
await page.pause();
// Access parent context
const context = page.context();Properties
| Property | Type | Description |
|---|---|---|
keyboard | Keyboard | Keyboard input control |
mouse | Mouse | Mouse input control |
touchscreen | Touchscreen | Touchscreen input control |
clock | Clock | Clock control for time-related testing |
video | `Video \ | null` |
// Keyboard
await page.keyboard.type("Hello World");
await page.keyboard.press("Enter");
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowLeft");
await page.keyboard.up("Shift");
await page.keyboard.insertText("some text");
// Mouse
await page.mouse.move(100, 200);
await page.mouse.click(100, 200);
await page.mouse.dblclick(100, 200);
await page.mouse.down();
await page.mouse.up();
await page.mouse.wheel(0, 100);
// Touchscreen
await page.touchscreen.tap(100, 200);
// Clock - freeze time
await page.clock.install({ time: new Date("2024-01-01T00:00:00") });
await page.clock.setFixedTime(new Date("2024-06-15T12:00:00"));
await page.clock.fastForward(30000); // 30 seconds
await page.clock.pauseAt(new Date("2024-01-01T10:00:00"));
await page.clock.resume();
await page.clock.setSystemTime(new Date("2024-01-01T00:00:00"));Events
| Event | Callback Argument | Description |
|---|---|---|
close | Page | Page is closed |
console | ConsoleMessage | Console message emitted in browser |
dialog | Dialog | Dialog appeared (alert, confirm, prompt, beforeunload) |
download | Download | Download started |
filechooser | FileChooser | File chooser dialog opened |
frameattached | Frame | Frame attached to the page |
framedetached | Frame | Frame detached from the page |
framenavigated | Frame | Frame navigated to a new URL |
load | Page | Load event fired |
pageerror | Error | Uncaught exception in the page |
popup | Page | New popup window opened |
request | Request | Network request issued |
requestfailed | Request | Network request failed |
requestfinished | Request | Network request completed |
response | Response | Network response received |
websocket | WebSocket | WebSocket connection created |
worker | Worker | Web Worker spawned |
// Listen for console messages
page.on("console", (msg) => {
console.log(`Browser console [${msg.type()}]: ${msg.text()}`);
});
// Handle dialogs (alert, confirm, prompt)
page.on("dialog", async (dialog) => {
console.log(`Dialog: ${dialog.message()}`);
await dialog.accept("answer");
});
// Wait for a popup
const popupPromise = page.waitForEvent("popup");
await page.getByRole("link", { name: "Open Popup" }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
// Capture downloads
const downloadPromise = page.waitForEvent("download");
await page.getByRole("link", { name: "Download" }).click();
const download = await downloadPromise;
await download.saveAs("/tmp/file.pdf");
// Monitor network requests
page.on("request", (request) => {
console.log(`>> ${request.method()} ${request.url()}`);
});
page.on("response", (response) => {
console.log(`<< ${response.status()} ${response.url()}`);
});
// Catch page errors
page.on("pageerror", (error) => {
console.error(`Page error: ${error.message}`);
});API
| Name | Description | Path |
|---|---|---|
| Assertions | Playwright uses expect from @playwright/test which extends Jest-like assertions with auto-retrying, web-first matchers. | assertions.md |
| APIRequestContext, Request, Response, and Route | APIRequestContext allows sending HTTP requests directly without a browser page. | request.md |
| BrowserContext, Browser, and BrowserType | A BrowserContext is an isolated browser session. | browser-context.md |
| Locator | The Locator class represents a way to find elements on a page at any moment. | locator.md |
| Page | The Page class represents a single tab or popup window in a browser. | page.md |
APIRequestContext, Request, Response, and Route
APIRequestContext
APIRequestContext allows sending HTTP requests directly without a browser page. Useful for API testing, setting up test data, or validating server state. In @playwright/test, use the request fixture or context.request.
import { test, expect } from "@playwright/test";
test("API test", async ({ request }) => {
const response = await request.get("https://api.example.com/users");
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toHaveLength(3);
});Methods
| Method | Signature |
|---|---|
get | (url: string, options?: RequestOptions) => Promise<APIResponse> |
post | (url: string, options?: RequestOptions) => Promise<APIResponse> |
put | (url: string, options?: RequestOptions) => Promise<APIResponse> |
patch | (url: string, options?: RequestOptions) => Promise<APIResponse> |
delete | (url: string, options?: RequestOptions) => Promise<APIResponse> |
head | (url: string, options?: RequestOptions) => Promise<APIResponse> |
fetch | `(urlOrRequest: string \ |
dispose | (options?: { reason?: string }) => Promise<void> |
storageState | (options?: { path?: string }) => Promise<StorageState> |
RequestOptions
| Option | Type | Description |
|---|---|---|
data | `string \ | Buffer \ |
form | `Record<string, string \ | number \ |
multipart | `FormData \ | Record<string, string \ |
headers | Record<string, string> | Request headers |
params | `Record<string, string \ | number \ |
failOnStatusCode | boolean | Throw on non-2xx/3xx status (default: false) |
ignoreHTTPSErrors | boolean | Ignore HTTPS errors |
maxRedirects | number | Maximum redirects to follow (default: 20) |
maxRetries | number | Maximum retries on network errors (default: 0) |
timeout | number | Request timeout in ms (default: 30000) |
// GET with query params
const resp = await request.get("/api/users", {
params: { page: 1, limit: 10 },
});
// POST JSON
const created = await request.post("/api/users", {
data: { name: "Alice", email: "alice@example.com" },
});
expect(created.status()).toBe(201);
// POST form data
const login = await request.post("/login", {
form: { username: "admin", password: "secret" },
});
// POST multipart (file upload)
const upload = await request.post("/api/upload", {
multipart: {
file: {
name: "report.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("pdf content"),
},
description: "Monthly report",
},
});
// PUT
await request.put("/api/users/1", {
data: { name: "Alice Updated" },
});
// PATCH
await request.patch("/api/users/1", {
data: { email: "newemail@example.com" },
});
// DELETE
await request.delete("/api/users/1");
// HEAD
const head = await request.head("/api/health");
expect(head.ok()).toBeTruthy();
// Generic fetch with custom method
const resp2 = await request.fetch("/api/resource", {
method: "OPTIONS",
headers: { "Access-Control-Request-Method": "POST" },
});
// Fail on non-2xx status
const resp3 = await request.get("/api/data", { failOnStatusCode: true });
// Save/restore storage state for authentication reuse
await request.storageState({ path: "auth.json" });
// Dispose when done (if created manually, not needed for fixtures)
await request.dispose();Using with Authentication
import { test as setup } from "@playwright/test";
setup("authenticate", async ({ request }) => {
// Log in via API
await request.post("/api/login", {
data: { username: "admin", password: "password" },
});
// Save signed-in state
await request.storageState({ path: ".auth/admin.json" });
});---
Request
The Request class represents a network request made by a page. Obtained from page.on("request"), route.request(), or page.waitForRequest().
Methods
| Method | Signature |
|---|---|
allHeaders | () => Promise<Record<string, string>> |
failure | `() => { errorText: string } \ |
frame | () => Frame |
headerValue | `(name: string) => Promise<string \ |
headers | () => Record<string, string> |
headersArray | () => Promise<Array<{ name: string; value: string }>> |
isNavigationRequest | () => boolean |
method | () => string |
postData | `() => string \ |
postDataBuffer | `() => Buffer \ |
postDataJSON | `() => Serializable \ |
redirectedFrom | `() => Request \ |
redirectedTo | `() => Request \ |
resourceType | () => string |
response | `() => Promise<Response \ |
serviceWorker | `() => Worker \ |
sizes | () => Promise<{ requestBodySize: number; requestHeadersSize: number; responseBodySize: number; responseHeadersSize: number }> |
timing | () => { startTime: number; domainLookupStart: number; domainLookupEnd: number; connectStart: number; secureConnectionStart: number; connectEnd: number; requestStart: number; responseStart: number; responseEnd: number } |
url | () => string |
// Capture requests
page.on("request", (request) => {
console.log(`${request.method()} ${request.url()}`);
console.log("Resource type:", request.resourceType());
if (request.method() === "POST") {
console.log("Post data:", request.postDataJSON());
}
});
// Wait for a specific request
const requestPromise = page.waitForRequest(
(req) => req.url().includes("/api/submit") && req.method() === "POST",
);
await page.getByRole("button", { name: "Submit" }).click();
const request = await requestPromise;
console.log("Submitted:", request.postDataJSON());
// Check request failure
page.on("requestfailed", (request) => {
const failure = request.failure();
console.log(`${request.url()} failed: ${failure?.errorText}`);
});
// Redirect chain
const response = await page.goto("http://example.com"); // redirects to https
const request2 = response!.request();
const redirectedFrom = request2.redirectedFrom();
console.log("Redirected from:", redirectedFrom?.url());
// Request timing
const timing = request.timing();
console.log("Time to first byte:", timing.responseStart - timing.startTime);
// Request sizes
const sizes = await request.sizes();
console.log("Response body size:", sizes.responseBodySize);---
Response
The Response class represents a network response received by a page. Obtained from page.on("response"), request.response(), page.goto(), or page.waitForResponse().
Methods
| Method | Signature |
|---|---|
allHeaders | () => Promise<Record<string, string>> |
body | () => Promise<Buffer> |
finished | `() => Promise<Error \ |
frame | () => Frame |
fromServiceWorker | () => boolean |
headerValue | `(name: string) => Promise<string \ |
headerValues | (name: string) => Promise<string[]> |
headers | () => Record<string, string> |
headersArray | () => Promise<Array<{ name: string; value: string }>> |
json | () => Promise<Serializable> |
ok | () => boolean |
request | () => Request |
securityDetails | `() => Promise<{ issuer: string; protocol: string; subjectName: string; validFrom: number; validTo: number } \ |
serverAddr | `() => Promise<{ ipAddress: string; port: number } \ |
status | () => number |
statusText | () => string |
text | () => Promise<string> |
url | () => string |
// Wait for response and inspect
const responsePromise = page.waitForResponse("**/api/data");
await page.getByRole("button", { name: "Load" }).click();
const response = await responsePromise;
console.log("Status:", response.status(), response.statusText());
console.log("OK:", response.ok()); // true if 200-299
console.log("URL:", response.url());
// Parse response body
const json = await response.json();
const text = await response.text();
const buffer = await response.body();
// Headers
const contentType = await response.headerValue("content-type");
const allHeaders = await response.allHeaders();
const setCookies = await response.headerValues("set-cookie");
// Check when response finished
const error = await response.finished();
if (error) {
console.log("Response error:", error.message);
}
// Security details (HTTPS)
const security = await response.securityDetails();
if (security) {
console.log("Protocol:", security.protocol);
console.log("Issuer:", security.issuer);
}
// Server address
const addr = await response.serverAddr();
console.log(`Server: ${addr?.ipAddress}:${addr?.port}`);
// Navigate and check response
const navResponse = await page.goto("https://example.com");
expect(navResponse?.status()).toBe(200);---
Route
The Route class represents an intercepted network request. Used within page.route() or context.route() handlers. Each route must be fulfilled, continued, aborted, or passed to fallback.
Methods
| Method | Signature |
|---|---|
abort | (errorCode?: string) => Promise<void> |
continue | `(options?: { headers?: Record<string, string>; method?: string; postData?: string \ |
fallback | `(options?: { headers?: Record<string, string>; method?: string; postData?: string \ |
fetch | `(options?: { headers?: Record<string, string>; maxRedirects?: number; maxRetries?: number; method?: string; postData?: string \ |
fulfill | `(options?: { body?: string \ |
request | () => Request |
Error Codes for abort()
aborted, accessdenied, addressunreachable, blockedbyclient, blockedbyresponse, connectionaborted, connectionclosed, connectionfailed, connectionrefused, connectionreset, internetdisconnected, namenotresolved, timedout, failed
// Fulfill with mock data
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
json: [{ id: 1, name: "Alice" }],
});
});
// Fulfill from a file
await page.route("**/api/config", async (route) => {
await route.fulfill({ path: "./fixtures/config.json" });
});
// Modify request and continue
await page.route("**/api/**", async (route, request) => {
await route.continue({
headers: { ...request.headers(), "x-test": "true" },
});
});
// Abort requests
await page.route("**/*.{png,jpg,gif}", async (route) => {
await route.abort();
});
// Fetch, modify, and fulfill (intercept response)
await page.route("**/api/users", async (route) => {
const response = await route.fetch();
const json = await response.json();
// Modify the response
json.push({ id: 99, name: "Injected User" });
await route.fulfill({
response,
json,
});
});
// Fallback to other handlers (handler chaining)
// Handlers are evaluated in reverse registration order.
// fallback() passes to the next handler; continue() sends to network.
await page.route("**/api/**", async (route) => {
// Second handler: add auth header to all API requests
await route.fallback({
headers: { ...route.request().headers(), Authorization: "Bearer token" },
});
});
await page.route("**/api/users", async (route) => {
// First handler (registered later, runs first): mock /api/users
await route.fulfill({ json: [{ id: 1, name: "Mock" }] });
});
// Conditional routing
await page.route("**/api/**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({ json: { mocked: true } });
} else {
await route.continue();
}
});Actions
Playwright provides methods to interact with page elements. All actions auto-wait for the element to be actionable before performing the action (visible, stable, enabled, etc.). Actions automatically scroll the element into view.
Text Input
fill
Sets the value of <input>, <textarea>, and [contenteditable] elements. Clears existing content first. Triggers input and change events.
import { test } from "@playwright/test";
test("fill text inputs", async ({ page }) => {
// Text input
await page.getByRole("textbox").fill("Peter");
// Date input
await page.getByLabel("Birth date").fill("2020-02-02");
// Time input
await page.getByLabel("Appointment time").fill("13:15");
// Datetime-local input
await page.getByLabel("Local time").fill("2020-03-02T05:15");
});clear
Clears the input field.
test("clear input", async ({ page }) => {
await page.getByRole("textbox").clear();
});Checkboxes and Radio Buttons
check, uncheck, setChecked
Works with input[type=checkbox], input[type=radio], and [role=checkbox].
test("checkboxes and radios", async ({ page }) => {
// Check
await page.getByLabel("I agree to the terms above").check();
// Verify checked state
await expect(page.getByLabel("Subscribe to newsletter")).toBeChecked();
// Uncheck
await page.getByLabel("Subscribe to newsletter").uncheck();
// Set to specific state
await page.getByLabel("Remember me").setChecked(true);
await page.getByLabel("Remember me").setChecked(false);
// Radio buttons
await page.getByLabel("XL").check();
});Select Options
selectOption
Selects options in <select> elements. Supports matching by value, label, or element handle.
test("select options", async ({ page }) => {
// By value
await page.getByLabel("Choose a color").selectOption("blue");
// By label
await page.getByLabel("Choose a color").selectOption({ label: "Blue" });
// Multiple selections
await page
.getByLabel("Choose multiple colors")
.selectOption(["red", "green", "blue"]);
});Mouse Click
click
Performs a click action. Auto-waits for the element to be visible, stable, receiving events, and enabled.
test("click variants", async ({ page }) => {
// Basic click
await page.getByRole("button").click();
// Double click
await page.getByText("Item").dblclick();
// Right click (context menu)
await page.getByText("Item").click({ button: "right" });
});Click with Modifiers
test("click with modifiers", async ({ page }) => {
// Shift+click
await page.getByText("Item").click({ modifiers: ["Shift"] });
// Ctrl/Cmd+click (cross-platform)
await page.getByText("Item").click({ modifiers: ["ControlOrMeta"] });
// Multiple modifiers
await page.getByText("Item").click({ modifiers: ["Shift", "Alt"] });
});Click at Position
test("click at position", async ({ page }) => {
// Click relative to top-left corner of element
await page.getByText("Item").click({ position: { x: 0, y: 0 } });
// Click at center offset
await page.getByText("Item").click({ position: { x: 10, y: 20 } });
});Force Click
Bypasses actionability checks. Use when you know the element is ready but Playwright's checks fail.
test("force click", async ({ page }) => {
await page.getByRole("button").click({ force: true });
});Programmatic Click
Dispatches a click event directly via JavaScript, bypassing all actionability checks.
test("programmatic click", async ({ page }) => {
await page.getByRole("button").dispatchEvent("click");
});Hover
test("hover", async ({ page }) => {
await page.getByText("Item").hover();
});Type Characters
pressSequentially
Types text character by character, triggering all keyboard events (keydown, keypress, keyup) for each character. Use when the page has special keyboard handling. For normal form input, prefer fill().
test("press sequentially", async ({ page }) => {
// Type character by character
await page.locator("#area").pressSequentially("Hello World!");
// With delay between keystrokes (simulates real typing)
await page
.locator("#area")
.pressSequentially("Hello World!", { delay: 100 });
});Keys and Shortcuts
press
Produces a single keystroke or key combination. Supports named keys, modifiers, and shortcuts.
test("keyboard shortcuts", async ({ page }) => {
// Named keys
await page.getByText("Submit").press("Enter");
await page.getByRole("textbox").press("Tab");
await page.getByRole("textbox").press("Backspace");
// Key combinations
await page.getByRole("textbox").press("Control+ArrowRight");
await page.locator("#name").press("Shift+A");
await page.locator("#name").press("Shift+ArrowLeft");
// Character keys
await page.getByRole("textbox").press("$");
await page.getByRole("textbox").press("a");
// Function keys
await page.press("body", "F1");
});Supported key names: F1-F12, Digit0-Digit9, KeyA-KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, ArrowUp, ArrowLeft, ArrowRight, End, Home, Insert, PageDown, PageUp, Enter, Space, Shift, Control, Alt, Meta.
Upload Files
setInputFiles
Sets files on <input type="file"> elements. Supports single files, multiple files, directories, buffers, and clearing.
import path from "node:path";
test("file uploads", async ({ page }) => {
// Single file
await page
.getByLabel("Upload file")
.setInputFiles(path.join(__dirname, "myfile.pdf"));
// Multiple files
await page.getByLabel("Upload files").setInputFiles([
path.join(__dirname, "file1.txt"),
path.join(__dirname, "file2.txt"),
]);
// Directory upload
await page
.getByLabel("Upload directory")
.setInputFiles(path.join(__dirname, "mydir"));
// Clear files
await page.getByLabel("Upload file").setInputFiles([]);
// From buffer (no actual file on disk)
await page.getByLabel("Upload file").setInputFiles({
name: "file.txt",
mimeType: "text/plain",
buffer: Buffer.from("this is test"),
});
});Handle dynamically created file inputs:
test("file chooser dialog", async ({ page }) => {
const fileChooserPromise = page.waitForEvent("filechooser");
await page.getByLabel("Upload").click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, "myfile.pdf"));
});Focus
test("focus element", async ({ page }) => {
await page.getByLabel("Password").focus();
});Drag and Drop
dragTo
test("drag and drop", async ({ page }) => {
// High-level API
await page.locator("#item-to-be-dragged").dragTo(page.locator("#drop-zone"));
});Manual drag for precise control:
test("manual drag and drop", async ({ page }) => {
await page.locator("#item-to-be-dragged").hover();
await page.mouse.down();
await page.locator("#drop-zone").hover();
await page.mouse.up();
});Scrolling
Playwright auto-scrolls elements into view before actions. For manual scrolling:
test("scrolling", async ({ page }) => {
// Scroll element into view
await page.getByText("Footer text").scrollIntoViewIfNeeded();
// Mouse wheel scroll
await page.getByTestId("scrolling-container").hover();
await page.mouse.wheel(0, 10);
// Programmatic scroll via evaluate
await page
.getByTestId("scrolling-container")
.evaluate((e) => (e.scrollTop += 100));
});Assertions
Playwright uses expect with auto-retrying assertions that wait until the expected condition is met. The default timeout is 5 seconds, configurable via testConfig.expect. Always await auto-retrying assertions.
Auto-Retrying Locator Assertions (27)
These retry until the condition is met or the timeout expires.
Visibility and State
import { test, expect } from "@playwright/test";
test("visibility and state assertions", async ({ page }) => {
const locator = page.locator("#element");
// Attached to DOM (options: attached | detached)
await expect(locator).toBeAttached();
await expect(locator).toBeAttached({ attached: false });
// Visible / Hidden
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
// Enabled / Disabled
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
// Editable
await expect(locator).toBeEditable();
await expect(locator).toBeEditable({ editable: false });
// Focused
await expect(locator).toBeFocused();
// Empty (no children or text)
await expect(locator).toBeEmpty();
// Checked (checkbox/radio)
await expect(page.getByRole("checkbox")).toBeChecked();
await expect(page.getByRole("checkbox")).toBeChecked({ checked: false });
// In viewport
await expect(locator).toBeInViewport();
await expect(locator).toBeInViewport({ ratio: 0.5 });
});Content and Text
test("content assertions", async ({ page }) => {
const locator = page.locator("#element");
// Contains text (substring, case-insensitive by default)
await expect(locator).toContainText("expected text");
await expect(locator).toContainText(/regex pattern/i);
// Exact text match
await expect(locator).toHaveText("exact text");
await expect(locator).toHaveText(/full regex/);
// Multiple elements text (array)
await expect(page.getByRole("listitem")).toHaveText([
"first",
"second",
"third",
]);
// Count
await expect(page.getByRole("listitem")).toHaveCount(3);
});Attributes and Properties
test("attribute assertions", async ({ page }) => {
const locator = page.locator("#element");
// DOM attribute
await expect(locator).toHaveAttribute("href", "/docs");
await expect(locator).toHaveAttribute("href", /\/docs/);
await expect(locator).toHaveAttribute("href"); // attribute exists
// CSS class
await expect(locator).toHaveClass("btn primary");
await expect(locator).toHaveClass(/active/);
await expect(page.locator("li")).toHaveClass([
"item",
"item active",
"item",
]);
// Contains CSS classes (subset match)
await expect(locator).toContainClass("primary");
// CSS property (computed)
await expect(locator).toHaveCSS("display", "flex");
await expect(locator).toHaveCSS("color", "rgb(255, 0, 0)");
// ID
await expect(locator).toHaveId("main-content");
// JavaScript property
await expect(locator).toHaveJSProperty("loaded", true);
});Form Values
test("form value assertions", async ({ page }) => {
// Input value
await expect(page.getByLabel("Username")).toHaveValue("john");
await expect(page.getByLabel("Username")).toHaveValue(/john/i);
// Multi-select values
await expect(page.getByLabel("Colors")).toHaveValues([
/red/,
/green/,
/blue/,
]);
});Accessibility
test("accessibility assertions", async ({ page }) => {
const locator = page.locator("#element");
// ARIA role
await expect(locator).toHaveRole("button");
// Accessible name (from aria-label, label, text content, etc.)
await expect(locator).toHaveAccessibleName("Submit form");
await expect(locator).toHaveAccessibleName(/submit/i);
// Accessible description (from aria-describedby, etc.)
await expect(locator).toHaveAccessibleDescription("Click to submit");
// Accessible error message (from aria-errormessage)
await expect(locator).toHaveAccessibleErrorMessage("Field is required");
});Visual and Snapshots
test("visual assertions", async ({ page }) => {
// Screenshot comparison (locator-level)
await expect(page.locator("#chart")).toHaveScreenshot("chart.png");
await expect(page.locator("#chart")).toHaveScreenshot("chart.png", {
maxDiffPixels: 100,
});
// ARIA snapshot
await expect(page.locator("#nav")).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "About"
`);
});Page Assertions (3)
test("page assertions", async ({ page }) => {
// Page title
await expect(page).toHaveTitle("My App");
await expect(page).toHaveTitle(/My App/);
// Page URL
await expect(page).toHaveURL("https://example.com/dashboard");
await expect(page).toHaveURL(/dashboard/);
// Page screenshot comparison
await expect(page).toHaveScreenshot("full-page.png");
});API Response Assertions (1)
test("API response assertions", async ({ page }) => {
const response = await page.request.get("https://api.example.com/data");
// Status is 2xx
await expect(response).toBeOK();
await expect(response).not.toBeOK();
});Negation (.not)
Negate any assertion by inserting .not before the matcher.
test("negation", async ({ page }) => {
await expect(page.locator("#spinner")).not.toBeVisible();
await expect(page.locator("#input")).not.toHaveValue("old value");
await expect(page).not.toHaveURL(/login/);
});Soft Assertions
Soft assertions do not terminate the test on failure. The test continues but is marked as failed at the end.
test("soft assertions", async ({ page }) => {
await expect.soft(page.getByTestId("status")).toHaveText("Success");
await expect.soft(page.getByTestId("eta")).toHaveText("1 day");
// Optionally bail out if any soft assertion has failed
expect(test.info().errors).toHaveLength(0);
});Custom Expect Messages
Provide a second argument for debugging context when an assertion fails.
test("custom messages", async ({ page }) => {
await expect(page.getByText("Name"), "should be logged in").toBeVisible();
expect.soft(value, "my soft assertion").toBe(56);
});expect.configure
Create pre-configured expect instances with custom timeout or soft mode.
test("configured expect", async ({ page }) => {
// Longer timeout
const slowExpect = expect.configure({ timeout: 10_000 });
await slowExpect(page.locator("#slow-element")).toHaveText("Loaded");
// Pre-configured soft
const softExpect = expect.configure({ soft: true });
await softExpect(page.locator("#a")).toHaveText("A");
await softExpect(page.locator("#b")).toHaveText("B");
});expect.poll
Convert any synchronous check into a polling/retrying assertion.
test("polling assertion", async ({ page }) => {
// Basic polling
await expect
.poll(
async () => {
const response = await page.request.get("https://api.example.com");
return response.status();
},
{
message: "make sure API eventually succeeds",
timeout: 10_000,
},
)
.toBe(200);
// Custom polling intervals
await expect
.poll(
async () => {
const response = await page.request.get("https://api.example.com");
return response.status();
},
{
intervals: [1_000, 2_000, 10_000],
timeout: 60_000,
},
)
.toBe(200);
});expect.toPass
Retry a block of code until it passes without throwing.
test("toPass retry block", async ({ page }) => {
// Basic
await expect(async () => {
const response = await page.request.get("https://api.example.com");
expect(response.status()).toBe(200);
}).toPass();
// With options
await expect(async () => {
const response = await page.request.get("https://api.example.com");
expect(response.status()).toBe(200);
}).toPass({
intervals: [1_000, 2_000, 10_000],
timeout: 60_000,
});
});Custom Matchers (expect.extend)
Add project-specific assertions that integrate with Playwright's retry mechanism.
// fixtures.ts
import { expect as baseExpect } from "@playwright/test";
import type { Locator } from "@playwright/test";
export const expect = baseExpect.extend({
async toHaveAmount(
locator: Locator,
expected: number,
options?: { timeout?: number },
) {
const assertionName = "toHaveAmount";
let pass: boolean;
let matcherResult: any;
try {
await baseExpect(locator).toHaveAttribute(
"data-amount",
String(expected),
options,
);
pass = !this.isNot;
} catch (e: any) {
matcherResult = e.matcherResult;
pass = this.isNot;
}
const message = pass
? () =>
this.utils.matcherHint(assertionName, undefined, undefined, {
isNot: this.isNot,
}) +
`\n\nLocator: ${locator}\n` +
`Expected: not ${this.utils.printExpected(expected)}\n` +
(matcherResult
? `Received: ${this.utils.printReceived(matcherResult.actual)}`
: "")
: () =>
this.utils.matcherHint(assertionName, undefined, undefined, {
isNot: this.isNot,
}) +
`\n\nLocator: ${locator}\n` +
`Expected: ${this.utils.printExpected(expected)}\n` +
(matcherResult
? `Received: ${this.utils.printReceived(matcherResult.actual)}`
: "");
return {
message,
pass,
name: assertionName,
expected,
actual: matcherResult?.actual,
};
},
});Usage:
import { test } from "@playwright/test";
import { expect } from "./fixtures";
test("custom matcher", async ({ page }) => {
await expect(page.locator(".cart")).toHaveAmount(4);
});Merge multiple custom expect extensions:
import { mergeExpects } from "@playwright/test";
import { expect as dbExpect } from "database-test-utils";
import { expect as a11yExpect } from "a11y-test-utils";
export const expect = mergeExpects(dbExpect, a11yExpect);Non-Retrying Generic Assertions
These are synchronous, do not auto-retry, and do not require await.
test("generic assertions", async ({ page }) => {
const value = await page.locator("#count").textContent();
// Equality
expect(value).toBe("5");
expect({ a: 1 }).toEqual({ a: 1 });
expect({ a: 1 }).toStrictEqual({ a: 1 });
// Truthiness
expect(value).toBeTruthy();
expect(null).toBeFalsy();
expect(value).toBeDefined();
expect(undefined).toBeUndefined();
expect(null).toBeNull();
expect(Number.NaN).toBeNaN();
// Numbers
expect(5).toBeGreaterThan(3);
expect(5).toBeGreaterThanOrEqual(5);
expect(3).toBeLessThan(5);
expect(3).toBeLessThanOrEqual(3);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
// Strings and arrays
expect("hello world").toContain("world");
expect([1, 2, 3]).toContain(2);
expect([{ a: 1 }]).toContainEqual({ a: 1 });
expect("hello").toHaveLength(5);
expect("hello").toMatch(/ell/);
// Objects
expect({ a: 1, b: 2 }).toHaveProperty("a", 1);
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
// Errors
expect(() => {
throw new Error("fail");
}).toThrow("fail");
// Instance
expect(new Date()).toBeInstanceOf(Date);
});Asymmetric Matchers
Use inside other assertions for flexible matching.
test("asymmetric matchers", async ({ page }) => {
expect({ name: "John", age: 30 }).toEqual({
name: expect.any(String),
age: expect.any(Number),
});
expect({ name: "John", id: 123 }).toEqual({
name: expect.anything(),
id: expect.anything(),
});
expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 3]));
expect({ a: 1, b: 2 }).toEqual(
expect.objectContaining({ a: 1 }),
);
expect("hello world").toEqual(expect.stringContaining("world"));
expect("hello world").toEqual(expect.stringMatching(/world/));
expect(0.1 + 0.2).toEqual(expect.closeTo(0.3, 5));
});Auto-Waiting
Playwright automatically waits for elements to pass actionability checks before performing actions. If the checks do not pass within the configured timeout, the action fails with a TimeoutError.
The Five Actionability Checks
Visible
An element is visible when it has a non-empty bounding box and does not have visibility: hidden computed style. Elements with display: none, zero size, or otherwise not rendered are not visible. Elements with opacity: 0 are considered visible.
Stable
An element is stable when it has maintained the same bounding box for at least two consecutive animation frames. This ensures animations and transitions have completed.
Receives Events
The element is the actual hit target at the action point. Another element (such as an overlay or modal backdrop) is not intercepting pointer events at the intended click location.
Enabled
The element is not disabled. Applies to form elements (<button>, <select>, <input>, <textarea>, <option>, <optgroup>) with the disabled attribute, or elements that are descendants of an element with aria-disabled="true".
Editable
The element is both enabled and not read-only. Read-only elements have the readonly attribute or aria-readonly="true" on supported ARIA roles.
Action-to-Check Matrix
Each action requires a different subset of actionability checks to pass before it executes:
| Action | Visible | Stable | Receives Events | Enabled | Editable |
|---|---|---|---|---|---|
click() | + | + | + | + | |
dblclick() | + | + | + | + | |
check() | + | + | + | + | |
uncheck() | + | + | + | + | |
setChecked() | + | + | + | + | |
tap() | + | + | + | + | |
hover() | + | + | + | ||
dragTo() | + | + | + | ||
screenshot() | + | + | |||
fill() | + | + | + | ||
clear() | + | + | + | ||
selectOption() | + | + | |||
selectText() | + | ||||
scrollIntoViewIfNeeded() | + | ||||
blur() | |||||
dispatchEvent() | |||||
focus() | |||||
press() | |||||
pressSequentially() | |||||
setInputFiles() |
Key Observations
- Click-family actions (
click,dblclick,check,uncheck,setChecked,tap) require all checks except Editable. - hover and dragTo skip the Enabled check (you can hover over disabled elements).
- fill and clear require Visible, Enabled, and Editable, but skip Stable and Receives Events.
- selectOption requires Visible and Enabled only.
- Keyboard actions (
press,pressSequentially) and dispatchEvent have no actionability checks at all. - focus and blur also have no actionability checks.
- setInputFiles has no actionability checks.
Forcing Actions
Some actions support a force option that disables non-essential actionability checks:
import { test } from "@playwright/test";
test("force click bypasses checks", async ({ page }) => {
// Skips Stable and Receives Events checks
// Still requires the element to be attached to the DOM
await page.getByRole("button").click({ force: true });
});Use force: true when you are confident the element is ready but Playwright's checks fail due to overlays, animations, or other DOM conditions. Prefer fixing the root cause over using force when possible.
Auto-Retrying Assertions
Playwright assertions also auto-retry until the condition is met:
import { test, expect } from "@playwright/test";
test("assertions auto-retry", async ({ page }) => {
// Retries until element is visible or timeout
await expect(page.locator("#status")).toBeVisible();
// Retries until text matches
await expect(page.locator("#status")).toContainText("Complete");
// Retries until enabled
await expect(page.getByRole("button")).toBeEnabled();
// Retries until URL matches
await expect(page).toHaveURL(/dashboard/);
});Frames
A Page can have one or more Frame objects attached to it. Each page has a main frame where page-level interactions operate. Additional frames are attached via <iframe> HTML tags. Playwright provides two approaches: frameLocator() for locator-based interaction and page.frame() for direct Frame object access.
frameLocator
The recommended approach for interacting with iframe content. Returns a FrameLocator that can be chained with any locator method.
import { test, expect } from "@playwright/test";
test("interact with iframe content", async ({ page }) => {
// Locate iframe by CSS selector, then find elements inside
const username = page.frameLocator(".frame-class").getByLabel("User Name");
await username.fill("John");
// Chain with any locator method
await page
.frameLocator("#payment-frame")
.getByRole("button", { name: "Pay" })
.click();
// Use assertions on frame content
await expect(
page.frameLocator("#status-frame").getByText("Success"),
).toBeVisible();
});frameLocator with Different Selectors
test("frameLocator selector variants", async ({ page }) => {
// By CSS class
page.frameLocator(".widget-frame");
// By ID
page.frameLocator("#embed-frame");
// By attribute
page.frameLocator('iframe[name="content"]');
// By title
page.frameLocator('iframe[title="Payment"]');
// By src
page.frameLocator('iframe[src*="widget"]');
// nth iframe (zero-based)
page.frameLocator("iframe").nth(0);
page.frameLocator("iframe").first();
page.frameLocator("iframe").last();
});Nested Iframes
Chain frameLocator() calls to access iframes within iframes.
test("nested iframes", async ({ page }) => {
// Outer iframe > inner iframe > element
const innerButton = page
.frameLocator("#outer-frame")
.frameLocator("#inner-frame")
.getByRole("button", { name: "Submit" });
await innerButton.click();
// Multiple levels deep
await page
.frameLocator("#level1")
.frameLocator("#level2")
.frameLocator("#level3")
.getByText("Deep content")
.isVisible();
});page.frame() -- Frame Object Access
Access a Frame object directly by name or URL. The Frame object supports the same interaction methods as Page (fill, click, locator, etc.).
By Name Attribute
test("frame by name", async ({ page }) => {
// Access frame by its name attribute: <iframe name="frame-login">
const frame = page.frame("frame-login");
if (frame) {
await frame.fill("#username-input", "John");
await frame.fill("#password-input", "secret");
await frame.click("#submit");
}
});By URL
test("frame by URL", async ({ page }) => {
// Exact URL
const frame = page.frame({ url: "https://example.com/widget" });
// URL pattern with regex
const frame2 = page.frame({ url: /.*domain.*/ });
if (frame2) {
await frame2.fill("#input", "value");
await frame2.click("button");
}
});List All Frames
test("list all frames", async ({ page }) => {
await page.goto("https://example.com");
// Get all frames including the main frame
const allFrames = page.frames();
console.log(`Total frames: ${allFrames.length}`);
for (const frame of allFrames) {
console.log(`Frame: ${frame.name()} - ${frame.url()}`);
}
});Using Locators on Frame Objects
test("frame.locator()", async ({ page }) => {
const frame = page.frame("content");
if (frame) {
// Use locator API on the frame object
await frame.locator("#search").fill("query");
await frame.locator("button.submit").click();
// Use getByRole, getByText, etc.
await frame.getByRole("button", { name: "Search" }).click();
await expect(frame.getByText("Results")).toBeVisible();
}
});frameLocator vs Frame Object
| Feature | frameLocator() | page.frame() |
|---|---|---|
| Returns | FrameLocator (locator-oriented) | Frame object (page-like API) |
| Chaining with locators | Direct: .getByRole(), etc. | Via .locator(), .getByRole() |
| Auto-waiting for iframe | Yes | No (returns null if not found) |
| Null safety | Assertions fail on timeout | Must check for null |
| Nested iframes | Chain .frameLocator() calls | Navigate via child frames |
| Best for | Most iframe interactions | When you need the Frame API |
Prefer frameLocator() for most use cases. Use page.frame() when you need to call Frame-specific APIs or need to work with the frame as an object.
Getting Started
| Name | Description | Path |
|---|---|---|
| Continuous Integration | Running Playwright tests in CI environments, using Docker images for consistent execution… | ci.md |
| Running and Debugging Tests | Overview of how to run Playwright tests from the command line, debug failures with the… | running-and-debugging.md |
| Writing Tests | Playwright tests use the @playwright/test module, which provides a test function for… | writing-tests.md |