Codegen outputs a runnable .spec.ts in the time it takes to click through a flow once, but the generated test is named 'test', contains the credentials you typed, and rarely includes more than one assertion. Treating it as finished code is the most common mistake. This guide covers how to run codegen, how to pick better locators from the options the Inspector offers, how to clean up the output before committing it, and where codegen can't help so you know what to write manually.
What codegen actually generates
Codegen is Playwright's built-in test recorder. You launch it with a single command, interact with a page in the browser that opens, and a panel on the side writes the corresponding TypeScript (or JavaScript, Python, Java, or C#) as you go.
What you get out the other end is a .spec.ts file that contains:
page.goto()calls for any navigation- Action methods like
click(),fill(),check(), andselectOption()for every interaction you performed - Locators for each element you touched, picked automatically by Playwright's locator strategy
- A basic assertion at the end if you used the assertion tool in the inspector
The generated code is runnable immediately. Paste it into a test file and run it with npx playwright test; in most cases it passes on the first try. The value isn't that the output is perfect; it's that you have a working skeleton in under two minutes instead of thirty.
Running codegen against lab.becomeqa.com
Launch codegen against any URL:
npx playwright codegen https://lab.becomeqa.comTwo windows open at the same time. The first is a standard Chromium browser showing the target page. The second is the Playwright Inspector: a separate window with a code panel, a record button, and a locator picker.
You don't need a special project for this. Codegen works outside a Playwright project entirely. If you're in a project directory, the generated code respects the language configured there. If you're not, it defaults to TypeScript.
To generate code in a specific language:
# JavaScript
npx playwright codegen --target javascript https://lab.becomeqa.com
# Python
npx playwright codegen --target python https://lab.becomeqa.com
# Java
npx playwright codegen --target java https://lab.becomeqa.comTo target a specific browser instead of the default Chromium:
npx playwright codegen --browser firefox https://lab.becomeqa.com
npx playwright codegen --browser webkit https://lab.becomeqa.comThe --browser flag is useful when you suspect locators differ between Chromium and WebKit, or when you're building a test that specifically targets Firefox behavior.
The codegen interface: three tools you need to know
The Playwright Inspector gives you three things worth understanding before you start recording.
The Record button starts and stops recording. When it's active (red), every interaction in the browser window is captured as code. Click it again to pause without closing the session. This lets you navigate to a hard-to-reach state manually (like opening a modal or navigating several pages deep) and then resume recording once you're there. The Locator picker (the crosshair icon) switches the browser into inspection mode. Move your cursor over any element and the Inspector shows you the locator Playwright would generate for it, along with a preview of how many elements that locator matches. Green means one match. Yellow means multiple matches, so that locator is ambiguous and shouldn't be used in a test. The assertion panel (the checkbox icon) lets you add assertions while recording. Click it, then click an element in the browser, and codegen inserts atoBeVisible() assertion into the code. You can also assert text content or checked state from the dropdown that appears. This is one of the most underused features: beginners often record actions but forget to assert anything.
Why codegen picks getByRole first
When you click a button labeled "Login", codegen doesn't generate page.locator('button.login-btn'). It generates page.getByRole('button', { name: 'Login' }).
This is intentional. Playwright's locator strategy prioritizes locators in this order:
1. getByRole: matches by ARIA role and accessible name
2. getByLabel: for form inputs associated with a label
3. getByPlaceholder: for inputs with placeholder text
4. getByText: for non-interactive elements
5. getByTestId: for elements with a data-testid attribute
6. CSS or XPath: last resort
The reason getByRole comes first is resilience. A button's CSS class can change when a designer updates the stylesheet. Its position in the DOM can shift when someone adds a new feature. But a button labeled "Login" almost always stays labeled "Login": changing that would break the UI for real users too. Tests built on accessible locators are more stable because they break for the same reasons real users would notice.
When you see codegen generating something like page.locator('.btn-primary'), that's a sign the element doesn't have a good accessible name. It's worth stopping and asking whether a data-testid attribute can be added to that element.
aria-label or making sure the label element is properly associated with its input. Better accessibility and better test locators come from the same source.Recording a complete login flow
Here's how to record an actual login test against lab.becomeqa.com step by step.
Start codegen:
npx playwright codegen https://lab.becomeqa.comOnce both windows are open and recording is active, perform this sequence in the browser:
1. Click the Login button in the navigation
2. Fill in the email field with a test account
3. Fill in the password field
4. Click Submit
5. Wait for the dashboard to load
6. Use the assertion picker to assert that something on the dashboard is visible
After completing those steps, the Inspector panel shows something close to this:
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/');
await page.getByRole('button', { name: 'Login' }).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('My Travel Items')).toBeVisible();
});Click the copy button in the Inspector, create a file at tests/login.spec.ts, and paste it in. Run it:
npx playwright test tests/login.spec.ts --project=chromiumIt passes. You have a working test in the time it took to click through the login flow once.
Cleaning up generated code
The generated code runs, but it's not production quality. Three things need attention before it belongs in a real test suite.
Hardcoded credentials. The generated code contains the actual email and password you typed. Those don't belong in source control. Move them to environment variables:import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('My Travel Items')).toBeVisible();
});If you have a baseURL in playwright.config.ts, also replace the hardcoded URL in page.goto() with '/'.
'test'. Rename it to describe the behavior being verified, like 'user can log in with valid credentials'.
Missing assertions. Codegen only records actions and the assertions you explicitly added. A login test should assert more than one thing: not just that "My Travel Items" text appears, but that the URL changed, or that the user menu shows the correct email. Add those manually.
Repetitive locators. If you recorded a flow that uses the same locator five times, extract it to a variable:
const emailInput = page.getByLabel('Email');
await emailInput.fill(process.env.TEST_EMAIL!);This is minor at the test level, but becomes essential when the same locators appear across multiple test files. That's the point where you move to Page Object Model. Don't do it before you need to.
Recording in different browsers and saving output to a file
Codegen supports all three of Playwright's browser engines. By default it uses Chromium. Switch to Firefox or WebKit when you need locators that work across engines or when debugging browser-specific behavior:
npx playwright codegen --browser webkit https://lab.becomeqa.comYou can also save the generated code directly to a file instead of copying from the Inspector panel:
npx playwright codegen --output tests/login.spec.ts https://lab.becomeqa.comWhen you use --output, the file is written when you close the Inspector window. If the file already exists, it gets overwritten. Combine this with a specific target language to generate test files in any supported stack:
npx playwright codegen --target javascript --output tests/login.spec.js https://lab.becomeqa.comFor teams using device emulation, codegen supports --device to simulate mobile viewports:
npx playwright codegen --device "iPhone 13" https://lab.becomeqa.comThis opens the browser at the iPhone 13 screen size with the correct user agent. The generated code includes devices['iPhone 13'] in the context options.
Using codegen as a locator explorer (without recording tests)
One of the most practical uses of codegen doesn't involve recording a full test at all. Sometimes you just need to find the right locator for a specific element and you don't want to spend time inspecting the DOM manually.
Launch codegen, click the Locator picker, and move your cursor over the element you're interested in. The Inspector shows you the locator in real time. It also shows you how many elements match: critical information before you commit a locator to a test.
npx playwright codegen https://lab.becomeqa.comOnce the browser opens, press the record button to stop recording so you're not generating code you don't need. Switch to the Locator picker mode and hover over elements. The Inspector updates the locator display as you move between elements. You can type into the locator field to test variations and see how many elements match.
This workflow replaces hours of trial-and-error in the test runner. You validate a locator before you write the test, not after it fails.
The limits of codegen: what it cannot record
Codegen handles most user interactions: clicks, form fills, navigation, dropdowns, checkboxes, radio buttons. There are several categories it handles poorly or not at all.
File uploads. Codegen cannot recordpage.setInputFiles(). When you click a file input and select a file from your filesystem, codegen may capture the click but not the file selection itself. You need to write file upload code manually.
Drag and drop. HTML drag-and-drop events don't translate cleanly to codegen recordings. The generated code may show a click without the drag motion, which means the interaction silently doesn't do what you expect. Use page.dragAndDrop() or simulate the individual mousedown, mousemove, and mouseup events for reliable drag interactions.
Complex waits. Codegen records actions at the moment you perform them. It doesn't record the implicit thinking you do as a human ("I waited for the spinner to disappear before clicking"). In slow or unreliable environments, generated tests may fail because they run faster than the UI expects. Add explicit waits where the flow requires them:
await page.waitForLoadState('networkidle');
await expect(page.getByRole('progressbar')).toBeHidden();context.waitForEvent('page'). Codegen may miss the new tab entirely.
Authenticated states. Codegen doesn't know about stored authentication. If you want tests to start already logged in, use Playwright's storageState to save session cookies after logging in once, then load that state at the start of each test. Codegen can help you record the login once, but the storage state pattern needs to be wired up manually.
OAuth and third-party login flows. Any flow that redirects to an external identity provider (Google, GitHub, Microsoft) is difficult to record with codegen because the locators exist on a third-party page you may not control. These flows need separate handling and often require mocking the OAuth callback instead of going through the real provider.
FAQ
Is codegen code good enough to commit directly?Rarely. It's a fast starting point, not a finished test. Always rename the test, remove hardcoded credentials, verify the assertions are meaningful, and check that locators match the page's accessibility structure.
Can I use codegen on a localhost URL?Yes. npx playwright codegen http://localhost:3000 works exactly the same way. If your app requires authentication to reach the page you want to record, you can log in manually before clicking the record button.
Only if there's no better option. A CSS selector like .item-card:nth-child(2) will break the moment the page layout changes. Try to understand why Playwright fell back to CSS: the element probably lacks an accessible name. If you can't add one, getByTestId with a data-testid attribute is the next best choice.
No measurable difference. The recording happens in the Inspector process, not inside the browser itself.
Can I pause in the middle of recording and navigate manually?Yes. Click the record button to pause, navigate to the state you need, then click record again to resume. Only interactions while recording is active are captured.
Why did codegen generatepage.locator('text=Submit') instead of getByRole?
This happens when Playwright can't find a reliable role-based locator. It falls back to text matching. Check whether the element is a button with an accessible name; it should generate getByRole('button', { name: 'Submit' }) for a standard button element.