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)
| Fixture | Type | Description |
|---|---|---|
page | Page | Isolated browser page |
context | BrowserContext | Isolated browser context (shares cookies/storage within a test) |
request | APIRequestContext | Playwright API testing client |
Worker-scope fixtures (shared across tests in a worker)
| Fixture | Type | Description |
|---|---|---|
browser | Browser | Browser instance |
browserName | string | '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
| Scope | Lifetime | Use for |
|---|---|---|
'test' (default) | One test | Page, auth tokens per test |
'worker' | One worker process | DB 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 }); });
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).