Playwright and stable selectors - the data-testid attribute

Playwright and stable selectors - the data-testid attribute

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

Selector playground (mp4)

Email field (mp4)

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

Next: Cross-browser runs with Playwright.