Playwright Test, the TypeScript test runner, is where most of Playwright's productivity comes from: one config file controls browsers, timeouts, retries and artifacts; fixtures replace setup code with dependency injection; and parallel workers and sharding keep large suites fast. This guide covers each with working TypeScript examples.
The Test Runner — test, expect, hooks
You import test and expect from @playwright/test. The page fixture is injected automatically — no manual browser/context/page setup.
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Products')).toBeVisible();
});
Grouping & hooks
test.describe('Checkout', () => {
test.beforeEach(async ({ page }) => { await page.goto("/"); });
test.afterEach(async ({ page }, testInfo) => { /* per-test cleanup */ });
test('adds item to cart', async ({ page }) => { /* ... */ });
});
// beforeAll / afterAll also available; test.skip / test.only / test.fixme
Every Playwright call returns a Promise — always await it. Forgetting await is the #1 TypeScript beginner bug and causes race conditions and confusing failures.
playwright.config.ts — The Control Centre
This single file replaces most of what a Java base class + Maven config does: which browsers, retries, parallelism, base URL, and what artifacts to capture.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // run tests in parallel
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'https://www.saucedemo.com',
trace: 'on-first-retry', // trace when a test retries
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});
| Config key | Purpose |
|---|---|
| testDir | Where the spec files live |
| fullyParallel | Run all tests in parallel (across workers) |
| workers | Number of parallel worker processes |
| retries | Auto-retry failed tests N times |
| use.baseURL | goto("/") becomes relative |
| use.trace | off / on / retain-on-failure / on-first-retry |
| use.screenshot / video | Artifact capture policy |
| projects | Browser/device matrix (each a named run) |
| reporter | html / list / json / junit / allure |
Projects, fixtures, built-in parallelism/sharding and this config file are the TypeScript runner’s superpowers. In Java you achieve the same outcomes with JUnit/TestNG + a factory + testng.xml. When a tutorial shows playwright.config.ts, that is this runner.
Fixtures — The TypeScript Superpower
Custom fixtures inject ready-made objects (page objects, logged-in pages, test data) into your tests. This is the idiomatic TS alternative to a Java base class + factory.
// fixtures/test.ts — extend the base test with page objects
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { InventoryPage } from '../pages/inventory.page';
type Pages = { loginPage: LoginPage; inventoryPage: InventoryPage };
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
});
export { expect } from '@playwright/test';
// Now tests receive page objects directly — clean and DRY
import { test, expect } from '../fixtures/test';
test('valid login', async ({ page, loginPage, inventoryPage }) => {
await page.goto('/');
await loginPage.login('standard_user', 'secret_sauce');
await expect(inventoryPage.header).toHaveText("Products");
});
Reusing authentication (setup project)
// auth.setup.ts — log in once, save storage state
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/');
// ...login...
await page.context().storageState({ path: 'auth/user.json' });
});
// playwright.config.ts — make tests depend on setup and reuse the state
// projects: [
// { name: 'setup', testMatch: /auth\.setup\.ts/ },
// { name: 'chromium', dependencies: ['setup'],
// use: { storageState: 'auth/user.json' } },
// ]
Data-Driven Testing
const logins = [
{ user: 'standard_user', pass: 'secret_sauce', ok: true },
{ user: 'locked_out_user', pass: 'secret_sauce', ok: false },
];
for (const data of logins) {
test(`login: ${data.user}`, async ({ page, loginPage }) => {
await page.goto('/');
await loginPage.login(data.user, data.pass);
if (data.ok) await expect(page.getByText("Products")).toBeVisible();
else await expect(page.getByTestId("error")).toBeVisible();
});
}
For CSV/JSON, import a parser (e.g. csv-parse) or read a JSON file and loop the same way. Each iteration becomes its own reported test.
Parallelism & Sharding (built-in)
- Tests run in parallel across worker processes by default (fullyParallel + workers).
- Files run in parallel; tests within a file run serially unless you opt in.
- Opt a file into parallel: test.describe.configure({ mode: "parallel" }).
- Each worker gets its own browser + isolated context automatically — no ThreadLocal needed.
# Run a subset / control parallelism
npx playwright test --project=chromium
npx playwright test --workers=4
npx playwright test tests/login.spec.ts
npx playwright test -g "checkout" # by title
# Shard across CI machines (e.g. 3 runners)
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
Reporters & CI
- Built-in: html (npx playwright show-report), list, dot, json, junit.
- Allure: npm i -D allure-playwright, add to reporter array, then allure serve.
- Traces/videos/screenshots attach to the HTML report automatically per your config policy.
# .github/workflows/tests.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with: { name: playwright-report, path: playwright-report/ }
Debugging Toolkit
| Command | What it does |
|---|---|
| npx playwright test --ui | UI Mode — watch, time-travel, pick locators (TS-only gem) |
| npx playwright test --debug | Run with the Inspector, step through |
| npx playwright codegen <url> | Record clicks → generate TS code |
| npx playwright show-trace trace.zip | Open the Trace Viewer |
| npx playwright show-report | Open the HTML report |
| await page.pause() | Pause mid-test in the Inspector |
npx playwright test --ui gives a watch-mode dashboard: pick tests, see every step with before/after DOM snapshots, edit and re-run instantly. Java has no direct equivalent — it relies on the Trace Viewer + Inspector. If you are learning TS, live in UI Mode.
FAQs
What is playwright.config.ts used for?
It defines how tests run: test directory, timeouts, retries, workers, reporters, shared use options such as baseURL and trace, web server startup, and projects for each browser or device.
How do you create a custom fixture?
Extend the base test with test.extend, define the fixture function that sets up a value, passes it to use() and cleans up afterwards, then import your extended test in spec files.
How many workers should Playwright use in CI?
Start with the CI machine's CPU count (often set via workers in config) and adjust: too many workers on a small runner slows everything down; shard across machines for large suites.