A Playwright test's page.getByLabel('Assignee') finds nothing although the issue form clearly renders an Assignee label — why?
answer
- The query does not search label elements
- Controls are asked what names them
- ARIA attributes come before the HTML association
- Only labelable controls get a label list
- A miss often signals an accessibility gap
basics
~20 sBecause page.getByLabel searches controls, not label elements. It matches an element through aria-labelledby, aria-label, or a real HTML label association on a form control. A styled div next to a label element has none of those, so nothing matches.
solid answer
~40 s`page.getByLabel` does not look for `<label>` elements; it walks candidate elements and asks what labels each one has. An element qualifies if it has `aria-labelledby` pointing at elements whose text matches, or an `aria-label` attribute that matches, or — only for labelable controls such as `input`, `select`, `textarea`, `button`, `output`, `meter` and `progress` — a genuine HTML label association through `for`/`id` or wrapping. A custom picker built from a `div` with `role="combobox"` has none of these, so a visually adjacent `<label>Assignee</label>` is invisible to the query. The fixes are all on the page side: give the control an `aria-label`, point `aria-labelledby` at the existing label, or make the label reference a real form control. Adding `{ exact: true }` or changing the string will not help.
code
typescript · 17 linesimport { expect, test } from '@playwright/test';
test('only labelable controls answer to getByLabel', async ({ page }) => {
await page.setContent(`
<label>Assignee</label>
<div role="combobox" class="assignee-picker">Unassigned</div>
<label for="reviewer">Reviewer</label>
<input id="reviewer">
`);
// The div has no aria-label and is not labelable: no match.
await expect(page.getByLabel('Assignee')).toHaveCount(0);
// The input is associated with its label through for/id.
await expect(page.getByLabel('Reviewer')).toHaveCount(1);
});go deeper
Remember that the query returns the control, not the label element, and that the control must really be tied to its label through for and id or by wrapping. Visual proximity alone is never enough.
Explain the priority order: aria-labelledby, then aria-label, then the HTML association, and only for labelable controls such as input, select, textarea and button. Everything else has no labels to match.
Diagnose before you edit the test: identify whether the target is a native control, verify the for and id link, and only then question the string. Say clearly when the correct fix belongs in the component.
Treat these misses as accessibility signal. Deciding that a label lookup failing is a product defect rather than a test inconvenience is what keeps a suite from certifying an unusable interface.
## What the query actually searches The name `getByLabel` misleads people into thinking Playwright finds `<label>` elements and returns them, or returns whatever sits next to them. It does neither. `page.getByLabel('Assignee')` iterates over elements and, for each, computes the set of labels that element has. It returns the elements whose label text matches the argument — matched, like the other text queries, as a **case-insensitive substring** by default, with `{ exact: true }` available to require the whole string. The label set is computed in a strict priority order: 1. **`aria-labelledby`** — if the element has it and the referenced elements exist, their text is the label set, and nothing further is consulted. 2. **`aria-label`** — if the attribute is present and not blank, its value is the label. 3. **The HTML label association** — and only for elements the HTML spec calls labelable: `input` (excluding `type="hidden"`), `select`, `textarea`, `button`, `output`, `meter` and `progress`. For those, the browser's own list of associated labels is used. Anything else has **no labels at all**, and no `<label>` sitting beside it changes that. ## Why the issue form fails A modern assignee picker is usually not a `<select>`. It is a `div` with a role, a hidden input, and a popup: ```html <label>Assignee</label> <div role="combobox" class="assignee-picker">Unassigned</div> ``` Walk the priority list against the `div`: no `aria-labelledby`, no `aria-label`, and a `div` is not labelable, so the browser associates no label with it. The label set is empty, and `getByLabel('Assignee')` matches nothing — correctly, because a screen-reader user is in exactly the same position. The query is reporting a real accessibility gap, not a Playwright quirk. ## The failure shapes, ranked by frequency | Symptom | Cause | Fix on the page | |---|---|---| | Label renders, query finds nothing | Control is a `div` or `span` with no ARIA | Add `aria-label` or `aria-labelledby` | | `<label for="assignee">` present | `for` points at an id that does not exist | Correct the `id`, or wrap the control | | Works for one field, not its twin | The twin's label wraps text but not the input | Wrap the control or use `for`/`id` | | Query finds the wrong control | Label text is a substring of another label | Pass `{ exact: true }` | | Field is `type="hidden"` | Hidden inputs are excluded from the label rule | Query the visible control instead | ## Diagnosing it quickly 1. Ask whether the target is a native form control. If it is not, the answer is almost always missing ARIA and the test is not the thing to change. 2. If it is native, check the association rather than the appearance: an `id` typo in `for` breaks the link while the page still looks identical. 3. Only after both check out, suspect the string — a required-field asterisk or a trailing colon makes `{ exact: true }` fail while the default substring still passes. ## What not to do - Do not switch to `getByPlaceholder` as a workaround. A placeholder is not a label, it disappears as soon as the user types, and encoding it in a test entrenches an accessibility defect. - Do not fall back to a positional CSS selector such as the sibling after the label; it re-breaks on the next markup change and hides the real problem. - Do not add an `aria-label` that differs from the visible label text. Two names for one control is its own defect, and the test then asserts a string no user ever sees. ## The senior point The valuable answer here is not the list of rules — it is recognising that **a failing `getByLabel` is usually a product bug, not a test bug**. The query implements roughly what assistive technology does, so a control it cannot name is a control a screen reader cannot announce. The right change lands in the component: give the picker an `aria-labelledby` that points at the existing `<label>`, and the test starts passing for the same reason the page becomes usable. Teams that instead route around the failure with a test id accumulate a suite that is green while the product is unusable, which is precisely the outcome the label query was designed to expose.
- The label reads 'Assignee *' for a required field. Does getByLabel('Assignee') still match?Yes with the default, because the match is a case-insensitive substring of the label text. It stops matching if you pass `{ exact: true }`, since the asterisk is part of the label. Either drop `exact`, or include the full rendered text, or move the marker out of the label.
- If the control has both aria-label and an associated label element, which text does the query use?`aria-labelledby` wins if present and resolvable, then `aria-label`, and only then the associated `<label>` elements. So an `aria-label` overrides the visible label text for this query. That mismatch is worth flagging in review: users see one name and the test asserts another.
- Would getByTitle be a reasonable substitute when a control has no label?It works only if the control actually carries a `title` attribute, and a tooltip is a poor stand-in for a label — it is not shown on touch devices and is inconsistently announced. Prefer fixing the labelling; use `getByTitle` for elements whose title is genuinely the identifying text.
saying these in an interview costs you the question
- Says getByLabel returns the label element itself
- Thinks any div next to a label can be matched
- Believes exact: true fixes a zero-match label lookup
- Suggests switching to a placeholder query instead
- Assumes visual proximity creates a label association
- Treats a failing label lookup as purely a test defect