QA How-To
How to Fix Playwright strict mode violation multiple elements
Fix Playwright strict mode violation multiple elements with unique role locators, container scope, filters, list assertions, and clear collection handling.
25 min read | 2,975 words
TL;DR
To fix Playwright strict mode violation multiple elements, determine every node matched by the locator, then express the missing user intent. Add an accessible name, scope within the correct dialog, row, card, or navigation, or filter by related content. If multiple elements are expected, use a collection assertion or iteration instead of a single-target action.
Key Takeaways
- Strict mode fails single-element operations when a locator matches more than one node because Playwright cannot infer user intent.
- Inspect the complete match set before narrowing so hidden duplicates, repeated components, and accessibility problems remain visible.
- Prefer role plus accessible name, meaningful container scope, and relational filters over DOM position.
- Use first, last, or nth only when order is an explicit product requirement and assert that requirement.
- Handle collections with toHaveCount, array forms of toHaveText, allTextContents, or controlled iteration.
- Treat duplicate test IDs or duplicate accessible controls as potential application defects, not only test-code problems.
- Be careful with locator.or because it can match both alternatives and become strict when both are present.
To fix playwright strict mode violation multiple elements, make the locator uniquely identify the control required by a single-element action, or rewrite the test to handle the matched collection intentionally. Playwright refuses to guess which of several matching nodes should receive a click, fill, or single-element assertion.
The error is useful. It exposes an ambiguous selector, repeated UI, responsive duplicate, hidden template, or missing accessibility distinction before the test silently chooses the wrong node. This guide provides a repeatable triage process, runnable TypeScript examples, collection patterns, table and card strategies, frame guidance, and review rules that preserve user intent.
TL;DR
| Test intent | Correct strategy | Example |
|---|---|---|
| Click one uniquely named button | Role plus accessible name | getByRole('button', { name: 'Save profile' }) |
| Click within one dialog | Scope to named dialog | dialog.getByRole('button', { name: 'Save' }) |
| Select a card by its heading | Filter parent by child | cards.filter({ has: heading }) |
| Select a row by unique text | Filter row, then locate action | rows.filter({ hasText: 'INV-1042' }) |
| Verify number of repeated items | Collection assertion | await expect(items).toHaveCount(5) |
| Verify ordered item labels | Array text assertion | await expect(items).toHaveText(['A', 'B']) |
| Click the third item by requirement | Positional locator plus order assertion | items.nth(2) |
| Accept either of two UI branches | or plus explicit disambiguation |
Assert which branch is active |
Do not make first() your automatic fix. It converts ambiguity into dependence on DOM order and can let the test pass against the wrong control.
1. What Playwright Strict Mode Means
A Playwright Locator can represent zero, one, or many elements. That flexibility supports lists and reusable queries. Strictness applies when an operation implies exactly one target. click(), fill(), press(), boundingBox(), and single-target assertions such as toBeVisible() must know which element to use. If the locator resolves to multiple nodes, Playwright throws a strict mode violation.
Operations designed for collections are different. count(), allTextContents(), evaluateAll(), and collection assertions can work with several matches. The locator itself is not invalid simply because it represents a list. The operation and the test's intent determine whether uniqueness is required.
A typical message lists matching nodes and selector hints. Read every listed candidate. They may reveal two buttons with the same accessible name, a desktop and mobile copy, a header and modal action, or a text locator whose substring also matches a longer label.
Strictness is safer than an implicit first-match policy. Without it, a layout change could reorder nodes and send a destructive action to a different control while the test still passes. Your repair should add the missing business distinction, not merely suppress the guardrail.
2. Why One Locator Matches Multiple Elements
Ambiguity comes from both test design and application structure. The most common sources are broad text, repeated components, duplicate test IDs, hidden responsive markup, open and closed overlays, and accessible controls with identical names.
| Cause | Example | Better distinction |
|---|---|---|
| Broad role only | Every button on page |
Add accessible name and container |
| Partial text | Submit and Submit order |
Use exact accessible name when appropriate |
| Repeated cards | Ten Add to cart buttons |
Locate product card, then button |
| Repeated table rows | Several Edit links |
Locate row by business key, then link |
| Responsive duplicates | Desktop and mobile navigation | Scope to active named navigation |
| Duplicate test IDs | Two data-testid="save" nodes |
Fix product contract or scope during migration |
| Multiple dialogs | Background and foreground dialog | Locate named visible dialog |
| Alternative branches | Both sides of locator.or() appear |
Choose branch explicitly or narrow result |
| Hidden templates | Mounted component copies | Target active component and review lifecycle |
The locator count is an observation, not the entire diagnosis. Two identical Delete buttons may be expected in a list, while two identical Confirm deletion buttons inside one dialog may be a product bug. Connect cardinality to the business state.
The Playwright getByTestId guide explains how test ID contracts should be designed and why uniqueness still matters for single controls.
3. Fastest Way to Fix Playwright strict mode violation multiple elements
Reproduce the failure with the exact locator and inspect the candidates. Then add one meaningful dimension at a time.
import { test, expect } from '@playwright/test';
test('updates the shipping address', async ({ page }) => {
await page.setContent(`
<main>
<section aria-labelledby="billing-title">
<h2 id="billing-title">Billing address</h2>
<button type="button">Edit</button>
</section>
<section aria-labelledby="shipping-title">
<h2 id="shipping-title">Shipping address</h2>
<button type="button">Edit</button>
</section>
</main>
`);
const sections = page.locator('section');
const shipping = sections.filter({
has: page.getByRole('heading', { name: 'Shipping address' }),
});
const edit = shipping.getByRole('button', { name: 'Edit' });
await expect(shipping).toHaveCount(1);
await expect(edit).toBeVisible();
await edit.click();
});
A broad page.getByRole('button', { name: 'Edit' }) matches both buttons. The repaired locator starts with the business entity and then finds its action. This reads naturally and survives unrelated sections being inserted.
During triage, print await locator.count() and await locator.allTextContents(), or use the locator picker in Playwright Inspector. Remove temporary logging after the selector communicates the distinction. Run the focused test under the viewport and account state that failed because responsive or permission-specific copies can change the match set.
4. Prefer Role and Accessible Name
Role locators combine a control's semantic role with its computed accessible name. They model how users and assistive technology identify controls and often provide the missing uniqueness without implementation-specific selectors.
const saveProfile = page.getByRole('button', {
name: 'Save profile',
exact: true,
});
await saveProfile.click();
The exact option is useful when Save also matches Save draft or Save profile. Do not add it mechanically. Accessible names can include normalized whitespace and labels that product copy intentionally changes. Choose the name that is part of the user contract.
Labels are ideal for form controls:
await page.getByLabel('Work email', { exact: true }).fill('qa@example.com');
If two visible buttons in the same context genuinely have the same role and accessible name but perform different actions, that is an accessibility concern. A sighted user might distinguish them by nearby context, but a screen reader user may not. Scope to the context for the test and raise the product issue when semantics remain insufficient.
Avoid generic CSS or XPath simply to escape strictness. A unique DOM path may identify one node while erasing user intent and becoming fragile. The Playwright getByRole locator guide covers accessible names, exact matching, and resilient role selection in more depth.
5. Scope the Locator to the Correct Container
Repeated control names are normal across dialogs, forms, navigation regions, table rows, and cards. Scope the action inside the region that represents the current task.
const dialog = page.getByRole('dialog', { name: 'Delete project' });
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: 'Delete', exact: true }).click();
This avoids a page-level Delete button or a hidden dialog template. Good containers have meaningful roles and accessible names: dialog, navigation, main, form, article, region, row, and listitem. A stable test ID can identify a component boundary when semantic HTML is insufficient.
Chaining is evaluated relative to the preceding locator. Keep the chain readable by naming intermediate business regions. A one-line selector with five nested CSS fragments may technically be unique but is difficult to review and diagnose.
Scope also helps with localization. A product card can be located by a stable product identifier while the action uses its localized accessible name. Decide which parts are user content and which are stable test contracts.
Be careful with a container that itself matches multiple nodes. Asserting toHaveCount(1) on the container can produce a clearer failure than waiting until the nested click. If multiple containers are expected, add a business filter before locating the child action.
6. Use filter with has, hasNot, hasText, and hasNotText
Locator filters narrow a candidate set using text or descendant locators. They are useful when the target's identity is expressed by related content rather than its own label.
const plans = page.getByRole('article');
const proPlan = plans.filter({
has: page.getByRole('heading', { name: 'Pro' }),
hasNotText: 'Legacy',
});
await expect(proPlan).toHaveCount(1);
await proPlan.getByRole('button', { name: 'Choose plan' }).click();
hasText searches text within each candidate and supports strings or regular expressions. Prefer a specific business identifier over a short substring that could appear in descriptions, badges, or hidden content.
For a table:
const invoiceRow = page.getByRole('row').filter({
has: page.getByRole('cell', { name: 'INV-1042', exact: true }),
});
await expect(invoiceRow).toHaveCount(1);
await invoiceRow.getByRole('button', { name: 'Download' }).click();
The inner has locator is matched relative to each outer candidate. It must identify a descendant, not an unrelated element elsewhere on the page. If the page contains nested rows or repeated text, inspect the resulting count.
Filters preserve the live locator model and retry behavior. Avoid extracting all text into JavaScript and choosing an index unless the business operation genuinely requires data processing outside the DOM query.
7. Combine Locator Conditions with and
locator.and() matches elements that satisfy both locators. It can express an intersection between semantic and stable attributes without writing a complex CSS selector.
const subscribe = page
.getByRole('button')
.and(page.getByTestId('newsletter-submit'));
await expect(subscribe).toHaveAccessibleName('Subscribe');
await subscribe.click();
This pattern is helpful during migrations when a test ID identifies the component contract and the role verifies semantic behavior. It is also useful when one locator captures a role and another captures a narrow CSS state.
Both locators must be compatible and refer to the same element. and does not find one element inside another. Use chaining or filter({ has }) for ancestor and descendant relationships.
Do not combine conditions until the result is unreadable. Role plus accessible name is usually clearer than intersecting several implementation selectors. Use the locator picker and count assertion to validate the result under relevant states.
An intersection can become empty after a legitimate product change, which produces a normal timeout rather than a strict violation. The failure is still useful if the two conditions represent intentional contracts. Avoid intersecting transient classes such as animation names because they create timing-dependent locators.
8. Use first, last, and nth Only When Position Is the Requirement
Positional locators opt out of strictness by selecting one match. They are correct when order itself is part of the tested behavior, such as the first search result, third leaderboard row, or last item in a chronological feed.
const results = page.getByRole('listitem');
await expect(results).toHaveCount(3);
await expect(results).toHaveText([
/Playwright Guide/,
/API Testing Guide/,
/SDET Interview Guide/,
]);
await results.nth(0).getByRole('link').click();
The assertions establish why nth(0) is meaningful. Without them, a sponsored card, hidden template, or sorting regression could silently change the clicked item.
Avoid first() as a universal patch for a locator that unexpectedly matches two controls. The test may select a hidden copy today and a destructive header action tomorrow. If the intended control has a business identity, encode it through name, scope, or related content.
Position is zero-based for nth(). Use a named constant or business explanation when the index is not obvious. For dynamic lists, wait for an expected collection state before taking a positional action.
If the goal is simply to verify that at least one candidate is visible, clarify the product contract. A single first().toBeVisible() can pass while another unexpected duplicate remains. It may be better to assert a precise count or fix the application markup.
9. Handle Collections Intentionally
When multiple elements are expected, choose APIs designed for collections instead of narrowing to one. This preserves coverage and makes list behavior explicit.
const rows = page.getByTestId('result-row');
await expect(rows).toHaveCount(3);
await expect(rows).toContainText(['Alice', 'Bob', 'Chen']);
const labels = await rows.allTextContents();
expect(labels).toHaveLength(3);
The array form of toHaveText compares the complete ordered list. toContainText with an array checks corresponding elements for contained text. Use the assertion that matches whether exact normalized text and order are part of the requirement.
For per-item actions, first establish that the collection is stable, then iterate locators:
await expect(rows).toHaveCount(3);
for (const row of await rows.all()) {
await expect(row).toBeVisible();
}
locator.all() does not wait for a dynamic list to finish loading. Calling it too early can return a partial array. Wait with toHaveCount or another business-ready condition first.
Avoid clicking every matching element unless that is genuinely the scenario. Repeated actions can change the collection while you iterate. If each row needs an independent destructive operation, use isolated test data or re-locate by stable business key after every mutation.
10. Diagnose Hidden Duplicates, Portals, and Component Libraries
Component frameworks often render portals at the document root, retain closed content for transitions, or mount desktop and mobile variants together. The visible interface can therefore contain one apparent control while the DOM contains several matches.
Inspect candidate state during diagnosis:
const matches = page.getByRole('button', { name: 'Confirm' });
console.log('count:', await matches.count());
for (let index = 0; index < await matches.count(); index += 1) {
const candidate = matches.nth(index);
console.log({
index,
visible: await candidate.isVisible(),
html: await candidate.evaluate((element) => element.outerHTML),
});
}
Then target the active component, for example a named visible dialog. Do not keep a loop that chooses the first visible button as the permanent locator if the container can express the same intent.
Hidden duplicates can indicate an application defect when IDs are duplicated, accessible relationships point to the wrong node, or closed dialogs remain exposed to assistive technology. Playwright strictness may be the first signal. Collaborate with developers instead of treating every duplicate as test-only.
The hidden element troubleshooting guide covers visibility and actionability after uniqueness is established.
11. Use locator.or Carefully for Alternative UI Branches
locator.or() creates a locator that matches either alternative. It is useful when a test can legitimately see one of two states, such as a security dialog or the expected dashboard. If both appear, the combined locator has multiple matches and a single-target assertion can throw a strict mode violation.
const securityDialog = page.getByRole('dialog', { name: 'Confirm identity' });
const dashboardHeading = page.getByRole('heading', { name: 'Dashboard' });
await expect(securityDialog.or(dashboardHeading).first()).toBeVisible();
if (await securityDialog.isVisible()) {
await securityDialog.getByRole('button', { name: 'Continue' }).click();
}
await expect(dashboardHeading).toBeVisible();
Here first() is deliberate for the wait gate because either state can signal progress, and the next branch checks the specific state. It is not used to choose an arbitrary business action.
An even clearer design may wait for a stable backend or page state that determines the branch. Avoid using or to combine unrelated fallback selectors for the same element. Fallback selectors can hide regressions in the preferred accessibility contract.
If both alternatives should never coexist, assert that invariant or report a product issue. The test should not silently accept an impossible state merely because one expected element is present.
12. Resolve Strictness in Frames, Tables, and Reusable Widgets
Frames introduce another scope boundary. Use frameLocator() and locate within the intended frame. A page-level selector does not normally cross frame documents, but repeated widgets inside one frame still require the same uniqueness rules.
const paymentFrame = page.frameLocator('iframe[title="Secure payment"]');
await paymentFrame.getByLabel('Card number').fill('4111111111111111');
If several frames share the same title, first make the frame locator unique through a stable attribute or containing component. Do not use a global numeric index unless frame order is part of the integration contract.
For tables, identify a row through a unique business key, then locate the cell or action within it. For reusable widgets, identify the instance through its heading, label, or component test ID. For repeated forms, scope to the form's accessible name.
Shadow DOM is supported by Playwright locators in common open-shadow cases, but XPath does not pierce shadow roots. Do not switch selector engines solely because strictness appears. The core question remains which business instance the test intends.
When a composite widget contains repeated hidden options, interact through its accessible role and state. For example, open a combobox, then select an option within the active listbox. This encodes the user path and prevents matching an inactive listbox retained elsewhere.
13. Prevent and Fix Playwright strict mode violation multiple elements in Framework Code
Create locator conventions that preserve context. Page objects should return component roots and semantic child locators, not page-wide generic actions. Test data should expose stable business keys. Product markup should provide unique IDs where required and accessible names that distinguish controls.
A page-object method can capture the relational pattern:
import { expect, type Locator, type Page } from '@playwright/test';
export class OrdersPage {
constructor(private readonly page: Page) {}
row(orderNumber: string): Locator {
return this.page.getByRole('row').filter({
has: this.page.getByRole('cell', { name: orderNumber, exact: true }),
});
}
async open(orderNumber: string): Promise<void> {
const row = this.row(orderNumber);
await expect(row).toHaveCount(1);
await row.getByRole('link', { name: 'View' }).click();
}
}
Review new first(), last(), and nth() calls. Ask whether order is asserted or whether a business identifier is available. Monitor duplicate test IDs and accessibility violations in component tests where feedback is faster.
After fixing a strict violation, run the test across relevant viewport, locale, permission, and feature-flag states. A locator unique for an administrator on desktop may be ambiguous for a mobile user with an onboarding dialog. The goal is not uniqueness in one snapshot, but clear intent across supported states.
Interview Questions and Answers
Q: What is Playwright strict mode?
It is the rule that single-target operations must resolve to exactly one element. A locator may represent a collection, but click, fill, and similar operations fail when Playwright cannot determine one target.
Q: Why is strictness valuable?
It prevents tests from silently selecting the first DOM match. That catches ambiguous selectors, duplicated UI, and accessibility distinctions before a layout change sends an action to the wrong element.
Q: How do you debug a strict mode violation?
I inspect the full candidate list, count, text, visibility, accessible names, and container context. Then I decide whether the scenario requires one business entity or intentionally covers a collection.
Q: When is first() acceptable?
It is acceptable when first position is itself the requirement, or as a controlled wait gate for alternative states followed by explicit branching. I assert the relevant order or state so DOM position is not accidental.
Q: How do you locate an Edit button in one table row?
I locate the row by a unique cell such as an order number, assert one row, then locate the Edit button within that row. This models the entity-action relationship.
Q: What is the difference between filter({ has }) and chaining?
filter({ has }) keeps outer candidates that contain a matching descendant. Chaining searches for descendants inside an already selected outer locator. They solve parent selection and child selection respectively.
Q: How can locator.or() cause strictness?
The combined locator matches both alternatives when both are present. A single-element assertion then sees multiple nodes, so the test must disambiguate, branch, or assert that coexistence is invalid.
Q: How do you assert a list without strictness?
I use collection-aware assertions such as toHaveCount or array forms of toHaveText and toContainText. For individual checks, I wait for a stable list before iterating locators.
Common Mistakes
- Adding
first()whenever a selector becomes ambiguous. - Selecting controls by DOM index without asserting order.
- Using page-wide text for repeated card, row, or dialog actions.
- Replacing a semantic locator with a brittle unique CSS path.
- Ignoring duplicate test IDs or identical accessible names in the product.
- Calling
locator.all()before a dynamic collection has stabilized. - Using
or()and assuming only one alternative can ever exist. - Filtering by a short substring that appears in hidden or nested content.
- Forgetting that the container locator can also match multiple elements.
- Treating an expected collection as one element instead of using list assertions.
- Keeping diagnostic visibility loops as permanent selector logic.
- Fixing desktop uniqueness without checking mobile, locale, or permission variants.
Conclusion
To fix playwright strict mode violation multiple elements, decide whether the test intends one element or a collection. For one element, express the missing business identity through role, accessible name, component scope, related content, or a stable test contract. For a collection, use count, ordered text, extraction, or controlled iteration APIs.
Keep strictness working for you. Avoid arbitrary positional shortcuts, investigate duplicate semantics, and verify the repaired locator across supported states. The best selector is not merely unique in the current DOM. It explains which user-visible object the test intends and why.
Interview Questions and Answers
Explain Playwright locator strictness.
Locators can model collections, but an operation that requires one DOM target must resolve to exactly one element. Strictness stops ambiguous clicks and fills instead of applying an implicit first-match rule. Collection-aware operations remain valid with several matches.
What is your preferred fix for repeated action names?
I locate the business container, such as a named dialog, order row, or product card, then locate the action within it. This preserves the relationship users understand and avoids global selectors. I assert the container is unique when that is part of the contract.
How do role locators reduce strict violations?
Role plus accessible name adds user-facing identity. It distinguishes controls more clearly than a tag or class and can reveal when the application itself provides ambiguous accessible names. That makes the locator both testable and accessibility-aware.
When would you use nth?
I use `nth()` when position is explicitly under test, such as the third ranked result. I first assert count and order so the index cannot silently refer to a different item. Otherwise, I locate by business identity.
How do you test all elements in a dynamic list?
I wait for the expected collection state with `toHaveCount` or a business-ready assertion, then use list assertions or iterate over `all()`. I avoid calling `all()` before the list stabilizes. Mutating actions require re-locating items after each change.
What product defects can strict mode expose?
It can expose duplicate IDs and duplicate test IDs. It can also reveal multiple active dialogs or hidden responsive controls exposed to accessibility APIs. Identical accessible names for different actions may indicate a product accessibility defect.
How do filter has and hasText differ?
`has` uses a descendant locator and is useful for semantic child relationships. `hasText` searches descendant text and is concise, but broad substrings can match unintended content. I choose the narrowest stable business distinction.
How do you handle either a dashboard or security prompt?
I can wait on a combined `or` locator, then explicitly check which state is visible and complete that branch. I finish by asserting the stable expected destination. I also handle the case where both states would be invalid.
Frequently Asked Questions
How do I fix Playwright strict mode violation multiple elements?
Inspect every match, then add the missing user intent through accessible name, container scope, or a relational filter. If multiple matches are expected, use a collection-aware assertion or iterate after the list is stable.
Why does Playwright strict mode fail on click?
A click needs exactly one target, but the locator resolved to more than one element. Playwright refuses to guess which element should receive the user action.
Can I disable strict mode in Playwright locators?
The reliable solution is to make single-target intent explicit or use a collection operation. Positional locators opt into one match, but they should be used only when position is a real requirement.
Is using locator first a good strict mode fix?
Only when the first position is meaningful and asserted. Using `first()` to silence unexpected duplicates can make the test act on the wrong or hidden element after DOM order changes.
How do I click a button in a specific Playwright table row?
Locate the row by a unique cell value with `filter({ has })` or `hasText`. Assert the row count is one, then locate and click the button within that row. This keeps the action tied to the business entity.
Which Playwright operations allow multiple elements?
Collection operations such as `count()`, `allTextContents()`, `evaluateAll()`, and collection forms of assertions are designed for multiple matches. Single-target actions and assertions require uniqueness.
Why does locator or cause a strict mode violation?
`locator.or()` matches either alternative, so it returns two elements when both alternatives exist. Disambiguate the active branch, use a controlled first wait gate, or assert that both should not coexist.
Related Guides
- How to Fix Playwright locator resolved to hidden element
- How to Fix Playwright waiting for element to be visible enabled and stable
- How to Fix Playwright browserType.launch executable does not exist
- How to Fix Playwright element is not visible
- How to Fix Playwright element is outside of the viewport
- How to Fix Playwright Execution context was destroyed