Playwright Codegen Complete Guide: Record, Edit, Refactor
Master Playwright codegen for 2026: record tests visually, inspect locators, generate auth flows, edit recorded specs, and avoid common pitfalls.
Playwright Codegen Complete Guide: Record, Edit, Refactor
Playwright Codegen is the recording tool that bridges manual testing and automated tests. You drive the browser, Codegen captures every action as TypeScript (or Python, Java, .NET), and you walk away with a runnable spec. It is the fastest way to bootstrap a test, the easiest way to teach Playwright to new team members, and the best way to capture the exact accessible locators your app exposes.
This guide is the working playbook for using Codegen in 2026: launching it, the recording window, the locator picker, saving auth state, refactoring the output, and the patterns that turn recorded scripts into production-quality tests. Every example is TypeScript with Playwright 1.49+.
For Playwright fundamentals, the Playwright E2E Complete Guide is the starting point. The playwright-e2e skill ensures AI assistants generate tests in the same style Codegen would.
Launching Codegen
# Start with a URL
npx playwright codegen https://qaskills.sh
# Start on a specific project
npx playwright codegen --project=chromium
# Start with a device
npx playwright codegen --device="iPhone 15 Pro"
# Save HAR file alongside recording
npx playwright codegen --save-har=fixture.har https://qaskills.sh
# Save trace alongside recording
npx playwright codegen --save-trace=trace.zip https://qaskills.sh
# Specify output language
npx playwright codegen --target=python
The window opens two panes side by side: a browser on the left, a code editor on the right. Every action you take in the browser appears as a typed command in the editor.
The recording window
| Region | Purpose |
|---|---|
| Browser pane | Drives the actual application |
| Code editor pane | Shows the generated spec, language-aware |
| Toolbar | Pause, resume, copy, clear, choose language |
| Picker | Highlight elements and copy locators |
| Assertions | Add assertions during recording |
The toolbar has buttons for "Record", "Pick locator", "Assert visibility", "Assert text", and "Assert value". Use them mid-recording to insert assertions that match what you see.
Your first recording
Click around, type, hit buttons. The code grows in real time.
// Generated by Codegen
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('https://qaskills.sh');
await page.getByRole('link', { name: 'Skills' }).click();
await page.getByRole('searchbox').fill('playwright');
await page.getByRole('searchbox').press('Enter');
await expect(page.getByRole('link', { name: 'Playwright E2E' })).toBeVisible();
});
Notice that Codegen produced getByRole and getByText locators rather than CSS. That is the picker preferring the accessibility tree, which is exactly the locator strategy you want.
The locator picker
Click the "Pick locator" button (target icon) and hover over any element. The recommended locator appears in a popover.
| Strategy | Example |
|---|---|
| Role + name | getByRole('button', { name: 'Save' }) |
| Label | getByLabel('Email') |
| Placeholder | getByPlaceholder('Search') |
| Text | getByText('Welcome back') |
| Test ID | getByTestId('user-avatar') |
| CSS | locator('.btn.primary') |
The picker walks down the priority list and offers the most stable option. Click to copy.
Adding assertions during recording
Three assertion buttons inject the corresponding expect:
| Button | Generates |
|---|---|
| Assert visibility | await expect(locator).toBeVisible() |
| Assert text | await expect(locator).toContainText('...') |
| Assert value | await expect(locator).toHaveValue('...') |
Click the button, then click the element in the browser. The assertion appears in the code at the current cursor position.
Recording with auth: storage state
For pages behind login, record the login once and reuse the storage state.
# Step 1: record login, save storage state
npx playwright codegen --save-storage=auth.json https://qaskills.sh/login
# Step 2: record actual test using the saved state
npx playwright codegen --load-storage=auth.json https://qaskills.sh
The second command opens directly on the authenticated page and starts recording from there. Hand-edit the spec to load storageState from a config-driven path instead of the raw JSON.
Recording mobile flows
npx playwright codegen --device="iPhone 15 Pro" https://qaskills.sh
The browser opens at the iPhone viewport with touch events. Tapping a button records a .click() (Playwright translates the click into the appropriate touch event internally).
Recording multi-tab flows
When the app opens a new tab, Codegen attaches automatically and records actions in either tab.
const pagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: 'Docs' }).click();
const popup = await pagePromise;
await popup.getByRole('heading', { name: 'Documentation' }).waitFor();
See Playwright Multi Page Popup Handling Guide for the runtime details.
Refactoring recorded specs
Codegen produces working tests, not great tests. Treat the output as a starting point and refactor.
1. Add descriptive names
// Before
test('test', async ({ page }) => { ... });
// After
test('@smoke search returns Playwright skill', async ({ page }) => { ... });
2. Extract page objects
// Before
await page.getByRole('searchbox').fill('playwright');
await page.getByRole('searchbox').press('Enter');
// After
class SkillsPage {
constructor(private readonly page: Page) {}
readonly search = this.page.getByRole('searchbox');
async searchFor(text: string) {
await this.search.fill(text);
await this.search.press('Enter');
}
}
3. Remove redundant waits
Codegen sometimes adds page.waitForURL calls that the next assertion would handle anyway. Delete them.
4. Group related actions
// Before: flat sequence
await page.goto('/login');
await page.getByLabel('Email').fill('...');
await page.getByLabel('Password').fill('...');
await page.getByRole('button', { name: 'Sign in' }).click();
// After: grouped helper
async function login(page: Page, email: string, password: string) {
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
}
5. Replace hardcoded data
Replace literals with fixtures or environment variables for portability.
Output languages
Codegen supports four target languages.
| Language | Flag | Output |
|---|---|---|
| TypeScript (default) | (none) | @playwright/test |
| JavaScript | --target=javascript | @playwright/test |
| Python | --target=python | playwright pytest plugin |
| Python async | --target=python-async | asyncio Playwright |
| Java | --target=java | Playwright for Java |
| .NET | --target=csharp | Playwright for .NET |
For polyglot teams, use Codegen to record once and then translate to the target language.
Recording with HAR
--save-har captures every request for later replay.
npx playwright codegen --save-har=fixture.har https://qaskills.sh
In your spec, replay the HAR to test offline.
await page.routeFromHAR('./fixture.har');
See Playwright Network Mocking Route Handler Guide for the playback API.
Recording with trace
--save-trace saves a Playwright trace alongside the recording.
npx playwright codegen --save-trace=trace.zip https://qaskills.sh
Open with npx playwright show-trace trace.zip to inspect every snapshot, network call, and console message.
Common pitfalls
Pitfall 1: Recording static text as a locator for buttons. Codegen sometimes picks getByText('Save') when getByRole('button', { name: 'Save' }) would be more stable. Manually correct.
Pitfall 2: Hardcoded values in assertions. Codegen captures literal text, including timestamps. Replace with regex or relative comparisons.
Pitfall 3: Recording dev URLs. Recorded tests often hard-code http://localhost:3000. Move base URL to config.
Pitfall 4: Missing test.use. Recorded tests do not include locale, timezone, or storage state. Add per project or per file.
Pitfall 5: Recording with browser extensions. Extensions inject DOM that breaks recording. Use an incognito profile or --user-data-dir with a clean profile.
Anti-patterns
- Committing the literal recorded spec without refactoring. The output is a starting point.
- Recording the same flow over and over. Record once, extract a helper, reuse.
- Recording entire flows when you only need a tricky step. Record the step, paste into an existing test.
- Skipping the picker for elements you know. The picker is still faster than typing the locator.
Codegen vs UI Mode
| Tool | Use when |
|---|---|
| Codegen | Bootstrapping a new spec from scratch |
| UI Mode | Iterating on existing specs, debugging |
Both share the picker. Codegen produces code; UI Mode runs code. Use Codegen first, then move to UI Mode for the inner loop. See Playwright UI Mode Complete 2026 Guide.
Productive Codegen workflows
Bootstrap workflow:
npx playwright codegen https://staging.example.com/feature- Click through the happy path.
- Save the spec.
- Refactor: page objects, fixtures, descriptive names.
- Add edge cases by hand.
Auth workflow:
npx playwright codegen --save-storage=auth.json /login- Sign in once.
- Quit Codegen.
npx playwright codegen --load-storage=auth.json /dashboard- Record the rest of the test as a logged-in user.
Mobile workflow:
npx playwright codegen --device="Pixel 8" /- Walk through the mobile flow.
- Convert the recorded test into a per-device parameter.
Conclusion and next steps
Codegen is the on-ramp from manual testing to automated testing. Use it to bootstrap specs, train new team members, and capture the exact accessible locators your app exposes. Treat the output as a draft and refactor toward production quality.
Install the playwright-e2e skill so AI assistants generate test code that mirrors Codegen's locator choices. For the iterative loop after recording, read Playwright UI Mode Complete 2026 Guide.