QA How-To
How to Use Playwright locator filter (2026)
Learn playwright locator filter with hasText, has, hasNot, visible, chaining, strictness, runnable TypeScript, debugging, and interview answers for CI.
21 min read | 2,921 words
TL;DR
Call locator.filter() on a collection of cards, rows, or list items, then narrow by hasText, has, hasNotText, hasNot, or visible. Keep inner locators relative and in the same frame, and assert uniqueness when it is part of the contract.
Key Takeaways
- Start with a semantic outer collection, then filter each candidate by business identity.
- Use hasText for descendant text and has for a structured relative locator.
- Keep outer and inner locators within the same frame and candidate scope.
- Treat negative filters cautiously because they can admit unexpected new states.
- Use visible filtering only when visibility is a legitimate selection criterion.
- Let strictness expose ambiguous matches instead of defaulting to first().
- Use toHaveCount() and loaded-state assertions for dynamic collections.
Playwright locator.filter() narrows an existing locator by descendant text, the presence or absence of another relative locator, or visibility. A dependable playwright locator filter starts with a meaningful collection such as cards, rows, or list items, then keeps only the members that satisfy the product relationship you care about.
Filtering is especially valuable when a page repeats the same component and the unique identity lives inside each component. This guide explains every current option, relative matching rules, strictness, chaining, debugging, performance, page objects, and interview-ready tradeoffs with runnable TypeScript.
TL;DR
const productCard = page.getByRole('article').filter({
has: page.getByRole('heading', { name: 'Trail Backpack' }),
});
await productCard.getByRole('button', { name: 'Add to cart' }).click();
| Option | Keeps outer elements that... | Value |
|---|---|---|
hasText |
Contain matching descendant text | string or RegExp |
hasNotText |
Do not contain matching descendant text | string or RegExp |
has |
Contain a match for a relative inner locator | Locator |
hasNot |
Do not contain a match for a relative inner locator | Locator |
visible |
Match the requested visible or invisible state | boolean |
Text strings use case-insensitive substring matching. Use a bounded regular expression when precision matters. Inner locators for has and hasNot are evaluated relative to each outer candidate, must stay in the same frame, and cannot contain a FrameLocator.
1. What playwright locator filter Does
A Locator represents a query that Playwright resolves when an action or assertion runs. filter() returns another Locator, so it preserves lazy resolution, automatic waiting in actions, and resilience to DOM replacement. It does not take a one-time snapshot of matching elements.
Start with an outer collection:
const rows = page.getByRole('row');
Then describe which row matters:
const paidRow = rows.filter({
hasText: 'Invoice 1042',
});
The filter evaluates each row's descendants and retains matching rows. That relationship is the central benefit. A page-wide text locator can find the invoice label, but it does not automatically identify the correct row whose action button should be clicked.
filter() can accept more than one option, and those constraints work together. Chained filter calls also narrow the current result further. In logical terms, normal filter chaining is an AND operation.
const target = page.getByRole('row')
.filter({ hasText: 'Invoice 1042' })
.filter({ hasNotText: 'Cancelled' });
Filtering does not waive strictness. A click still requires one matching target. If two rows contain the same invoice phrase, Playwright should report the ambiguity rather than silently choose one. Use additional product identity, assert count, or clarify the UI contract.
Prefer filtering containers, not arbitrary ancestors. article, row, listitem, and named regions communicate structure. A long chain of div locators may work today but couples the test to markup that users do not recognize.
2. Build a Runnable Filtered Locator Test
Initialize Playwright Test if needed:
npm init playwright@latest
npx playwright install
Save this example as tests/filter-card.spec.ts:
import { test, expect } from '@playwright/test';
test('adds the intended product from a repeated card list', async ({ page }) => {
await page.setContent(`
<main>
<article>
<h2>City Backpack</h2>
<p>Capacity: 18 L</p>
<button>Add to cart</button>
</article>
<article>
<h2>Trail Backpack</h2>
<p>Capacity: 32 L</p>
<button>Add to cart</button>
</article>
<p role="status">Cart is empty</p>
</main>
<script>
document.querySelectorAll("article").forEach(card => {
card.querySelector("button").addEventListener("click", () => {
document.querySelector("[role=status]").textContent =
card.querySelector("h2").textContent + " added";
});
});
</script>
`);
const trailCard = page.getByRole('article').filter({
has: page.getByRole('heading', { name: 'Trail Backpack' }),
});
await expect(trailCard).toHaveCount(1);
await trailCard.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByRole('status'))
.toHaveText('Trail Backpack added');
});
Run it with npx playwright test tests/filter-card.spec.ts. The heading is evaluated within each article candidate. After the correct article is found, the button lookup is scoped to that article.
The explicit count assertion is useful when unique product identity is part of the test contract. It makes duplicate data fail before an action produces a strictness error. Do not add toHaveCount(1) to every locator by habit, since actions already enforce strictness when they require one element.
Replace setContent() with page.goto() in an application suite. Keep the structure: semantic outer collection, business-specific filter, scoped action, and user-visible result.
3. Filter by Text with hasText and hasNotText
hasText matches text anywhere within each outer element, including descendants. A string performs case-insensitive substring matching. A JavaScript regular expression gives explicit control over boundaries, alternatives, and case.
const cards = page.getByRole('article');
const anyPlaywrightCard = cards.filter({
hasText: 'playwright',
});
const exactTitleCard = cards.filter({
hasText: /^Playwright Locator Design$/i,
});
The regular expression applies to the text content considered for the outer element. Because a card may include title, metadata, and buttons, a whole-card anchored expression can be too strict. When exact identity belongs to a heading, use has with a precise heading locator instead.
hasNotText removes containers containing unwanted text:
const activeOrders = page.getByRole('row')
.filter({ hasText: /Order #[0-9]+/ })
.filter({ hasNotText: 'Cancelled' });
Negative filtering is useful, but a positive state is usually easier to reason about. If rows expose Active, filter for it rather than merely excluding Cancelled, because other states such as Pending or Failed might otherwise remain.
Text matching is best when the text itself is the product identity and can appear anywhere in the component. Use has when the location or semantics of the text matter. A card containing the word "Pro" in its description is not necessarily the card with a heading exactly named "Pro".
Avoid giant regular expressions that accept several unrelated copies. They turn product changes into silent passes. Use the least flexible match that represents the requirement.
4. Filter by Descendant Locator with has
has keeps an outer candidate when a relative inner locator matches inside it. The inner locator can use role, label, text, test ID, CSS, or another supported locator strategy.
const row = page.getByRole('row').filter({
has: page.getByRole('cell', { name: 'qa@example.com', exact: true }),
});
await row.getByRole('button', { name: 'Edit user' }).click();
The key word is relative. Playwright evaluates the inner locator starting from each outer candidate, not from the document root. This makes the relationship readable: choose the row that contains the named cell.
The inner locator must not depend on elements outside the outer candidate. Suppose an article contains a content element. Filtering content with an inner locator that begins at the parent article cannot work, because evaluating it would need to climb outside the candidate. Choose the article as the outer locator or build the inner query entirely beneath content.
Outer and inner locators must belong to the same frame, and the inner locator cannot contain a FrameLocator. To work inside an iframe, first enter the frame with frameLocator(), create both locators there, and filter within that document.
const frame = page.frameLocator('iframe[title="Orders"]');
const order = frame.getByRole('row').filter({
has: frame.getByRole('cell', { name: 'Order 1042' }),
});
This pattern keeps both locators in the same frame. For component relationships on the main page, has is often clearer than CSS :has() because it composes Playwright's user-facing locators and appears clearly in traces.
For locator priority and accessible identity, see the Playwright locator best practices guide.
5. Exclude Descendants with hasNot and hasNotText
hasNot keeps an outer element only when the relative inner locator does not match inside it. It is useful for excluding disabled variants, archived records, warning badges, or optional controls.
const selectablePlans = page.getByRole('article').filter({
hasNot: page.getByText('Unavailable', { exact: true }),
});
hasNotText works at the text level:
const nonArchivedProjects = page.getByRole('listitem').filter({
hasNotText: 'Archived',
});
Choose based on contract. If Archived is a status badge with a stable role or test ID, hasNot can express that structure. If any descendant copy is sufficient, hasNotText is concise.
A negative condition describes what is absent, so it can unintentionally retain new states. Imagine plans can be Available, Sold out, or Invite only. Excluding Sold out still includes Invite only. If the test requires Available, a positive filter for that state is safer.
Combine positive and negative constraints when both matter:
const target = page.getByRole('article').filter({
has: page.getByRole('heading', { name: 'Team Plan' }),
hasNot: page.getByText('Legacy', { exact: true }),
});
Both conditions refer to each candidate article. Keep the number of constraints small enough that a reviewer can understand the selection. When a component has a durable accessible name, a direct role locator may be simpler than several exclusions.
Negative filters can be excellent assertions too. await expect(list.filter({ hasText: 'Error' })).toHaveCount(0) verifies absence across a stable list. Make sure the list has loaded first, otherwise an empty page can create a false pass.
6. Use visible Filtering Deliberately
Current Playwright releases support filter({ visible: true }) and filter({ visible: false }). Visibility filtering is useful when the DOM intentionally contains duplicate active and inactive variants, such as responsive controls or transition states.
const visibleContinue = page.locator('button')
.filter({ hasText: 'Continue', visible: true });
await visibleContinue.click();
Visibility is not a substitute for strong identity. If two visible Continue buttons exist, the locator remains ambiguous. If a role locator already excludes elements hidden from the accessibility tree, adding a visible filter may be redundant.
Use it when visibility is genuinely part of selection:
await expect(
page.locator('[data-panel]').filter({ visible: false })
).toHaveCount(2);
This can validate a component that keeps inactive panels in the DOM. For a normal interaction, a named tab and its controlled panel may communicate intent better.
Do not use visibility filtering to ignore an unexpected duplicate caused by a rendering defect. First determine whether both nodes are legitimate. Responsive layouts sometimes render desktop and mobile navigation simultaneously, but a well-designed test should still scope the correct region when that region has a semantic identity.
Visibility can change between locator creation and action because locators are lazy. That is generally beneficial. However, a transitioning UI may briefly match different variants. Wait for a stable application signal, such as the intended dialog or responsive layout state, rather than relying on a coincidental moment.
7. Chain Filters and Combine Locator Operations
Chained filters narrow the same result set. This is useful when each line names one independent criterion.
const invoice = page.getByRole('row')
.filter({ hasText: 'Invoice 1042' })
.filter({ hasText: 'Paid' })
.filter({ hasNotText: 'Refunded' });
You can also provide compatible constraints in one call. Choose the form that reads most clearly. Chaining does not mean Playwright first freezes a list and then filters JavaScript arrays. The result remains a locator query.
Compare related operations:
| Operation | Meaning | Typical use |
|---|---|---|
filter({ hasText }) |
Outer element contains text | Card or row identity |
filter({ has }) |
Outer element contains relative locator | Semantic descendant relationship |
locator.and(other) |
Element matches both locator queries | Intersect two identities for the same node |
locator.or(other) |
Element matches either query | Alternative UI states |
locator.locator(child) |
Find descendants of current matches | Act within a chosen container |
nth(index) |
Select by zero-based position | Order is explicitly the contract |
Use filter() for container-to-descendant relationships. Use and() when the same element must satisfy both locators. After filtering a card, use a descendant locator to find its action.
const card = page.getByRole('article').filter({
has: page.getByRole('heading', { name: 'Enterprise' }),
});
const upgrade = card.getByRole('button', { name: 'Upgrade' });
Avoid ending every ambiguous locator with first() or nth(). Position is correct only when order is the behavior under test. The Playwright locator nth guide explains that tradeoff.
8. Debug playwright locator filter Strictness
A filtered locator can match zero, one, or many elements. Inspect the selection before changing it:
const candidates = page.getByRole('article').filter({
hasText: 'Starter',
});
await expect(candidates).toHaveCount(1);
If count is zero, check the outer collection first. Does page.getByRole('article') match the rendered components? Then check the filter value, frame, final text, and whether the desired descendant is actually inside each outer candidate.
If count is greater than one, determine whether the product legitimately repeats identity. Scope the outer locator to a named section, filter with a more precise descendant, or use unique scenario data. Do not immediately add first().
Playwright Inspector helps test locators against the current page:
npx playwright test tests/catalog.spec.ts --debug
Trace Viewer is better for CI-only failures because it preserves DOM snapshots and action timing. Look for hidden responsive copies, stale loading skeletons, localization changes, and status badges that alter combined card text.
Classify common failures:
- Wrong outer locator: the desired text exists, but not under the chosen card or row.
- Relative locator escapes: the
hasquery depends on an ancestor outside the candidate. - Cross-frame relationship: outer and inner locators belong to different frames.
- Broad substring: several containers include the same short text.
- Negative false positive: a new state is not excluded.
- Dynamic list race: a count was read before loading completed.
- Visibility workaround: an unexpected duplicate was hidden instead of explained.
A strictness error is useful. It prevents the test from acting on an arbitrary matching component and exposes ambiguous product identity early.
9. Wait for Dynamic Lists and Filtered Results
filter() itself does not wait for a list to reach a business-complete state. Actions on the resulting locator auto-wait for actionability, and locator assertions retry, but you still need an assertion that represents list readiness.
const results = page.getByRole('listitem');
await expect(results).toHaveCount(5);
const matchingResult = results.filter({
has: page.getByRole('heading', { name: 'Playwright Engineer' }),
});
await expect(matchingResult).toHaveCount(1);
The first assertion is appropriate only if five results are guaranteed. Otherwise, wait for a loading indicator to disappear and positively assert the desired result.
Avoid this snapshot pattern on a changing list:
const count = await matchingResult.count();
expect(count).toBe(1);
count() reads the current number once. toHaveCount(1) retries until the expected state or timeout. Snapshot reads remain useful for diagnostics and controlled iteration after stability is established.
If a filter depends on content loaded by an API, assert the UI rather than sleeping for the expected network duration. A response promise can coordinate the request boundary when necessary, but the filtered locator assertion proves rendering completed.
The Playwright waiting for visible and stable elements guide covers why observable state is more reliable than fixed delays.
10. Design Maintainable Filter Helpers and Page Objects
A page object can expose a locator factory whose argument is domain identity:
import { type Locator, type Page } from '@playwright/test';
export class UsersPage {
constructor(private readonly page: Page) {}
userRow(email: string): Locator {
return this.page.getByRole('row').filter({
has: this.page.getByRole('cell', { name: email, exact: true }),
});
}
async editUser(email: string): Promise<void> {
await this.userRow(email)
.getByRole('button', { name: 'Edit user' })
.click();
}
}
This helper retains the semantic relationship and returns a Locator, so the caller can add scenario-specific assertions. It does not capture an element handle or return a raw array.
Avoid generic abstractions such as filterByText(locator, text). They merely rename the API and hide whether substring text is strong enough. Domain functions such as orderRow(orderNumber) or planCard(planName) communicate why a filter is valid.
Keep inner locators near the outer container. A page-wide locator stored in an unrelated class can still be evaluated relatively when passed to has, but the code becomes harder to review. Group component relationships in one page object or component object.
When identity is already available as an accessible name, use a direct role locator. Filtering is a tool for repeated containers, not a requirement for every page object method. Prefer the shortest locator that expresses a durable user or product contract.
11. Review Performance, Readability, and Selector Risk
Locator performance problems are usually selector design problems, not reasons to abandon semantic locators. Begin with a reasonably narrow outer collection, then apply a specific filter. Searching every div on a large page and evaluating several broad text expressions creates unnecessary work and unreadable traces.
Review a filtered locator with these questions:
- Does the outer locator describe the component boundary?
- Does the filter express real identity or merely current copy?
- Is a string substring too broad?
- Does the inner locator stay within the candidate and frame?
- Can a positive state replace a fragile negative condition?
- Would a direct role or label locator be simpler?
- Is position being used where semantic identity exists?
- Does the test assert uniqueness when uniqueness matters?
- Is list readiness proven before an absence assertion?
- Would a localization or responsive change alter the intended contract?
Use test IDs when the product lacks stable user-facing identity and the team owns a documented automation contract. A test ID on the card plus semantic locators inside it can be clearer than a complex text filter.
Do not embed CSS layout paths inside has unless structure is genuinely the contract. Filter's strength is the ability to compose readable locator strategies. Preserve that advantage so failures tell the reviewer which product relationship changed.
Interview Questions and Answers
Q: What does locator.filter() return?
It returns a new Locator narrowed by the supplied conditions. The result remains lazy, so Playwright resolves it when an action or assertion runs. It can be chained and still participates in normal locator strictness.
Q: What is the difference between hasText and has?
hasText checks descendant text anywhere inside each outer candidate. has checks whether a relative inner Locator matches inside each candidate, so it can express role, label, test ID, or structured identity. I use has when the descendant's semantics matter.
Q: What restrictions apply to the has locator?
The inner locator is evaluated relative to each outer candidate. It cannot depend on nodes outside that candidate, must belong to the same frame as the outer locator, and cannot include a FrameLocator.
Q: How do multiple filters combine?
Normal chained filters narrow the current matches, so they behave as logical AND conditions. Multiple compatible options in one filter call also apply to each candidate. I keep each criterion readable and avoid over-constrained locator puzzles.
Q: When should visible filtering be used?
I use it when visible or hidden state is intentionally part of selecting among legitimate DOM variants. I do not use it to hide an unexplained duplicate or replace semantic scoping.
Q: Why is nth() usually weaker than filter()?
nth() selects by position, which can silently change when sorting or data changes. filter() can select by business identity inside a component. Position is valid when order itself is the requirement.
Q: How do you debug a strictness violation after filtering?
I assert or inspect the filtered count, verify the outer collection, and identify which text or descendant makes multiple candidates match. Then I add semantic scope or precise identity, rather than choosing the first result.
Common Mistakes
- Starting from every
divinstead of a meaningful card, row, list item, or region. - Assuming a string
hasTextmatch is exact when it is a case-insensitive substring. - Passing an inner
haslocator that relies on an ancestor outside the outer candidate. - Combining outer and inner locators from different frames.
- Using
hasNotTextas a positive state model and accidentally accepting new states. - Adding
visible: trueto conceal an unintended duplicate. - Ending an ambiguous filter with
first()without proving order is relevant. - Reading
count()once while a dynamic list is still loading. - Asserting zero matches before the page has reached a loaded state.
- Building a large regular expression that tolerates unrelated content changes.
- Wrapping
filter()in generic helpers that remove domain meaning. - Treating a filtered locator as an element snapshot instead of a lazy query.
A practical fix is to restate the locator in plain English. "The order row that contains the exact order number and an active status" maps naturally to an outer row plus two positive descendant conditions. If the sentence instead says "the third row that currently looks right," the selection probably needs a stronger product identity.
Conclusion
Use playwright locator filter to identify a repeated container by what it contains or excludes. Choose hasText for broad descendant text, has for semantic descendant relationships, negative options with care, and visible only when visibility is a legitimate selection dimension.
Keep outer and inner locators in the same frame, preserve relative scope, and let strictness expose ambiguity. Start by replacing one positional card or row selector with a semantic filter, then assert the chosen component's business outcome.
Interview Questions and Answers
What is Playwright locator.filter()?
It creates a new Locator narrowed by conditions on each outer match. The result remains lazy and participates in normal auto-waiting and strictness. It is especially useful for repeated cards, rows, and list items.
How do hasText and has differ?
hasText searches descendant text, while has evaluates a relative inner Locator within each candidate. I use has when the descendant's role or structure is part of the contract.
What are the relative locator rules for has?
The inner locator starts from each outer candidate, cannot rely on nodes outside it, must be in the same frame, and cannot include a FrameLocator. Violating those boundaries produces a locator that cannot express the intended relationship.
How do chained filters combine?
Each filter further narrows the current matches, so the normal relationship is logical AND. I keep each condition independent and readable, and simplify to a direct locator when possible.
When would you use hasNot or hasNotText?
I use them to exclude a legitimate variant such as archived or unavailable items. I prefer a positive required state when one exists because negative filters can admit unexpected new states.
How do you handle multiple matches after filtering?
I inspect or assert the count, then add semantic page scope or a more precise descendant identity. I do not add first() unless the first position itself is the documented behavior.
How do you synchronize a filter against a dynamic list?
I wait for a business-ready UI signal or use a retrying locator assertion such as toHaveCount(). I avoid taking a one-time count before the list finishes rendering and avoid fixed sleeps.
Frequently Asked Questions
What does Playwright locator filter do?
It returns a new Locator containing only outer matches that satisfy the specified descendant text, relative locator, exclusion, or visibility conditions. The locator remains lazy and resolves during actions and assertions.
Is Playwright hasText exact?
A string hasText value uses case-insensitive substring matching. Use a carefully bounded regular expression or a has locator with an exact semantic descendant when exact identity matters.
How does filter has work in Playwright?
The inner locator is evaluated relative to each outer candidate. A candidate remains only if that inner locator matches within it, and both locators must be in the same frame.
Can I combine multiple Playwright filters?
Yes. Chain filter calls or provide compatible options in one call. The constraints narrow the same candidates and normally behave as logical AND conditions.
How do I filter visible elements in Playwright?
Use locator.filter({ visible: true }) for visible matches or visible: false for invisible matches. Use it only when visibility is an intentional part of identity, not to hide unexplained duplicates.
Why does a filtered locator still have a strictness error?
Filtering can still leave multiple matches. Add meaningful scope or identity, or assert a unique count, instead of silently selecting first() unless order is the requirement.
Can filter has cross an iframe?
No. Outer and inner locators must belong to the same frame, and the inner locator cannot contain a FrameLocator. Enter the frame first and build both locators inside it.