Playwright Cheatsheet

Writing Tests

Use this Playwright reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Basic test structure

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

test('page title is correct', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle('Example Domain');
});

test() overloads

test('name', async ({ page }) => { /* ... */ });
test('name', { tag: '@smoke' }, async ({ page }) => { /* ... */ });
test.only('name', async ({ page }) => { /* ... */ }); // focus — run only this
test.skip('name', async ({ page }) => { /* ... */ }); // skip unconditionally
test.fixme('name', async ({ page }) => { /* ... */ });// known broken, do not fail CI
test.fail('name', async ({ page }) => { /* ... */ }); // expected to fail

Conditional skip / fixme at runtime

test('mobile only', async ({ page, isMobile }) => {
  test.skip(!isMobile, 'Desktop not relevant');
  // ...
});

test('buggy feature', async ({ page }) => {
  test.fixme(process.env.CI === 'true', 'Flaky on CI, see #123');
});

Slow tests

test('heavy upload', async ({ page }) => {
  test.slow();          // triples the timeout for this test only
  // ...
});

Grouping with describe

test.describe('auth flow', () => {
  test('login', async ({ page }) => { /* ... */ });
  test('logout', async ({ page }) => { /* ... */ });
});

test.describe.serial('ordered suite', () => {
  // tests run in sequence, next test skipped if previous fails
});

test.describe.only('focused group', () => { /* ... */ });
test.describe.skip('skip group', () => { /* ... */ });

Hooks

test.beforeAll(async ({ browser }) => {
  // runs once per describe block — note fixture is browser, not page
});

test.afterAll(async () => { /* cleanup */ });

test.beforeEach(async ({ page }) => {
  await page.goto('/');
});

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus) {
    await page.screenshot({ path: `failure-${testInfo.title}.png` });
  }
});

beforeAll/afterAll in describe.serial share the same worker; elsewhere each runs in its own worker.

TestInfo object

Available as the second argument to test and hooks:

test('example', async ({ page }, testInfo) => {
  testInfo.title;          // 'example'
  testInfo.titlePath;      // ['describe', 'example']
  testInfo.file;           // absolute path to spec file
  testInfo.outputDir;      // per-test output directory
  testInfo.snapshotDir;    // snapshot directory
  testInfo.retry;          // 0 = first attempt
  testInfo.annotations;    // [{type, description}]
  testInfo.attachments;    // add via testInfo.attach()

  await testInfo.attach('screenshot', {
    body: await page.screenshot(),
    contentType: 'image/png',
  });
});

Annotations and tags

test('login @smoke @auth', async ({ page }) => { /* ... */ });

// Filter by tag at CLI:
//   npx playwright test --grep @smoke
//   npx playwright test --grep-invert @slow

test('known bug', async ({ page }) => {
  test.info().annotations.push({ type: 'issue', description: 'https://github.com/org/repo/issues/42' });
});

Parameterized tests

const locales = ['en', 'de', 'fr'];
for (const locale of locales) {
  test(`homepage renders in ${locale}`, async ({ page }) => {
    await page.goto(`/${locale}`);
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
  });
}

// Or with a data table:
const CREDENTIALS = [
  { user: 'admin', pass: 'secret' },
  { user: 'guest', pass: 'guest' },
];
for (const { user, pass } of CREDENTIALS) {
  test(`login as ${user}`, async ({ page }) => {
    await page.getByLabel('Username').fill(user);
    await page.getByLabel('Password').fill(pass);
  });
}

Cross-browser coverage is NOT a loop — the page fixture's browser is fixed per project. Define projects in playwright.config.ts instead (see Configuration).

Timeouts

ScopeConfig keyDefault
Per-testtimeout30 000 ms
Per-expect assertionexpect.timeout5 000 ms
Global (all tests)globalTimeoutnone
test('slow test', async ({ page }) => {
  test.setTimeout(60_000);   // override for this test only
  // ...
});

Retries

// playwright.config.ts
export default defineConfig({ retries: 2 });

There is no per-test retries option — configure per file or per describe block:

test.describe(() => {
  test.describe.configure({ retries: 3 });

  test('flaky', async ({ page }) => { /* ... */ });
});
npx playwright test --retries=3   # override from the CLI

Expect soft assertions

// Soft: continue on failure, report all at end
await expect.soft(page.locator('h1')).toHaveText('Home');
await expect.soft(page.locator('h2')).toHaveText('Welcome');
// all soft failures collected and surfaced together

expect.poll / expect.toPass

// Poll a custom predicate until it passes
await expect.poll(async () => {
  const res = await page.evaluate(() => window.appReady);
  return res;
}, { timeout: 10_000 }).toBe(true);

// Retry an entire block
await expect(async () => {
  const count = await page.locator('.item').count();
  expect(count).toBeGreaterThan(3);
}).toPass({ timeout: 15_000 });