Introduction
Long :nth-child selectors break the first time someone tweaks the layout. Cypress best practices tell you to target dedicated data-* attributes (their examples and the parallel blog post often use data-cy). In the Playwright snippets below I stick to data-testid only: that is the attribute getByTestId queries by default, so you stay aligned with Playwright docs while following the same stability idea Cypress recommends.
Previous post: URLs and configuration.
The data-testid attribute
getByTestId resolves to [data-testid=...] out of the box; you do not need extra config to match Playwright’s documented style.
React example (same refactor as the Cypress series, with Playwright’s attribute name):
<input
className="form-control form-control-lg"
type="email"
data-testid="email-input"
...
/>
<input
type="password"
data-testid="password-input"
...
/>
<button type="submit" data-testid="sign-in-button">
Sign in
</button>Spec code
import { test, expect } from "@playwright/test"
test("logs in", async ({ page }) => {
await page.goto("/login")
await page.getByTestId("email-input").fill("test@test.com")
await page.getByTestId("password-input").fill("test")
await page.getByTestId("sign-in-button").click()
await expect(page).toHaveURL(/localhost:4100\/$/)
})getByTestId participates in the same auto-waiting and composition rules as other locators.
Reference videos
Git references
https://github.com/12masta/react-redux-realworld-example-app/tree/5-cypress
https://github.com/12masta/react-redux-realworld-example-app/pull/6/files