Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
core/locators.md
1# Locator Strategies23## Table of Contents451. [Priority Order](#priority-order)62. [User-Facing Locators](#user-facing-locators)73. [Filtering & Chaining](#filtering--chaining)84. [Dynamic Content](#dynamic-content)95. [Shadow DOM](#shadow-dom)106. [Iframes](#iframes)1112## Priority Order1314Use locators in this order of preference:15161. **Role-based** (most resilient): `getByRole`172. **Label-based**: `getByLabel`, `getByPlaceholder`183. **Text-based**: `getByText`, `getByTitle`194. **Test IDs** (when semantic locators aren't possible): `getByTestId`205. **CSS/XPath** (last resort): `locator('css=...')`, `locator('xpath=...')`2122## User-Facing Locators2324### getByRole2526Most robust approach - matches how users and assistive technology perceive the page.2728```typescript29// Buttons30page.getByRole("button", { name: "Submit", exact: true }); // exact accessible name31page.getByRole("button", { name: /submit/i }); // flexible case-insensitive match3233// Links34page.getByRole("link", { name: "Home" });3536// Form elements37page.getByRole("textbox", { name: "Email" });38page.getByRole("checkbox", { name: "Remember me" });39page.getByRole("combobox", { name: "Country" });40page.getByRole("radio", { name: "Option A" });4142// Headings43page.getByRole("heading", { name: "Welcome", level: 1 });4445// Lists & items46page.getByRole("list").getByRole("listitem");4748// Navigation & regions49page.getByRole("navigation");50page.getByRole("main");51page.getByRole("dialog");52page.getByRole("alert");53```5455### getByLabel5657For form elements with associated labels.5859```typescript60// Input with <label for="email">61page.getByLabel("Email address");6263// Input with aria-label64page.getByLabel("Search");6566// Exact match67page.getByLabel("Email", { exact: true });68```6970### getByPlaceholder7172```typescript73page.getByPlaceholder("Enter your email");74page.getByPlaceholder(/email/i);75```7677### getByText7879```typescript80// Partial match (default)81page.getByText("Welcome");8283// Exact match84page.getByText("Welcome to our site", { exact: true });8586// Regex87page.getByText(/welcome/i);88```8990### getByTestId9192Configure custom test ID attribute in `playwright.config.ts`:9394```typescript95use: {96testIdAttribute: "data-testid"; // default97}98```99100Usage:101102```typescript103// HTML: <button data-testid="submit-btn">Submit</button>104page.getByTestId("submit-btn");105```106107## Filtering & Chaining108109### filter()110111Narrow down locators:112113```typescript114// Filter by text115page.getByRole("listitem").filter({ hasText: "Product" });116117// Filter by NOT having text118page.getByRole("listitem").filter({ hasNotText: "Out of stock" });119120// Filter by child locator121page.getByRole("listitem").filter({122has: page.getByRole("button", { name: "Buy" }),123});124125// Combine filters126page127.getByRole("listitem")128.filter({ hasText: "Product" })129.filter({ has: page.getByText("$9.99") });130```131132### Chaining133134```typescript135// Navigate down the DOM tree136page.getByRole("article").getByRole("heading");137138// Get parent/ancestor139page.getByText("Child").locator("..");140page.getByText("Child").locator("xpath=ancestor::article");141```142143### nth() and first()/last()144145```typescript146page.getByRole("listitem").first();147page.getByRole("listitem").last();148page.getByRole("listitem").nth(2); // 0-indexed149```150151## Dynamic Content152153### Waiting for Elements154155Locators auto-wait for actionability by default. For explicit state waiting:156157```typescript158await page.getByRole("button").waitFor({ state: "visible" });159await page.getByText("Loading").waitFor({ state: "hidden" });160```161162> **For comprehensive waiting strategies** (element state, navigation, network, polling with `toPass()`), see [assertions-waiting.md](assertions-waiting.md#waiting-strategies).163164### Lists with Dynamic Items165166```typescript167// Wait for specific count168await expect(page.getByRole("listitem")).toHaveCount(5);169170// Get all matching elements171const items = await page.getByRole("listitem").all();172for (const item of items) {173await expect(item).toBeVisible();174}175```176177## Shadow DOM178179Playwright pierces shadow DOM by default:180181```typescript182// Automatically finds elements inside shadow roots183page.getByRole("button", { name: "Shadow Button" });184185// Explicit shadow DOM traversal (if needed)186page.locator("my-component").locator("internal:shadow=button");187```188189## Iframes190191```typescript192// By frame name or URL193const frame = page.frameLocator('iframe[name="content"]');194await frame.getByRole("button").click();195196// By index197const frame = page.frameLocator("iframe").first();198199// Nested iframes200const nestedFrame = page.frameLocator("#outer").frameLocator("#inner");201await nestedFrame.getByText("Content").click();202```203204## Debugging Locators205206```typescript207// Highlight element in headed mode208await page.getByRole("button").highlight();209210// Count matches211const count = await page.getByRole("listitem").count();212213// Check if exists without waiting214const exists = (await page.getByRole("button").count()) > 0;215216// Use Playwright Inspector217// PWDEBUG=1 npx playwright test218```219220## Common Issues & Solutions221222| Issue | Solution |223| ----------------------- | ------------------------------------------------ |224| Multiple elements match | Add filters or use `nth()`, `first()`, `last()` |225| Element not found | Check visibility, wait for load, verify selector |226| Stale element | Locators are lazy; re-query if DOM changes |227| Dynamic IDs | Use stable attributes like role, text, test-id |228| Hidden elements | Use `{ force: true }` only when necessary |229230## Anti-Patterns to Avoid231232| Anti-Pattern | Problem | Solution |233| --------------------------------- | --------------------------------- | ------------------------------------------------- |234| `page.locator('.btn-primary')` | Brittle, implementation-dependent | `page.getByRole('button', { name: 'Submit' })` |235| `page.locator('#dynamic-id-123')` | Breaks when IDs change | Use stable attributes like role, text, or test-id |236| Testing implementation details | Breaks on refactoring | Test user-visible behavior |237238## Related References239240- **Debugging selector issues**: See [debugging.md](../debugging/debugging.md) for troubleshooting241- **Waiting for elements**: See [assertions-waiting.md](assertions-waiting.md) for waiting strategies242- **Using in Page Objects**: See [page-object-model.md](page-object-model.md) for organizing locators243