Locating elements
Every device method takes a smart selector — a single string that is interpreted one of two ways:
- A plain string targets a
data-testidattribute. - Anything that looks like CSS is used as a CSS selector, unchanged.
await device.click('submit-btn'); // → [data-testid="submit-btn"]
await device.click('#submit'); // → #submit (CSS id)
await device.click('.btn-primary'); // → .btn-primary (CSS class)
await device.click('form > button'); // → CSS combinator
await device.click('[aria-label="Close"]'); // → CSS attribute selector
await device.click('button'); // → HTML element name
Why test ids first
Test ids are stable. Classes change with styling, text changes with copy edits, and DOM structure changes with refactors — but a data-testid only changes when someone deliberately breaks the contract with the test suite.
Add test ids to the elements your tests touch:
<button data-testid="login-submit">Sign in</button>
Then the test reads exactly like the intent:
await device.click('login-submit');
What counts as CSS
A selector passes through as CSS when it starts with . or #, or contains [, >, a space, :, or * — or when it is a bare HTML element name (body, button, input, and so on).
await device.expect('body').toContain('Welcome'); // element name
await device.expect('[data-testid^="dish-card-"]').toHaveCount(12); // attribute prefix match
await device.waitFor('input:focus'); // pseudo-class
data-testid values with spaces or colons would be misread as CSS. Stick to kebab-case ids like cart-badge or dish-card-1.
When a selector fails
If an element can't be found, the error tells you the page URL and every data-testid present on the page — usually enough to spot the typo without opening a browser:
Click failed for "submit-btn" (resolved to "[data-testid="submit-btn"]")
Page URL: https://your-app.com/login
Available data-testid values: ["login-username", "login-password", "login-submit"]
Original error: Waiting for selector failed: 5000ms exceeded
Here the fix is obvious: the button's test id is login-submit, not submit-btn.
Escaping to raw CSS
If you need to bypass smart-selector interpretation entirely, use device.css:
await device.css.click('.legacy-btn');
const text = await device.css.getText('#status');
The css namespace calls Puppeteer directly and does not auto-wait — prefer the main device methods unless you have a reason.