The #1 reason tests break after deployments? Brittle locators. A developer changes a CSS class, and suddenly 50 tests fail. Let's fix that.
The Locator Hierarchy
Playwright recommends locators in this priority order:
1. getByRole() → Best: matches how users see the page
2. getByText() → Good: visible text content
3. getByLabel() → Good: form accessibility
4. getByTestId() → Good: explicit test contracts
5. CSS selectors → Okay: when nothing else works
6. XPath → Last resort: complex DOM traversal
Why this order? User-facing locators are stable. If your "Add to Cart" button changes from to , a CSS selector breaks. But getByRole('button', { name: 'Add to Cart' }) keeps working.
Role-Based Locators (Best Practice)
getByRole()
Matches elements by their ARIA role and accessible name:
// Exact match (default)
page.getByRole('button', { name: 'Add to Cart' });
// Case-insensitive
page.getByRole('button', { name: 'add to cart', exact: false });
// Regex pattern
page.getByRole('button', { name: /add to cart/i });
// Partial match
page.getByRole('heading', { name: /checkout/i });
Text-Based Locators
getByText()
Finds elements containing specific text:
// Product name on page
await page.getByText('Wireless Bluetooth Headphones').click();
// Price (be specific to avoid matching multiple prices)
await expect(page.getByText('$129.99')).toBeVisible();
// Status messages
await expect(page.getByText('Item added to cart')).toBeVisible();
await expect(page.getByText('Order confirmed')).toBeVisible();
// Partial text match
await page.getByText('Free shipping', { exact: false }).click();
When you control the application, add explicit test IDs:
// In your application code
<button data-testid="add-to-cart-btn">Add to Cart</button>
<div data-testid="cart-total">$129.99</div>
<span data-testid="cart-count">3</span>
// In your tests
await page.getByTestId('add-to-cart-btn').click();
await expect(page.getByTestId('cart-total')).toHaveText('$129.99');
await expect(page.getByTestId('cart-count')).toHaveText('3');
Custom Test ID Attribute
If your app uses a different attribute:
// playwright.config.ts
export default defineConfig({
use: {
testIdAttribute: 'data-test', // or 'data-cy', 'data-qa'
},
});
// Now getByTestId looks for data-test="..."
await page.getByTestId('product-card').click();
When to Use Test IDs
Use test IDs when:
Element has no accessible role or text
Multiple similar elements exist
You need a stable contract between dev and QA
// Product grid with many similar items
// HTML: <div data-testid="product-card-SKU123">...</div>
await page.getByTestId('product-card-SKU123').click();
// Dynamic content
// HTML: <span data-testid="inventory-count">5 left</span>
await expect(page.getByTestId('inventory-count')).toContainText('left');
Role and text-based locators survived because they match user perception, not implementation.
Debugging Locators
Playwright Inspector
npx playwright test --debug
Opens an inspector where you can:
Hover over elements to see suggested locators
Try locators in the console
Step through tests
Pick Locator Tool
npx playwright codegen https://www.saucedemo.com
Opens a browser where:
Click any element to see recommended locators
Locators are copied to clipboard
Multiple suggestions shown
Locator Console
In UI mode or debug mode:
// In the console
await page.locator('.product-card').count(); // How many matched?
await page.getByRole('button').allTextContents(); // What buttons exist?
await page.locator('.cart-item').evaluateAll(els => els.map(e => e.textContent));
Key Takeaways
Prefer role-based locators: getByRole() matches user perception and survives refactoring
Text locators are stable: Users see text, so text-based locators are durable
Test IDs create contracts: Explicit agreement between devs and QA
CSS/XPath are fallbacks: Use only when semantic locators fail
Chain for precision: Narrow down with .filter() and nested locators
Use codegen: Let Playwright suggest the best locators
Next Lesson
You've learned to find elements reliably. But what happens after you find them? In the next lesson, we'll explore auto-wait and web-first assertions - how Playwright ensures your actions succeed without manual waits.