Let me show you the bug that made me stop trusting position-based selectors.
We had a test that added the first product to the cart:
await page.locator('.product-card:nth-child(1) button').click();
It worked for months. Then the marketing team added a "Featured" product to the top of the page. Our test still passed. It was adding the wrong product to the cart, and the test could not tell the difference.
That is the worst thing a test can do. Not fail. Pass while being wrong.
nth-child(1) means "the first one in the list". It describes where the element sits. Positions change whenever someone edits the page.
Instead, describe what the element is:
await page.getByRole('button', { name: 'Add to cart' }).first().click();
getByRole finds elements the way a user sees them. A role is what an element is for: a button, a link, a checkbox. The name is the text a user reads on it.
Playwright gives you several ways to find elements, and there is a recommended order:
getByRole, best. It matches how people and screen readers see the page.getByLabel, for form fields, using the label next to them.getByPlaceholder,getByText, for other visible text.getByTestId, for elements with no clear text or role. This needs adata-testidattribute in the app's code.- Raw CSS or XPath, last resort.
There is a bonus in using getByRole first. If it cannot find your button, a screen reader probably cannot either. Your test just found an accessibility bug for free.
Remember this: if your selector would break when someone reorders the page, it is the wrong selector.
Reference: Locators.