Playwright Cheatsheet

Fixtures

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

Built-in fixtures

These are automatically available in every test via the callback argument.

Test-scope fixtures (new instance per test)

FixtureTypeDescription
pagePageIsolated browser page
contextBrowserContextIsolated browser context (shares cookies/storage within a test)
requestAPIRequestContextPlaywright API testing client

Worker-scope fixtures (shared across tests in a worker)

FixtureTypeDescription
browserBrowserBrowser instance
browserNamestring'chromium' | 'firefox' | 'webkit'

Every option under use is also exposed as a test-scoped option fixture — e.g. isMobile, viewport, baseURL:

test('example', async ({ page, browserName, isMobile }) => {
  test.skip(isMobile, 'Desktop only');
  console.log(browserName); // 'chromium'
});

Defining custom fixtures

// fixtures.ts
import { test as base, expect } from '@playwright/test';

type MyFixtures = {
  loggedInPage: Page;
  apiToken: string;
};

export const test = base.extend<MyFixtures>({
  // Page-scope fixture (default)
  loggedInPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');

    await use(page);  // <-- hand control to the test

    // teardown runs after the test
    await page.goto('/logout');
  },

  // Worker-scope fixture
  apiToken: [async ({}, use) => {
    const token = await fetchToken();
    await use(token);
    await revokeToken(token);
  }, { scope: 'worker' }],
});

export { expect };
// my.spec.ts — import from fixtures file, not @playwright/test
import { test, expect } from './fixtures';

test('dashboard loads', async ({ loggedInPage }) => {
  await expect(loggedInPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Fixture options (parameterization)

type Options = { defaultSearchTerm: string };

export const test = base.extend<Options>({
  defaultSearchTerm: ['playwright', { option: true }],  // mark as option
});

// Override in config
export default defineConfig({
  use: { defaultSearchTerm: 'testing' },
});

// Or per-project
projects: [
  { name: 'search-a', use: { defaultSearchTerm: 'alpha' } },
  { name: 'search-b', use: { defaultSearchTerm: 'beta' } },
],

Fixture scopes

ScopeLifetimeUse for
'test' (default)One testPage, auth tokens per test
'worker'One worker processDB connections, browser instance
heavyResource: [async ({}, use) => {
  const resource = await createExpensiveResource();
  await use(resource);
  await resource.cleanup();
}, { scope: 'worker' }],

auto fixtures

Run for every test without needing to list them in the argument:

export const test = base.extend({
  autoSetup: [async ({ page }, use) => {
    // runs before every test automatically
    await page.addInitScript(() => {
      window.__E2E__ = true;
    });
    await use();
  }, { auto: true }],
});

Fixture composition

type AppFixtures = {
  authenticatedPage: Page;
  adminPage: Page;
};

export const test = base.extend<AppFixtures>({
  authenticatedPage: async ({ page }, use) => {
    await loginAs(page, 'user@example.com');
    await use(page);
  },
  adminPage: async ({ page }, use) => {
    await loginAs(page, 'admin@example.com');
    await use(page);
  },
});

Page Object Model with fixtures

// page-objects/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Sign in' }).click();
  }
}

// fixtures.ts
import { LoginPage } from './page-objects/LoginPage';

type POMs = { loginPage: LoginPage };

export const test = base.extend<POMs>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

// spec.ts
test('login', async ({ loginPage }) => {
  await loginPage.login('user@example.com', 'secret');
});

test.use() — fixture overrides per describe

test.describe('mobile', () => {
  test.use({ viewport: { width: 390, height: 844 } });

  test('mobile nav', async ({ page }) => {
    // page uses 390×844
  });
});

Sharing state between tests (serial + worker fixtures)

test.describe.serial('checkout flow', () => {
  let orderId: string;

  test('add to cart', async ({ page }) => {
    await page.getByRole('button', { name: 'Add to cart' }).click();
    orderId = await page.locator('#order-id').textContent() ?? '';
  });

  test('checkout', async ({ page }) => {
    await page.goto(`/orders/${orderId}`);
  });
});

Prefer isolated tests over shared state — serial suites are fragile. Use fixtures + storage state (see Navigation) for shared auth instead.

storageState pattern (auth)

Preferred: a setup project that other projects depend on — it gets fixtures, tracing, and HTML-report visibility:

// auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts
projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

See the dependencies example in Configuration. The legacy alternative — a globalSetup script that saves storageState.json and sets use.storageState globally — still works, but runs outside the test runner (no fixtures, no trace).