| name | Playwright Locator filter & Visibility |
| description | Teaches the agent to build resilient Playwright locators with .filter (hasText/has/hasNot), narrow lists, and reason correctly about visibility, waitFor states, and timeouts. |
| version | 1.0.0 |
| author | thetestingacademy |
| license | MIT |
| tags | ["playwright","locator","filter","visibility","waitfor","hastext","resilient-locators","auto-wait"] |
| testingTypes | ["e2e","integration"] |
| frameworks | ["playwright"] |
| languages | ["typescript"] |
| domains | ["web"] |
| agents | ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"] |
Playwright Locator filter & Visibility
This skill makes the agent write locators that survive UI churn: role/label-first selection, narrowed with .filter({ hasText, has, hasNot }) instead of brittle CSS/XPath, and correct reasoning about visibility — knowing when to use the auto-waiting expect(...).toBeVisible() versus the imperative isVisible() / waitFor(). The recurring theme: locators are lazy and re-resolve on every action, and assertions auto-wait, so explicit sleeps and one-shot isVisible() checks are almost always wrong.
Use this skill when a locator is flaky, matches multiple elements, depends on text or a child, or when the agent is tempted to add waitForTimeout.
Core Principles
- Locators are lazy. A
Locator is a query, not an element. It re-resolves every time you act on it, so it tolerates re-renders. Define it once, use it repeatedly.
- Filter, do not concatenate selectors. Start from a semantic locator (
getByRole) and narrow with .filter({ hasText }), .filter({ has }), or .filter({ hasNot }) — not a long CSS chain.
expect(locator).toBeVisible() auto-waits; locator.isVisible() does not. The assertion polls until the timeout; isVisible() returns a boolean right now with no waiting.
- Never assert on a
count() snapshot for dynamic lists. Use await expect(locator).toHaveCount(n) which retries; (await loc.count()) === n is a race.
- Prefer waiting for state over
waitForTimeout. locator.waitFor({ state: 'visible' }) (or just acting on the locator) replaces arbitrary sleeps.
- Scope timeouts at the assertion, not globally — pass
{ timeout } to the specific expect/action that genuinely needs longer.
Workflow / Patterns
Pattern 1 — filter({ hasText }) to pick one row out of many
The classic case: a table/list where rows share structure but differ by text.
import { test, expect } from ;
(, ({ page }) => {
page.();
row = page.().({ : });
(row).();
row.(, { : }).();
refunded = page.().({ : });
(refunded).();
});