fix(web): an exact label query composes exactly as a has filter or a scope
Merged
OK
1mo ago+265+265+012345678901234567890123456789-39-39-01234567890123456789
rows.filter({ has: screen.getByLabel('Name', { exact: true }) })matched a row whose input is labelledLast Nametoo, so.first().getByRole('button', { name: 'Remove' }).tap()removed the wrong row. Standalone, the same query matched one input.- Cause:
project()inpackages/web/src/locators.tsbuilt the composable form of an exact label query as Playwright's substringgetByLabel, since Playwright's chain had no selector for the reader's exact label predicate. The standalone path kept the predicate; the composed path lost it. - Fix: a third selector engine,
e2e-label=<json>(packages/web/src/label-selector.ts), runs the in-page reader on the labelable controls under its root, closed shadow roots included, and keeps those whose labels equal the value after whitespace normalization. That is the same comparison the surface makes standalone, so ahasfilter or a scope on an exact label now matches exactly what the standalone query matches. - Playwright's own
getByLabel(..., { exact: true })was the shorter route and I did not take it: it reads a label's aria-hidden text, soDisplay name*would match standalone and miss insidehas. The new test pins that case. SELECTOR_ENGINES(packages/web/src/selector-engines.ts) is now the list both registration paths (launched browser, CDP context) read.
flowchart LR
A["getByLabel('Name', { exact: true })"] --> B{composed?}
B -- "standalone" --> C["e2e-roots=<labelable> then surface keeps labels == 'Name'"]
B -- "has / scope, before" --> D["e2e-roots= >> getByLabel('Name', substring)"]
B -- "has / scope, after" --> E["e2e-label={value:'Name',...} runs the reader in the page"]
const rows = screen.getByRole('listitem');
await rows
.filter({ has: screen.getByLabel('Name', { exact: true }) })
.first()
.getByRole('button', { name: 'Remove' })
.tap();
New browser-backed test: packages/web/tests/integration/label-composition.test.ts on a /rows fixture (Last Name, Name* with an aria-hidden marker, and a label carrying " and >>).