</>

Technology

Playwright

Difficulty

Advanced

Interview Question

How do you extend and compose Playwright fixtures?

Answer

Extending and Composing Playwright Fixtures

Fixtures are the recommended way to share setup code in Playwright. They compose cleanly — a fixture can depend on other fixtures.

Basic Extension Pattern

TypeScript
// fixtures/pages.ts
import { test as base } from '@playwright/test';
import { LoginPage }    from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
import { ProductPage }  from '../pages/ProductPage';
import { CartPage }     from '../pages/CartPage';

// Declare fixture types
type PageFixtures = {
  loginPage:     LoginPage;
  dashboardPage: DashboardPage;
  productPage:   ProductPage;
  cartPage:      CartPage;
};

// Extend base test
export const test = base.extend<PageFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
  productPage: async ({ page }, use) => {
    await use(new ProductPage(page));
  },
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },
});

export { expect } from '@playwright/test';

Fixture Depending on Another Fixture

TypeScript
type AuthFixtures = {
  loggedInPage: DashboardPage; // Already authenticated
  adminPage:    DashboardPage; // Authenticated as admin
};

export const test = base.extend<AuthFixtures>({
  // This fixture depends on the loginPage fixture
  loggedInPage: async ({ page }, use) => {
    // Perform login directly
    await page.goto('/login');
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
    await page.getByLabel('Password').fill(process.env.TEST_PASS!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await page.waitForURL('/dashboard');

    await use(new DashboardPage(page));

    // Teardown — logout after test
    await page.getByRole('button', { name: 'Logout' }).click();
  },
});

Layered Fixture Composition

TypeScript
// Layer 1: Core fixtures
const testWithPages = base.extend<PageFixtures>({
  loginPage: async ({ page }, use) => await use(new LoginPage(page)),
});

// Layer 2: Auth on top of pages
const testWithAuth = testWithPages.extend<AuthFixtures>({
  loggedInPage: async ({ loginPage, page }, use) => {
    await loginPage.navigate();
    await loginPage.login('user@test.com', 'pass');
    await use(new DashboardPage(page));
  },
});

// Layer 3: Feature-specific on top of auth
export const test = testWithAuth.extend<FeatureFixtures>({
  productPage: async ({ page }, use) => await use(new ProductPage(page)),
});

Worker-Scoped Fixtures (Shared Across Tests)

TypeScript
type WorkerFixtures = {
  adminToken: string;  // Fetched once per worker
};

export const test = base.extend<{}, WorkerFixtures>({
  adminToken: [async ({}, use) => {
    // Login via API once per worker — NOT per test
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email: 'admin@test.com', password: 'admin' }),
      headers: { 'Content-Type': 'application/json' },
    });
    const { token } = await res.json();

    await use(token); // Token shared with all tests in this worker

    // Teardown: invalidate token
    await fetch('/api/auth/logout', {
      headers: { Authorization: `Bearer ${token}` },
    });
  }, { scope: 'worker' }],
});

Using Composed Fixtures in Tests

TypeScript
// tests/checkout.spec.ts
import { test, expect } from '../fixtures/pages';

test('checkout flow', async ({ loggedInPage, productPage, cartPage }) => {
  // Already logged in via loggedInPage fixture
  await productPage.navigateTo('laptop');
  await productPage.addToCart();

  await cartPage.navigate();
  await expect(cartPage.itemCount).toHaveText('1');
  await cartPage.checkout();
});

Follow AutomateQA

Related Topics