Skip to content

RELIABLE SUITES · LESSON 9 OF 24

Custom fixtures and cleanup

Create reusable test setup with a clear lifecycle.

  • test.extend
  • fixtures
  • use
  • teardown

Your runnable checkpoint

Extract into a new practice folder. Each download contains tests, configuration, the complete local practice app and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install dependencies and browser binaries using the commands below. The test runner starts the practice server on port 4174. Tests target localhost, never the production EduPoly site.

1. Understand the concepts

A fixture supplies a dependency to tests that request it. It can create a value, call await use(value), and perform cleanup afterwards. This makes setup and teardown live together.

Test-scoped fixtures are created for individual tests; worker-scoped fixtures can be shared within a worker and must be designed for that lifetime. Do not put mutable per-test state in a worker fixture without isolation.

This fixture navigates to the app and returns a helper that creates a task. It wraps repeated mechanics while leaving assertions in the test. It does not hide an entire business workflow behind a mysterious one-line helper.

Import test from the fixture module so the custom dependency exists. Importing the base test by mistake means the fixture type and runtime dependency are absent.

2. Build and run

These are complete test files and configuration. The download also includes app/ and server.mjs; preserve their paths. Stop any manually started practice server before running the suite.

Terminal
npm install
npx playwright install chromium
npm run check
npm test

tests/lesson.spec.ts

tests/lesson.spec.ts
import { test, expect } from './fixtures';
test('create a task with a fixture', async ({ page, addTask }) => {
  await addTask('Fixture task');
  await expect(page.getByTestId('task-row')).toContainText('Fixture task');
});

tests/fixtures.ts

tests/fixtures.ts
import { test as base, expect } from '@playwright/test';
type Fixtures = { addTask: (title: string) => Promise<void> };
export const test = base.extend<Fixtures>({
  addTask: async ({ page }, use) => {
    await page.goto('/');
    await use(async title => {
      await page.getByLabel('Task title', { exact: true }).fill(title);
      await page.getByRole('button', { name: 'Add task', exact: true }).click();
    });
  }
});
export { expect };

playwright.config.ts

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  timeout: 30000,
  expect: { timeout: 5000 },
  reporter: [['list'], ['html', { open: 'never' }]],
  use: { baseURL: 'http://127.0.0.1:4174', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: { command: 'node server.mjs', url: 'http://127.0.0.1:4174', reuseExistingServer: false, timeout: 15000 }
});

package.json

package.json
{
  "name": "edupoly-playwright-custom-fixtures",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "test:ui": "playwright test --ui",
    "check": "tsc --noEmit",
    "report": "playwright show-report",
    "serve": "node server.mjs"
  },
  "devDependencies": {
    "@playwright/test": "1.56.1",
    "typescript": "5.9.3",
    "@types/node": "22.20.5"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": [
      "node"
    ],
    "noEmit": true
  },
  "include": [
    "tests/**/*.ts",
    "pages/**/*.ts",
    "playwright.config.ts"
  ]
}

.gitignore

.gitignore
node_modules/
playwright-report/
test-results/
playwright/.auth/
.env

3. Trace the code and check the result

  1. base.extend declares the extra fixture’s TypeScript contract.
  2. await use hands the helper to the test.
  3. The default page fixture handles browser-context cleanup.

Verify your result

  • The custom fixture supplies addTask.
  • The task is created in a fresh test context.

4. Practice independently

Add a second test using the same fixture and prove it starts empty.

Show the explanation

Assert zero rows before calling addTask in the second test. The fixture is test-scoped, so no previous task should be present.

When something goes wrong

If a browser executable is missing, run npx playwright install chromium in the project folder. If port 4174 is occupied, stop your manual practice server. Read assertion call logs for mismatched locators or expectations. Use npm run report to inspect failures; do not add fixed sleeps or broad resets to hide them.

Read the related official Playwright documentation ↗