C.W.K.
Stream
Lesson 03 of 04 · published

Codegen, Headed Mode, page.pause

~15 min · playwright-setup, codegen, debug

Level 0Test Curious
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Codegen drafts. Headed mode watches. page.pause lets you sit inside the test."

Codegen — Record a Draft

npx playwright codegen http://localhost:3000 opens two windows: a real browser pointed at the URL, and an Inspector side panel that emits code as you click. The flow is: click through the user journey you want to test; copy the generated code into a spec file; clean it up.

'Clean it up' is mandatory. The generated code is verbose, picks decent-but-not-great selectors (it'll grab a CSS class when role + name would survive better), and lacks assertions until you record them via the Inspector's 'Assert visibility' / 'Assert text' buttons. Treat codegen as a draft generator: it gets you 70% of the way, and the last 30% is the part that makes the test maintainable.

Headed Mode — Watch It Happen

Default test runs are headless — no visible browser. --headed shows the browser window so you can watch the test drive it in real time. Pair with --project=chromium to avoid three windows simultaneously, and --slowMo=500 to slow each action to 500 ms so you can actually see what's happening.

Headed mode is for debugging single tests, not for routine runs. It's slow (no parallelism, manual observation), but invaluable when a test fails in a way the trace viewer doesn't make obvious.

--debug — The Step-Through Mode

npx playwright test --debug opens the Playwright Inspector — a UI that pauses BEFORE the test runs, lets you step through actions one at a time, shows the locator under the cursor, and lets you experiment with new selectors in a REPL. The most efficient debug loop Playwright offers.

Inside the test, you can also await page.pause() at any line. The test pauses, Inspector opens, you step through from that point. The pause-and-inspect pattern is the modern equivalent of console.log.

Codegen is for the first draft. The trace viewer is for failure investigation. page.pause is for live experimentation. Three tools, three jobs — don't try to use one for all of them.

The Inspector's Hidden Powers

The Inspector (opens with codegen and --debug) has features that aren't obvious from the toolbar:

  • 'Pick locator' — hover any element in the browser, see the best Playwright selector for it.
  • 'Explore' — type a locator (page.getByRole('button')) and see which elements match in real time, highlighted in the browser.
  • 'Assert' buttons — Inspector generates expect(...).toBeVisible() / toHaveText() from your interaction.

The Headed-Mode Failure Pattern

When a test fails mysteriously, headed mode + slowMo + page.pause is the diagnostic. Add await page.pause() right before the failing line, run with --debug, step through, see where reality diverged from your assumption. Once you understand, remove the pause and fix the test (or the code).

Code

Codegen — record interactions, get code·bash
# Codegen — record a test by clicking
npx playwright codegen http://localhost:3000

# Codegen against a specific viewport (useful for mobile UI)
npx playwright codegen --device='Pixel 5' http://localhost:3000

# Codegen + save directly to a file
npx playwright codegen --target=playwright-test \
  --output=e2e/login.spec.ts \
  http://localhost:3000
Before vs after — codegen output then cleanup·typescript
// Generated by codegen — what it actually looks like (before cleanup)
import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.getByRole('link', { name: 'Sign in' }).click();
  await page.getByLabel('Email').click();
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').click();
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByText('Welcome back, user')).toBeVisible();
});

// Cleanup pass — name it, group it, remove redundant clicks
test('signs in with valid credentials', async ({ page }) => {
  await page.goto('/');

  await page.getByRole('link', { name: 'Sign in' }).click();
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Submit' }).click();

  await expect(page.getByText('Welcome back, user')).toBeVisible();
});
Headed + slowMo + --debug — the local debug loop·bash
# Headed mode — watch the browser
npx playwright test --headed --project=chromium

# Slow it down so you can actually see
npx playwright test --headed --project=chromium --slowMo=500

# Debug mode — Inspector pauses before each action, step through
npx playwright test --debug

# Debug a single test
npx playwright test e2e/login.spec.ts -g 'signs in' --debug
page.pause — modern equivalent of breakpoint·typescript
// page.pause() — drop a breakpoint inside a test
import { test, expect } from '@playwright/test';

test('something tricky', async ({ page }) => {
  await page.goto('/dashboard');
  await page.getByRole('button', { name: 'Open settings' }).click();

  await page.pause();   // ← pauses here when run with --debug
                        //   Inspector opens; you can step, try selectors,
                        //   inspect DOM state.

  await page.getByRole('switch', { name: 'Dark mode' }).check();
  await expect(page.locator('html')).toHaveClass(/dark/);
});

// Run with: npx playwright test --debug -g 'something tricky'
// Remember to remove page.pause() before commit.

External links

Exercise

Pick a small flow in your app (sign in, add an item, submit a form). Use codegen to record it. Copy the output into a spec file. Now clean it up: give the test a real name, remove redundant clicks (codegen often clicks before filling — the fill alone is enough), and add at least one assertion using expect(...).toBeVisible(). Run with --headed --slowMo=300 once to confirm. Then run normally.
Hint
If codegen picks a brittle selector (a long CSS path with auto-generated class names), pause the Inspector, hover the same element, and use 'Pick locator' to find the role/name version. Replace the CSS selector with the cleaner one.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.