Documentation/Buki/Playwright/ skills /playwright-fixtures-and-hooks

📖 playwright-fixtures-and-hooks

Use when managing test state and infrastructure with reusable Playwright fixtures and lifecycle hooks for efficient test setup and teardown.



Overview

Master Playwright's fixture system and lifecycle hooks to create reusable test infrastructure, manage test state, and build maintainable test suites. This skill covers built-in fixtures, custom fixtures, and best practices for test setup and teardown.

Built-in Fixtures

Core Fixtures

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

test('using built-in fixtures', async ({
  page,      // Page instance
  context,   // Browser context
  browser,   // Browser instance
  request,   // API request context
}) => {
  // Each test gets fresh page and context
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

Page Fixture

test('page fixture examples', async ({ page }) => {
  // Navigate
  await page.goto('https://example.com');

  // Interact
  await page.getByRole('button', { name: 'Click me' }).click();

  // Wait
  await page.waitForLoadState('networkidle');

  // Evaluate
  const title = await page.title();
  expect(title).toBe('Example Domain');
});

Context Fixture

test('context fixture examples', async ({ context, page }) => {
  // Add cookies
  await context.addCookies([
    {
      name: 'session',
      value: 'abc123',
      domain: 'example.com',
      path: '/',
    },
  ]);

  // Set permissions
  await context.grantPermissions(['geolocation']);

  // Create additional page in same context
  const page2 = await context.newPage();
  await page2.goto('https://example.com');

  // Both pages share cookies and storage
  await page.goto('https://example.com');
});

Browser Fixture

test('browser fixture examples', async ({ browser }) => {
  // Create custom context with options
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    locale: 'en-US',
    timezoneId: 'America/New_York',
    permissions: ['geolocation'],
  });

  const page = await context.newPage();
  await page.goto('https://example.com');

  await context.close();
});

Request Fixture

test('API testing with request fixture', async ({ request }) => {
  // Make GET request
  const response = await request.get('https://api.example.com/users');
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  const users = await response.json();
  expect(users).toHaveLength(10);

  // Make POST request
  const createResponse = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com',
    },
  });
  expect(createResponse.ok()).toBeTruthy();
});

Custom Fixtures

Basic Custom Fixture

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

type MyFixtures = {
  timestamp: string;
};

export const test = base.extend<MyFixtures>({
  timestamp: async ({}, use) => {
    const timestamp = new Date().toISOString();
    await use(timestamp);
  },
});

export { expect } from '@playwright/test';
// tests/example.spec.ts
import { test, expect } from '../fixtures/base-fixtures';

test('using custom timestamp fixture', async ({ timestamp, page }) => {
  console.log(`Test started at: ${timestamp}`);
  await page.goto('https://example.com');
});

Fixture with Setup and Teardown

import { test as base } from '@playwright/test';

type DatabaseFixtures = {
  database: Database;
};

export const test = base.extend<DatabaseFixtures>({
  database: async ({}, use) => {
    // Setup: Create database connection
    const db = await createDatabaseConnection();
    console.log('Database connected');

    // Provide fixture to test
    await use(db);

    // Teardown: Close database connection
    await db.close();
    console.log('Database closed');
  },
});

Fixture Scopes: Test vs Worker

import { test as base } from '@playwright/test';

type TestScopedFixtures = {
  uniqueId: string;
};

type WorkerScopedFixtures = {
  apiToken: string;
};

export const test = base.extend<TestScopedFixtures, WorkerScopedFixtures>({
  // Test-scoped: Created for each test
  uniqueId: async ({}, use) => {
    const id = `test-${Date.now()}-${Math.random()}`;
    await use(id);
  },

  // Worker-scoped: Created once per worker
  apiToken: [
    async ({}, use) => {
      const token = await generateApiToken();
      await use(token);
      await revokeApiToken(token);
    },
    { scope: 'worker' },
  ],
});

Authentication Fixtures

Authenticated User Fixture

// fixtures/auth-fixtures.ts
import { test as base } from '@playwright/test';

type AuthFixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ browser }, use) => {
    // Create new context with authentication
    const context = await browser.newContext({
      storageState: 'auth.json',
    });

    const page = await context.newPage();
    await use(page);

    await context.close();
  },
});

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

Multiple User Roles

// fixtures/multi-user-fixtures.ts
import { test as base } from '@playwright/test';

type UserFixtures = {
  adminPage: Page;
  userPage: Page;
  guestPage: Page;
};

export const test = base.extend<UserFixtures>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'auth/admin.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  userPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'auth/user.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  guestPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

Authentication Setup

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

setup('authenticate as admin', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByLabel('Password').fill('admin123');
  await page.getByRole('button', { name: 'Login' }).click();

  await page.waitForURL('**/dashboard');

  await page.context().storageState({ path: 'auth/admin.json' });
});

setup('authenticate as user', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('user123');
  await page.getByRole('button', { name: 'Login' }).click();

  await page.waitForURL('**/dashboard');

  await page.context().storageState({ path: 'auth/user.json' });
});

Database Fixtures

Test Database Fixture

// fixtures/database-fixtures.ts
import { test as base } from '@playwright/test';
import { PrismaClient } from '@prisma/client';

type DatabaseFixtures = {
  db: PrismaClient;
  cleanDb: void;
};

export const test = base.extend<DatabaseFixtures>({
  db: [
    async ({}, use) => {
      const db = new PrismaClient();
      await use(db);
      await db.$disconnect();
    },
    { scope: 'worker' },
  ],

  cleanDb: async ({ db }, use) => {
    // Clean database before test
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();

    await use();

    // Clean database after test
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();
  },
});

Seeded Data Fixture

// fixtures/seed-fixtures.ts
import { test as base } from './database-fixtures';

type SeedFixtures = {
  testUser: User;
  testProducts: Product[];
};

export const test = base.extend<SeedFixtures>({
  testUser: async ({ db, cleanDb }, use) => {
    const user = await db.user.create({
      data: {
        email: 'test@example.com',
        name: 'Test User',
        password: 'hashedpassword',
      },
    });

    await use(user);
  },

  testProducts: async ({ db, cleanDb }, use) => {
    const products = await db.product.createMany({
      data: [
        { name: 'Product 1', price: 10.99 },
        { name: 'Product 2', price: 20.99 },
        { name: 'Product 3', price: 30.99 },
      ],
    });

    const allProducts = await db.product.findMany();
    await use(allProducts);
  },
});

API Mocking Fixtures

Mock API Fixture

// fixtures/mock-api-fixtures.ts
import { test as base } from '@playwright/test';

type MockApiFixtures = {
  mockApi: void;
};

export const test = base.extend<MockApiFixtures>({
  mockApi: async ({ page }, use) => {
    // Mock API responses
    await page.route('**/api/users', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'User 1' },
          { id: 2, name: 'User 2' },
        ]),
      });
    });

    await page.route('**/api/products', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Product 1', price: 10 },
          { id: 2, name: 'Product 2', price: 20 },
        ]),
      });
    });

    await use();

    // Cleanup: Unroute all
    await page.unrouteAll();
  },
});

Conditional Mocking

// fixtures/conditional-mock-fixtures.ts
import { test as base } from '@playwright/test';

type ConditionalMockFixtures = {
  mockFailedApi: void;
  mockSlowApi: void;
};

export const test = base.extend<ConditionalMockFixtures>({
  mockFailedApi: async ({ page }, use) => {
    await page.route('**/api/**', async (route) => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Internal Server Error' }),
      });
    });

    await use();
    await page.unrouteAll();
  },

  mockSlowApi: async ({ page }, use) => {
    await page.route('**/api/**', async (route) => {
      // Simulate slow network
      await new Promise((resolve) => setTimeout(resolve, 3000));
      await route.continue();
    });

    await use();
    await page.unrouteAll();
  },
});

Lifecycle Hooks

Test Hooks

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

test.describe('User Management', () => {
  test.beforeAll(async () => {
    // Runs once before all tests in this describe block
    console.log('Setting up test suite');
  });

  test.beforeEach(async ({ page }) => {
    // Runs before each test
    await page.goto('https://example.com');
    console.log('Test starting');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // Runs after each test
    console.log(`Test ${testInfo.status}: ${testInfo.title}`);

    if (testInfo.status !== testInfo.expectedStatus) {
      // Test failed - capture additional debug info
      const screenshot = await page.screenshot();
      await testInfo.attach('failure-screenshot', {
        body: screenshot,
        contentType: 'image/png',
      });
    }
  });

  test.afterAll(async () => {
    // Runs once after all tests in this describe block
    console.log('Cleaning up test suite');
  });

  test('test 1', async ({ page }) => {
    // Test implementation
  });

  test('test 2', async ({ page }) => {
    // Test implementation
  });
});

Nested Hooks

test.describe('Parent Suite', () => {
  test.beforeEach(async ({ page }) => {
    console.log('Parent beforeEach');
    await page.goto('https://example.com');
  });

  test.describe('Child Suite 1', () => {
    test.beforeEach(async ({ page }) => {
      console.log('Child 1 beforeEach');
      await page.getByRole('link', { name: 'Products' }).click();
    });

    test('test in child 1', async ({ page }) => {
      // Parent beforeEach runs first, then child beforeEach
    });
  });

  test.describe('Child Suite 2', () => {
    test.beforeEach(async ({ page }) => {
      console.log('Child 2 beforeEach');
      await page.getByRole('link', { name: 'About' }).click();
    });

    test('test in child 2', async ({ page }) => {
      // Parent beforeEach runs first, then child beforeEach
    });
  });
});

Conditional Hooks

test.describe('Feature Tests', () => {
  test.beforeEach(async ({ page, browserName }) => {
    // Skip setup for Firefox
    if (browserName === 'firefox') {
      test.skip();
    }

    await page.goto('https://example.com');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // Only run teardown for failed tests
    if (testInfo.status === 'failed') {
      await page.screenshot({ path: `failure-${testInfo.title}.png` });
    }
  });

  test('feature test', async ({ page }) => {
    // Test implementation
  });
});

Fixture Dependencies

Dependent Fixtures

// fixtures/dependent-fixtures.ts
import { test as base } from '@playwright/test';

type DependentFixtures = {
  config: Config;
  apiClient: ApiClient;
  authenticatedClient: ApiClient;
};

export const test = base.extend<DependentFixtures>({
  // Base fixture
  config: async ({}, use) => {
    const config = {
      apiUrl: process.env.API_URL || 'http://localhost:3000',
      timeout: 30000,
    };
    await use(config);
  },

  // Depends on config
  apiClient: async ({ config }, use) => {
    const client = new ApiClient(config.apiUrl, config.timeout);
    await use(client);
  },

  // Depends on apiClient
  authenticatedClient: async ({ apiClient }, use) => {
    const token = await apiClient.login('user@example.com', 'password');
    apiClient.setAuthToken(token);
    await use(apiClient);
  },
});

Combining Multiple Fixtures

// fixtures/combined-fixtures.ts
import { test as base } from '@playwright/test';

type CombinedFixtures = {
  setupComplete: void;
};

export const test = base.extend<CombinedFixtures>({
  setupComplete: async (
    { page, db, mockApi, testUser },
    use
  ) => {
    // All dependent fixtures are initialized
    await page.goto('https://example.com');
    await page.context().addCookies([
      {
        name: 'userId',
        value: testUser.id.toString(),
        domain: 'example.com',
        path: '/',
      },
    ]);

    await use();
  },
});

Advanced Fixture Patterns

Factory Fixtures

// fixtures/factory-fixtures.ts
import { test as base } from '@playwright/test';

type FactoryFixtures = {
  createUser: (data: Partial<User>) => Promise<User>;
  createProduct: (data: Partial<Product>) => Promise<Product>;
};

export const test = base.extend<FactoryFixtures>({
  createUser: async ({ db }, use) => {
    const users: User[] = [];

    const createUser = async (data: Partial<User>) => {
      const user = await db.user.create({
        data: {
          email: data.email || `user-${Date.now()}@example.com`,
          name: data.name || 'Test User',
          password: data.password || 'password123',
          ...data,
        },
      });
      users.push(user);
      return user;
    };

    await use(createUser);

    // Cleanup: Delete all created users
    for (const user of users) {
      await db.user.delete({ where: { id: user.id } });
    }
  },

  createProduct: async ({ db }, use) => {
    const products: Product[] = [];

    const createProduct = async (data: Partial<Product>) => {
      const product = await db.product.create({
        data: {
          name: data.name || `Product ${Date.now()}`,
          price: data.price || 9.99,
          description: data.description || 'Test product',
          ...data,
        },
      });
      products.push(product);
      return product;
    };

    await use(createProduct);

    // Cleanup: Delete all created products
    for (const product of products) {
      await db.product.delete({ where: { id: product.id } });
    }
  },
});

Option Fixtures

// fixtures/option-fixtures.ts
import { test as base } from '@playwright/test';

type OptionsFixtures = {
  slowNetwork: boolean;
};

export const test = base.extend<OptionsFixtures>({
  slowNetwork: [false, { option: true }],

  page: async ({ page, slowNetwork }, use) => {
    if (slowNetwork) {
      await page.route('**/*', async (route) => {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        await route.continue();
      });
    }

    await use(page);
  },
});
// tests/slow-network.spec.ts
import { test, expect } from '../fixtures/option-fixtures';

test('test with slow network', async ({ page }) => {
  test.use({ slowNetwork: true });

  await page.goto('https://example.com');
  // This will be slow due to network throttling
});

test('test with normal network', async ({ page }) => {
  await page.goto('https://example.com');
  // Normal speed
});

Test Info and Attachments

Using Test Info

test('example with test info', async ({ page }, testInfo) => {
  console.log(`Test title: ${testInfo.title}`);
  console.log(`Project: ${testInfo.project.name}`);
  console.log(`Retry: ${testInfo.retry}`);

  await page.goto('https://example.com');

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

  // Attach JSON data
  await testInfo.attach('test-data', {
    body: JSON.stringify({ foo: 'bar' }),
    contentType: 'application/json',
  });

  // Attach text
  await testInfo.attach('notes', {
    body: 'Test completed successfully',
    contentType: 'text/plain',
  });
});

Conditional Test Execution

test('browser-specific test', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Not supported in Safari');

  await page.goto('https://example.com');
  // Test only runs in Chromium and Firefox
});

test('slow test', async ({ page }) => {
  test.slow(); // Triple timeout for this test

  await page.goto('https://slow-site.example.com');
  // Long-running operations
});

test('expected to fail', async ({ page }) => {
  test.fail(); // Mark as expected failure

  await page.goto('https://example.com');
  await expect(page.getByText('Non-existent')).toBeVisible();
});

Fixture Best Practices

Organizing Fixtures

fixtures/
├── index.ts              # Export all fixtures
├── auth-fixtures.ts      # Authentication fixtures
├── database-fixtures.ts  # Database fixtures
├── mock-api-fixtures.ts  # API mocking fixtures
└── page-fixtures.ts      # Page-related fixtures
// fixtures/index.ts
import { test as authTest } from './auth-fixtures';
import { test as dbTest } from './database-fixtures';
import { test as mockTest } from './mock-api-fixtures';

export const test = authTest.extend(dbTest.fixtures).extend(mockTest.fixtures);

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

Fixture Naming Conventions

// Good naming
export const test = base.extend({
  authenticatedPage: async ({}, use) => { /* ... */ },
  testUser: async ({}, use) => { /* ... */ },
  mockApi: async ({}, use) => { /* ... */ },
});

// Avoid
export const test = base.extend({
  page2: async ({}, use) => { /* ... */ },  // Not descriptive
  data: async ({}, use) => { /* ... */ },   // Too generic
  fixture1: async ({}, use) => { /* ... */ }, // Meaningless name
});

When to Use This Skill

  • Setting up reusable test infrastructure
  • Managing authentication state across tests
  • Creating database seeding and cleanup logic
  • Implementing API mocking for tests
  • Building factory fixtures for test data generation
  • Establishing test lifecycle patterns
  • Creating worker-scoped fixtures for performance
  • Organizing complex test setup and teardown
  • Implementing conditional test behavior
  • Building type-safe fixture systems

Resources