Learn / Cheat sheet
Playwright Cheat Sheet
The Playwright Test APIs you reach for every day, on one sheet. Locators are listed in the order Playwright recommends: role, label and text before test ids, and CSS or XPath last. Every assertion here is web-first: it retries until it passes or times out, so you don’t need manual waits.
Checked against Playwright Test 1.5x, TypeScript
Run and debug
| Task | Code |
|---|---|
| New project | npm init playwright@latest |
| Run all tests | npx playwright test |
| One file, one line | npx playwright test tests/login.spec.ts:12 |
| By title or tag | npx playwright test --grep @smoke |
| One browser project | npx playwright test --project=chromium |
| See the browser | npx playwright test --headed |
| Step through with the inspector | npx playwright test --debug |
| UI mode (watch, time travel) | npx playwright test --ui |
| Only last run’s failures | npx playwright test --last-failed |
| Hunt a flaky test | npx playwright test my.spec.ts --repeat-each=20 |
| Shard across CI machines | npx playwright test --shard=1/4 |
| Open the HTML report | npx playwright show-report |
| Open a trace | npx playwright show-trace trace.zip |
| Record a script | npx playwright codegen https://example.com |
Locators (preferred first)
| Task | Code |
|---|---|
| By role and accessible name | page.getByRole('button', { name: 'Sign in' }) |
| Form field by its label | page.getByLabel('Email') |
| By placeholder | page.getByPlaceholder('Search') |
| By text (exact) | page.getByText('Welcome back', { exact: true }) |
| By test id (data-testid) | page.getByTestId('checkout-submit') |
| CSS | page.locator('#orders tbody tr') |
| XPath | page.locator('xpath=//table[@id="orders"]//tr') |
| Filter by text | page.getByRole('row').filter({ hasText: 'Mei Lin' }) |
| Filter by child | page.getByRole('listitem').filter({ has: page.getByRole('button', { name: 'Buy' }) }) |
| Chain into a match | row.getByRole('button', { name: 'Cancel' }) |
| Pick one of many | .first() .last() .nth(2) |
| Inside an iframe | page.frameLocator('#payment').getByLabel('Card number') |
Locators are lazy: nothing is looked up until you act or assert, so define them once and reuse them.
Actions
| Task | Code |
|---|---|
| Navigate (relative to baseURL) | await page.goto('/login') |
| Click | await locator.click() |
| Replace the value of an input | await locator.fill('mei@example.com') |
| Type key by key | await locator.pressSequentially('abc', { delay: 50 }) |
| Press a key | await locator.press('Enter') |
| Checkbox or radio | await locator.check() / await locator.uncheck() |
| Select an option | await page.getByLabel('Country').selectOption('UK') |
| Upload a file | await locator.setInputFiles('fixtures/invoice.pdf') |
| Hover | await locator.hover() |
| Drag and drop | await source.dragTo(target) |
| Accept the next dialog | page.once('dialog', (d) => d.accept()) |
Web-first assertions
| Task | Code |
|---|---|
| Visible / hidden | await expect(locator).toBeVisible() / .toBeHidden() |
| Exact text | await expect(locator).toHaveText('Order placed') |
| Text contains | await expect(locator).toContainText('placed') |
| Input value | await expect(input).toHaveValue('42') |
| Number of matches | await expect(page.getByRole('row')).toHaveCount(5) |
| Enabled / disabled / checked | toBeEnabled() toBeDisabled() toBeChecked() |
| Attribute | await expect(menu).toHaveAttribute('aria-expanded', 'true') |
| URL and title | await expect(page).toHaveURL(/dashboard/) / .toHaveTitle('Orders') |
| Negate | await expect(spinner).not.toBeVisible() |
| Keep going after a failure | await expect.soft(total).toHaveText('£40.00') |
| Retry any async check | await expect.poll(async () => (await api.get(url)).status()).toBe(200) |
| Visual comparison | await expect(page).toHaveScreenshot('home.png') |
Network and API
| Task | Code |
|---|---|
| Mock a response | await page.route('**/api/orders', (r) => r.fulfill({ json: [] })) |
| Fail a request | await page.route('**/api/orders', (r) => r.abort()) |
| Change a real response | await page.route('**/api/me', async (r) => { const res = await r.fetch(); const json = await res.json(); await r.fulfill({ response: res, json: { ...json, plan: 'pro' } }); }) |
| Wait for a response | const res = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: 'Save' }).click();
expect((await res).ok()).toBeTruthy(); |
| API test | test('list users', async ({ request }) => {
const res = await request.get('/api/users');
await expect(res).toBeOK();
expect(await res.json()).toHaveLength(3);
}); |
Structure, fixtures and hooks
| Task | Code |
|---|---|
| Group tests | test.describe('checkout', () => { /* tests */ }) |
| Run before each test | test.beforeEach(async ({ page }) => { await page.goto('/'); }) |
| Tag a test | test('pays by card', { tag: '@smoke' }, async ({ page }) => { /* ... */ }) |
| Skip conditionally | test.skip(browserName === 'webkit', 'Not supported on WebKit') |
| Parallel within a file | test.describe.configure({ mode: 'parallel' }) |
| Custom fixture | export const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
}); |
| Reuse a signed-in state | test.use({ storageState: 'playwright/.auth/user.json' }) |
| Name a step in the report | await test.step('add to cart', async () => { /* ... */ }) |
playwright.config.ts
| Setting | Example |
|---|---|
| Base URL for page.goto | use: { baseURL: 'http://localhost:3000' } |
| Evidence on failure | use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure' } |
| Retries only in CI | retries: process.env.CI ? 2 : 0 |
| Parallelism | fullyParallel: true, workers: process.env.CI ? 2 : undefined |
| Fail if test.only is committed | forbidOnly: !!process.env.CI |
| Start the app first | webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI } |
| Browsers | projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }] |
| Default timeouts | timeout: 30_000, expect: { timeout: 5_000 } |