Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
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.
data-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?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.js