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
async/await everywhere

Every Playwright call returns a Promise — always await it. Forgetting await is the #1 TypeScript beginner bug and causes race conditions and confusing failures.

Advertisement

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 keyPurpose
testDirWhere the spec files live
fullyParallelRun all tests in parallel (across workers)
workersNumber of parallel worker processes
retriesAuto-retry failed tests N times
use.baseURLgoto("/") becomes relative
use.traceoff / on / retain-on-failure / on-first-retry
use.screenshot / videoArtifact capture policy
projectsBrowser/device matrix (each a named run)
reporterhtml / list / json / junit / allure
This is what Java does NOT have

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

CommandWhat it does
npx playwright test --uiUI Mode — watch, time-travel, pick locators (TS-only gem)
npx playwright test --debugRun with the Inspector, step through
npx playwright codegen <url>Record clicks → generate TS code
npx playwright show-trace trace.zipOpen the Trace Viewer
npx playwright show-reportOpen the HTML report
await page.pause()Pause mid-test in the Inspector
UI Mode is the TypeScript killer feature

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.