In Playwright, what is the difference between locator.and() and locator.or() when combining two locators?
answer
- Intersection versus union of two sets
- and narrows, or widens the match
- Same element must satisfy both for and
- or can match two elements at once
- Both reject a cross-frame argument
basics
~20 sand() is an intersection: one element must satisfy both locators. or() is a union: an element matching either one qualifies. Use and() to pin down a single element by two properties, and or() to wait for whichever of two outcomes appears.
solid answer
~40 s`locator.and(other)` matches elements satisfying **both** descriptions, so the same DOM node must match each side — `page.getByRole('button').and(page.getByTitle('Archive issue'))` is a button that is also titled that way. `locator.or(other)` matches elements satisfying **either**, which is how you wait for one of two possible outcomes: a New issue button, or the consent dialog that sometimes appears instead. The catch on `or()` is that when both are present the locator matches two elements, so an action on it violates the one-match rule; the usual shape is to wait on `a.or(b).first()`, branch on what appeared, then act on the concrete locator. Both operators reject an argument from a different frame, failing with `Locators must belong to the same frame.` Neither queries the page — they build a description like every other locator method.
code
typescript · 13 lines// and(): one element, two properties
const archive = page.getByRole('button').and(page.getByTitle('Archive issue'));
await archive.click();
// or(): wait for whichever branch the app took
const newIssue = page.getByRole('button', { name: 'New issue' });
const consent = page.getByText('Confirm workspace settings');
await expect(newIssue.or(consent).first()).toBeVisible();
if (await consent.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newIssue.click();go deeper
Hold on to the set language: and is an intersection and narrows the match, or is a union and widens it. Both return a new locator and neither touches the page yet.
Explain that and requires a single element to satisfy both descriptions, that or is the tool for an unpredictable branch, and why or often needs first() before you act.
Demonstrate the branch-and-act pattern on a real flaky flow, and argue why keeping the alternation inside the locator is better than a JavaScript if statement the reporter cannot see.
Decide when combinators are worth it at all. Frequent and() usage usually signals that the underlying elements lack a stable identity, which is a product-side fix rather than a test-side one.
## `and()` is an intersection `locator.and(other)` returns a locator matching the elements that satisfy **both** descriptions at once — the same DOM node has to match each side. It is the tool for pinning an element down by two independent properties when neither alone is unique: ```ts const archive = page.getByRole('button').and(page.getByTitle('Archive issue')); ``` That is "a button that is also titled Archive issue". On an issue detail page carrying several buttons and several elements with that title, the intersection is unique where each half is not. Because both sides describe the *same* element, `and()` cannot express "a card containing a button" — a card is not a button. That is a subtree relationship, and it belongs to `filter({ has })`. ## `or()` is a union `locator.or(other)` matches elements that satisfy **either** description, or both. Its real job is handling a branch you cannot predict: ```ts const newIssue = page.getByRole('button', { name: 'New issue' }); const consent = page.getByText('Confirm workspace settings'); await expect(newIssue.or(consent).first()).toBeVisible(); if (await consent.isVisible()) await page.getByRole('button', { name: 'Dismiss' }).click(); await newIssue.click(); ``` Without `or()` you would wait on one alternative, time out on the runs where the other appeared, and paper over it with a sleep. With it, the test waits for *whichever* arrives and then branches on what it actually got. ## Side by side | | `and()` | `or()` | |---|---|---| | Set operation | intersection | union | | Matches | elements satisfying both | elements satisfying either | | Typical size | narrows a set | widens a set | | Typical use | disambiguate one element | wait for one of two outcomes | | Cross-frame argument | rejected | rejected | Both throw when the argument locator belongs to a different frame — the message is `Locators must belong to the same frame.` Both are ordinary locator builders: they construct a description and query nothing until the result is used. ## The strictness caveat on `or()` When **both** alternatives are present on the page, an `or()` locator matches two elements. Any action on it then hits the one-match rule and fails. That is why the branching pattern above ends in `.first()` for the wait and then acts on the specific locator once the branch is known. Two habits follow: - use `or()` to **wait**, then act on the concrete locator you resolved to; - if you must act through the union directly, narrow it first. ## When `and()` is not the right answer `and()` is genuinely useful, but a lot of reaches for it are really something else: 1. **You want the container, tested by a descendant** — that is `filter({ has })`, not `and()`. 2. **You want the descendant, inside a container** — that is chaining, `container.locator(inner)`. 3. **You want text as one of the two conditions** — `filter({ hasText })` says it more directly, and reads better in a failure message. The honest use for `and()` is two *element-level* properties on one node, such as role plus title, or a role plus a test id owned by a design system. ## Reading them in failure output Both operators show up in the generated locator description, so a failing step names what you asked for. That is a small but real argument for reaching for `and()` or `or()` rather than hand-rolling the same logic in JavaScript with `count()` and an `if`: the intent stays in the locator, where the report can print it, instead of dissolving into control flow the reporter cannot see. ## Nothing is queried until you use it Both are ordinary builder methods. `and()` and `or()` combine two descriptions and return a third; no request reaches the page, no timeout starts, and no error is raised because a match is currently absent. Everything that waits happens later, when the combined locator is clicked, asserted on, or read from. Two practical consequences follow: - You can define combined locators at the top of a test, or in a shared module, without caring whether the page has rendered yet. - A combined locator that matches nothing is not an error at construction time — it becomes a timeout at the step that uses it, and the step's description will show both halves. The one thing checked eagerly is the frame. Because a locator carries the frame it was built against, passing an argument from another frame is rejected at the moment you combine them rather than at first use. ## A worked choice on an issue tracker Suppose the issue detail page has a toolbar of icon buttons, all with the same role, distinguished only by their `title`. Ask the three questions in order: 1. **Is the target a single element with two properties?** Yes — a button that is titled `Archive issue`. Use `and()`. 2. **Is the target a container identified by something inside it?** If instead you wanted the toolbar group holding that button, that is `filter({ has: … })`. 3. **Is the target one of two possible outcomes?** If archiving sometimes opens a confirmation instead, wait on `confirm.or(toast).first()` and branch. Answering those in order keeps you from reaching for a combinator where a filter or a chain says the same thing more plainly.
- Why does the branching example call `.first()` on the `or()` locator?Because if both the button and the dialog happen to be on screen, the union matches two elements and the one-match rule rejects the operation. Waiting on `.first()` keeps the wait honest, and the test then acts on the specific locator it resolved to.
- Could you express "the card containing an Assign button" with `and()`?No. `and()` requires one element to match both sides, and a card is not a button. That is a subtree relationship, so it belongs to `filter({ has: … })`, which keeps the match on the card while testing what it contains.
- What happens if the argument locator belongs to a different frame?Both `and()` and `or()` reject it with `Locators must belong to the same frame.` The check happens when you build the combined locator, not when you use it, so the failure surfaces at the line that combines them.
saying these in an interview costs you the question
- Thinks and() matches a container plus its descendant
- Assumes or() silently picks the first available element
- Forgets or() can match two elements at once
- Uses and() for a text condition instead of hasText
- Believes the operators query the page immediately