Skip to content
← Back

Lesson 3 of 6

Prove the real browser, including failure and keyboard use

Drive Chromium against your local Vite server: main flow with reload, a blocked-storage failure, an axe scan, and a keyboard-only add — without pointing tests at random websites.

Point Playwright at the local Vite server

The starter contains complete playwright.config.ts and all three e2e/*.spec.ts files; use them as runnable worked examples. Install its declared dependencies first with npm install, then install Chromium:

npx playwright install chromium
npm run test:e2e

The full config imports defineConfig and devices from @playwright/test, targets one Chromium project, sets testDir: './e2e', and uses http://127.0.0.1:4173 as baseURL. Its webServer runs npm run dev. Stop your manual dev server before this run so the harness starts the app itself.

For a manual scaffold, also install @playwright/test, @axe-core/playwright, and @types/node, then copy playwright.config.ts and the test:e2e script from the starter. page.goto('/') targets your app, never an unrelated public site. A server timeout is a reason to inspect the terminal and port, not to add a sleep.

Locate by role and prove reload, not a sleep

Prefer roles and visible names. getByLabel('Task') can match “Show tasks” and aria-label="Tasks" and then Playwright refuses the ambiguous match. Use the textbox role instead:

await page.getByRole('textbox', { name: 'Task' }).fill('Ship the tests');
await page.getByRole('button', { name: 'Add task' }).click();
await expect(page.getByText('Ship the tests', { exact: true })).toBeVisible();
await page.reload();
await expect(page.getByText('Ship the tests', { exact: true })).toBeVisible();

Do not use page.waitForTimeout(1000). Web-first assertions retry until they pass or time out. Persistence is the usual false pass: a row that never survived reload. The reload line is the check, not the click that added the row.

Failure paths: blocked storage and corrupt bytes

A suite that only adds a task will let an agent “fix” a storage error by wiping data. Add e2e/failure.spec.ts. Before goto, make localStorage throw:

await page.addInitScript(() => {
  Object.defineProperty(window, 'localStorage', {
    configurable: true,
    get() { throw new Error('storage blocked for this test'); }
  });
});

Assert the status region reports unavailability, then submit a title and assert no list item and no success message. In a second test, seed lintlabs-task-tracker-v1 with {"members":[1,2,3]} before load. Assert the error, then evaluate the same key and confirm the string is unchanged. That forbids a silent overwrite.

Axe is a floor; keyboard-only is the other half

Automated scans catch some structural issues (missing names, some contrast). They do not certify the page. Playwright’s own accessibility guide says many problems need a person. Gate on serious and critical axe findings for tags wcag2a and wcag2aa, and also complete add with the keyboard:

await page.getByRole('textbox', { name: 'Task' }).focus();
await page.keyboard.type('Added with the keyboard');
await page.keyboard.press('Enter');
await expect(page.getByText('Added with the keyboard', { exact: true })).toBeVisible();

Run npm run test:e2e. Copy the “N passed” line. If a test fails, run npx playwright show-report and open the trace instead of guessing. Say “axe clean on this flow and keyboard-completable”, never “the app is accessible”.

Sources

How did this lesson go? Give feedback →