Files
magnus919_agent-skills/playwright/references/02-selectors.md
T
Magnus HedemarkGitHubfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
ac1beb117d feat(skill): add Playwright skill (E2E testing + scraping + headless browsing) (#264)
Add ONE tool skill for Playwright: SKILL.md covering E2E test authoring,
selector robustness, network interception/mocking, parallel workers, CI
integration, scraping/headless patterns, accessibility snapshot checks, and
headed debugging; scripts/pwrun (agent-first smoke harness with --json,
fixture-tested); templates/ test-suite scaffold; eight dated references; a
schema-valid evals/evals.json (6 cases); a human-facing README; reverse
routing from qa-methodology and frontend-engineering; top-level README index
entry; and regenerated catalogs (llms.txt, marketplace, codex).

Closes #244.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 17:59:38 -04:00

3.2 KiB

Selector Robustness

Last Updated: 2026-08-03

Selectors are the #1 source of E2E flakiness. The goal is locators that describe what the element is (its role in the user experience), not where it happens to be in the DOM.

Locator priority

Use, in order of preference:

  1. Rolepage.getByRole('button', { name: 'Save' }). Mirrors how the page is presented to assistive tech and users; survives markup changes.
  2. Label / placeholder / textgetByLabel('Email'), getByPlaceholder('Search'), getByText('Saved', { exact: true }).
  3. Test idgetByTestId('checkout-form'). For elements whose role/label does not describe them (e.g., a decorative SVG, a canvas region). Test ids exist only for tests; agree on a naming convention.
  4. CSS / XPath — last resort: layout-adjacent queries that role and label cannot express (e.g., "the third row of a table" is better done with getByRole('row').nth(2)).

Composition over long strings

Chain and filter instead of concatenating brittle paths:

// Fragile: encodes nesting and order.
page.locator('div.product-card div.price span').click();

// Robust: describe the card by its visible content, then act within it.
const card = page.getByRole('article').filter({ hasText: 'Running shoes' });
await card.getByRole('button', { name: 'Add to cart' }).click();
  • filter({ hasText }) / filter({ has: locator }) narrow a collection.
  • first(), last(), nth(n) are code smells unless the ordering is the assertion (e.g., a sort test).

The repair loop

A flaky test is a bug report about your selectors, not a request for more waitForTimeout. When a test passes sometimes:

  1. Run the spec alone (npx playwright test <spec> --workers=1 --repeat-each=5) to measure flakiness deterministically.
  2. Use --debug or the trace to see what the failing action actually resolved. Common causes:
    • Zero matches — the element appears late (async render): use a web-first assertion or wait for its container, not a sleep.
    • Multiple matches — your locator is too generic: narrow with filter({ hasText }) or scope to a container.
    • Stale node — the element is re-rendered between lookup and action: re-query instead of storing a handle, or assert the new state after the re-render.
  3. Fix the locator to describe the unique user-facing element, then re-run the repeat-each loop until it is green 5/5.

Anti-patterns to avoid

  • page.waitForTimeout() — masks races, slows the suite.
  • page.waitForSelector() + manual click() — duplicates what locator.click() already does with retries.
  • Snapshots of CSS classes (expect(el).toHaveClass(...)) for behavioral assertions — classes are implementation details.
  • Text that duplicates across the page without disambiguation (getByText('Submit') matches 2 buttons) — add { exact: true } or scope.
  • XPath with positional predicates (//div[2]/span[1]) — order and structure change; roles do not.
  • Authoring structure and fixtures: 01-e2e-authoring.md.
  • Emulating slow networks to surface timing bugs: 03-network-interception-and-mocking.md.