How does Playwright's codegen decide which locator to emit for an element you click?
answer
- It scores candidates, not one rule
- User-facing anchors beat structure
- Role plus accessible name
- A flag renames the test-id attribute
- nth-child output means rewrite this
basics
~20 sPlaywright's recorder inspects the element and prefers user-facing anchors: the configured test-id attribute, then role with accessible name, label, placeholder or visible text. Only when none exists does it fall back to a structural CSS chain.
solid answer
~40 sThe generator scores candidate locators for the element you interacted with and emits the best one it can build. An element carrying the configured test-id attribute yields `getByTestId('driver-map')`; otherwise it reaches for what a user perceives — `getByRole('button', { name: 'Refresh status' })`, `getByLabel(...)`, `getByPlaceholder(...)`, `getByText(...)`, `getByAltText(...)` or `getByTitle(...)`. When the element exposes none of those, it falls back to a structural CSS chain such as `locator('section:nth-child(3) > div > span')`, and it may append `.first()` or `.nth(1)` when the pick would otherwise match several elements. `--test-id-attribute=data-qa` changes which attribute counts as a test id for the recording session. The CSS fallbacks are the lines to rewrite first.
code
typescript · 9 linesimport { test } from '@playwright/test';
test('track order', async ({ page }) => {
await page.goto('https://tracker.example.com/orders/42');
await page.getByRole('button', { name: 'Refresh status' }).click();
await page.getByLabel('Delivery notes').fill('Leave at door');
await page.getByTestId('driver-map').click();
await page.locator('section:nth-child(3) > div > span').click();
});go deeper
Recall that the recorder prefers locators a user would recognise — role with a name, label, text, test id — and that a long CSS chain in the output is a warning sign worth raising.
Explain the preference ladder and what pushes the generator down it: no accessible name, no text, no test id. Know that --test-id-attribute renames the attribute the recorder looks for.
Use the output diagnostically. A recording full of nth-child chains is telling you which parts of the interface are unnameable, and the fix often belongs in the product rather than in the test.
Own the loop between the two: decide whether unnameable controls get accessible names or test ids, so that what the recorder generates is good by default rather than repaired by hand each time.
When you click something, Playwright's recorder does not dump the element's CSS path. It looks at the element and asks which locator a human would have written. ## The preference order Roughly, best to worst: 1. **Test id** — if the element carries the attribute codegen is configured to treat as a test id, it emits `getByTestId('driver-map')`. The default attribute is `data-testid`; `--test-id-attribute=data-qa` changes it for the recording session. 2. **Role and accessible name** — `getByRole('button', { name: 'Refresh status' })`. This is the locator that survives markup churn, because it is expressed in terms of what the control *is* and what it is *called*. 3. **Other user-facing anchors** — `getByLabel('Delivery notes')` for a labelled input, `getByPlaceholder(...)`, `getByText('Out for delivery')`, `getByAltText(...)` for images, `getByTitle(...)`. 4. **Structural CSS** — a chain like `locator('section:nth-child(3) > div > span')`, or a selector built from an `id` or a stable-looking attribute, when nothing above applies. ## Why the top of the ladder looks like that The locators codegen prefers are the ones tied to how the page is perceived and operated rather than to how it is nested. A button keeps its role and its visible name across a restyle; its position in the DOM tree does not. That is why the generator spends effort computing an accessible name before it considers a CSS path. ## What forces the fallback | the element in your order tracker | what codegen emits | | --- | --- | | `<button>Refresh status</button>` | `getByRole('button', { name: 'Refresh status' })` | | `<input aria-label="Delivery notes">` | `getByLabel('Delivery notes')` | | `<div data-testid="driver-map">` | `getByTestId('driver-map')` | | `<span class="css-1x9f">17:42</span>` | `locator('section:nth-child(3) > div > span')` | The last row is the interesting one. Icon-only buttons, decorative wrappers, generated class names and map canvases give the generator nothing to name, so it produces exactly the kind of positional chain that breaks on the next markup change. Those lines are the ones to rewrite — or to fix in the product by giving the element a name or a test id. ## Ambiguity and duplicates On a page where every order row has a "Cancel" button, a locator by role and name matches many elements. Codegen will scope or disambiguate what it emits — chaining from a container, or appending `.first()` / `.nth(2)` to the call. A generated `.nth(2)` is a signal, not a solution: it encodes the position the row happened to occupy while you were recording. ## Reading the output critically - Every `getByRole(...)` with a real name: usually keep. - Every `getByTestId(...)`: keep, assuming the attribute is one your team maintains deliberately. - Every `locator('...nth-child...')`: treat as a defect report about the page, not as a test line. - Every trailing `.first()` / `.nth(n)`: check whether the intended element is really "the nth one". ## Version note The `--test-id-attribute` flag and the `getBy*` family used above are current as of Playwright 1.63. The scoring is a heuristic and its output can change between releases, so a generated locator is a suggestion — never treat byte-for-byte output as a contract you assert on.
- Your team marks elements with `data-qa` rather than `data-testid` — how do you get codegen to use it?Pass `--test-id-attribute=data-qa` when launching the recorder. The codegen session is configured from the command line, so an attribute name set for your test project does not reach the recorder on its own; without the flag it keeps looking for `data-testid` and falls back to role, text or CSS.
- What does a generated `.nth(2)` on a locator tell you about the page?That the locator matched several elements and the recorder disambiguated by position — typically a repeated row, such as one order per line. The index encodes where the element sat while you recorded, so replace it by scoping to something identifying about that row rather than keeping the ordinal.
saying these in an interview costs you the question
- Says codegen always emits CSS or XPath selectors
- Thinks the test-id attribute name cannot be changed
- Believes generated locators never need editing
- Assumes role locators match on HTML tag names only
- Treats a generated nth-child chain as good output