Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
The single most common cause of flaky automated tests is brittle selectors. A test that clicks .btn-primary.submit-order-btn breaks when a developer renames the CSS class for a redesign. A test that selects by div > ul > li:nth-child(3) breaks when someone adds a list item above it. Reliable selectors are resilient to UI changes because they select by what the user sees and interacts with, not by implementation details. The priority order: user-visible text and ARIA roles first (most resilient), data-testid attributes second (explicit contract), CSS class/structure last (breaks on redesigns).
Playwright's locator API exposes multiple ways to target an element, but they are not equally resilient to UI changes. Selectors based on CSS classes and DOM position break the moment a designer renames a class or reorders a list, while selectors based on ARIA roles and user-visible text survive redesigns because they describe the interface contract rather than the implementation. Choosing the right selector strategy is the highest-leverage decision in keeping a test suite green over time.
import { test, expect } from '@playwright/test';
test('selector comparison — brittle vs resilient', async ({ page }) => {
await page.goto('https://demoqa.com/buttons');
// ✗ BRITTLE: CSS class selector — breaks if class name changes
// await page.locator('.btn.btn-primary').click();
// ✗ BRITTLE: XPath by position — breaks if sibling elements are added
// await page.locator('//div[@class="mt-4"]/button[3]').click();
// ✓ BETTER: data-testid — explicit contract between QA and dev
// <button data-testid="submit-order">Submit</button>
// await page.locator('[data-testid="submit-order"]').click();
// ✓ BEST: ARIA role + accessible name — what a screen reader sees
// Resilient to CSS changes AND tests accessibility at the same time
const clickMeBtn = page.getByRole('button', { name: 'Click Me' });
await expect(clickMeBtn).toBeVisible();
await clickMeBtn.click();
console.log('✓ Clicked by ARIA role — resilient to CSS changes');
// ✓ GOOD: get by text content
await page.getByText('Double Click Me').dblclick();
console.log('✓ Double-click by text content');
// ✓ GOOD: get by label (for form inputs)
// await page.getByLabel('Email address').fill('test@example.com');
// Playwright's selector priority recommendation:
// 1. getByRole() — most resilient, tests accessibility
// 2. getByLabel() — for form inputs
// 3. getByText() — for visible text
// 4. getByTestId() — explicit, use when above don't apply
// 5. locator() — CSS/XPath as last resort
});node main.jsdata-testid attribute to be added?page.locator('button').count() on a page. How many buttons are found? This is why page.locator('button').click() often fails — there are multiple matches. Use getByRole('button', { name: 'Submit' }) to narrow it down.data-testid attributes exist on a web app you're testing. If they don't, write a one-paragraph spec for your dev team that explains what data-testid attributes to add to the 5 most important elements. This is a common QA↔Dev collaboration task.<div> testable with getByRole? Why does testing by ARIA role also verify accessibility compliance?Use these three in order. Each builds on the one before.
In one paragraph, explain why CSS class selectors are brittle in automated tests and why ARIA role-based selectors are more resilient. Give a concrete example where a CSS class selector breaks but an ARIA role selector doesn't.
Walk me through what `getByRole('button', { name: 'Submit' })` does under the hood: what HTML attributes and text content does Playwright inspect, and how does it handle a button with `aria-label='Submit form'` vs a button with visible text 'Submit'?
I'm joining a team that has 300 Playwright tests all using CSS selectors like `.primary-btn` and `#main-content > div:nth-child(2)`. The tests break constantly when the design is updated. Propose a migration strategy: how to prioritize which selectors to fix first, what to ask the dev team to add to the codebase, and how to prevent new tests from using brittle selectors.