IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Intermediate·4 / 27

Fixtures

Fixtures are the better alternative to beforeEach/afterEach. They're composable, on-demand, and automatically cleaned up. Once you understand them, you won't go back to setup hooks.

What fixtures are

use() is the yield point — setup before, teardown after

A fixture is a function that prepares something for a test and cleans it up after. The split is at await use(value) — everything before is setup, everything after is teardown. When you request a fixture in a test's argument list, Playwright runs it automatically and passes the value.

Built-in fixtures you already use: page, context, browser, request. They all work this way — Playwright creates them before the test and cleans up after.

Why fixtures beat beforeEach

With beforeEach, setup and teardown are in separate blocks that have to share state through outer variables. The test has no way to know what was set up without reading all the hooks. Fixtures solve this: setup, value, and teardown are all in one place, and the test explicitly declares what it needs in its argument list.

Another advantage: fixtures are on-demand. If a test doesn't need loggedInPage, Playwright doesn't create it. With beforeEach, it runs for every test whether or not the test needs what it sets up.

ts
// ❌ З beforeEach — setup розкиданий по хуках
let ordersPage: OrdersPage

test.beforeEach(async ({ page }) => {
  ordersPage = new OrdersPage(page)
  await ordersPage.goto()
  await ordersPage.seedTestOrders()
})

test.afterEach(async () => {
  await ordersPage.cleanup()
})

test('filter by status', async () => {
  await ordersPage.filterByStatus('pending')
  // ...
})

// ✅ З fixtures — все разом, явно
const test = base.extend<{ ordersPage: OrdersPage }>({
  ordersPage: async ({ page }, use) => {
    const ordersPage = new OrdersPage(page)
    await ordersPage.goto()
    await ordersPage.seedTestOrders()
    await use(ordersPage)         // <-- тут тест запускається
    await ordersPage.cleanup()
  },
})

test('filter by status', async ({ ordersPage }) => {
  await ordersPage.filterByStatus('pending')
  // ...
})

Create custom fixtures

Use test.extend() to add fixtures. Define types for your fixtures, then implement each one. Export the extended test and expect — your test files import from this instead of @playwright/test.

ts
// fixtures/index.ts
import { test as base, expect } from '@playwright/test'
import { OrdersPage } from '../pages/orders-page'
import { LoginPage } from '../pages/login-page'

type Fixtures = {
  ordersPage: OrdersPage
  loggedInPage: Page
}

export const test = base.extend<Fixtures>({
  // Фікстура що залежить від page
  ordersPage: async ({ page }, use) => {
    const orders = new OrdersPage(page)
    await orders.goto()
    await use(orders)
    // Нічого не прибираємо — кожен тест отримує свій ізольований page
  },

  // Фікстура що залежить від іншої фікстури
  loggedInPage: async ({ page }, use) => {
    await page.goto('/login')
    await page.getByLabel('Email').fill('admin@example.com')
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
    await page.getByRole('button', { name: 'Sign in' }).click()
    await page.waitForURL('/dashboard')
    await use(page)
    // page прибирається автоматично вбудованою фікстурою
  },
})

export { expect }
ts
// tests/orders.spec.ts
import { test, expect } from '../fixtures'

// Тест явно оголошує що йому потрібно
test('create order', async ({ ordersPage }) => {
  await ordersPage.createOrder({ item: 'Laptop', quantity: 1 })
  await expect(ordersPage.orderList).toContainText('Laptop')
})

// Можна запитувати кілька фікстур одночасно
test('admin views all orders', async ({ loggedInPage, ordersPage }) => {
  await expect(ordersPage.orderList).toBeVisible()
})

Override built-in fixtures

You can override the built-in page fixture to add behavior that applies to every test — like auto-navigating to the app's base URL, or listening for console errors.

ts
// Перевизначити page — auto-navigate і збирати console errors
export const test = base.extend({
  page: async ({ baseURL, page }, use) => {
    const consoleErrors: string[] = []
    page.on('console', msg => {
      if (msg.type() === 'error') consoleErrors.push(msg.text())
    })

    await page.goto(baseURL!)
    await use(page)

    // Після тесту — перевірити що не було JS помилок
    if (consoleErrors.length > 0) {
      console.warn('Console errors during test:', consoleErrors)
    }
  },
})

Worker-scoped fixtures — share between tests

Worker-scoped fixtures are created once per worker process and shared across all tests in that worker — ideal for expensive DB connections or login sessions

By default, fixtures are test-scoped — created fresh for each test. Worker-scoped fixtures are created once per parallel worker and shared across all tests in that worker. Use this for expensive operations like database connections or browser-level authentication.

ts
// Авторизація один раз на worker, а не на кожний тест
export const test = base.extend({
  workerAuthState: [
    async ({ browser }, use) => {
      // Це виконується один раз на worker
      const page = await browser.newPage({ storageState: undefined })
      await page.goto('/login')
      await page.getByLabel('Email').fill(`worker${test.info().parallelIndex}@test.com`)
      await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
      await page.getByRole('button', { name: 'Sign in' }).click()
      await page.waitForURL('/dashboard')

      const state = await page.context().storageState()
      await page.close()

      await use(state) // всі тести у worker'і отримують цей стан
    },
    { scope: 'worker' }, // <-- ключовий параметр
  ],
})