fix(locator): reads wait for their match until the action timeout
Merged
OK
14d ago+115+115+012345678901234567890123456789-40-40-01234567890123456789
What and why
textContent(),inputValue(),getAttribute(),isEnabled(),isDisabled(),isChecked(), andboundingBox()now poll for their one match until the action timeout, through the sameresolveExactlyOneloop actions use. They used to fail at once withLOCATOR_NOT_FOUNDwhen the node was still rendering, while actions and Playwright's reads wait.- Reads already waited for a missing frame and re-resolved a stale node within the action timeout; zero matches was the one case that did not. Item 7 of #1020 hit this on every menu close; #1078 proposed documenting it as by design instead, and was closed for this fix.
isVisible(),isHidden(),count(),all(), andallTextContents()still answer from the current screen, as in Playwright.resolveForReadis gone;LocatorEngine.readis its only caller (packages/e2e/src/locator/engine.ts).expect(browser).toHaveClassreads throughgetAttributeinside its own poll, bounded by the assertion deadline. A read that waits longer than that deadline turned a never-arriving node intoACTION_FAILED operation timed out. It now samplescount()first, so absence staysASSERTION_FAILEDobservingno node(packages/web/src/browser.ts). Found by the fresh-context review.- Docs:
reference/screenprose and table,migrate/playwrightrow back tothe same,core-concepts,migrate/selenium,skills/e2etext; changesetse2eminor and@e2e-dev/webpatch.
await app.open('/checkout');
await screen.getByRole('button', 'Apply').tap();
expect(await screen.getByLabel('Total').textContent()).toBe('$42.00'); // waits for the re-rendered field
Verified
Ran it locally: yes
- testbed, built CLI,
node node_modules/e2e/dist/cli/bin.js run tests/zz-reads-wait.e2e.ts: a scratch test opens/controls, inserts<output aria-label="Late total">42</output>after 1.5 s, then readstextContent().
| main | this branch |
|---|---|
fails in 0.5 s: LOCATOR_NOT_FOUND locator matched no nodes: getByLabel("Late total") | passes in 1.76 s |
packages/e2e/tests/integration/web-platform.test.ts(real Chromium through the built runner):reads wait for a node that arrives lateanda read of a node that never arrives fails with LOCATOR_NOT_FOUNDboth fail with main's engine built into dist and pass here. The second one takes the full 5 s action timeout.- Same suite:
class assertions on a node that never arrives fail as assertionsfails withACTION_FAILEDwhen thebrowser.tshunk is reverted, passes here asASSERTION_FAILEDwithobserved: no node.hung-page.test.tsstill ends a class assertion on a frozen renderer at its own timeout. - Unit:
locator-engine.test.tspolls zero matches then reads, and gives up at the action timeout without a read;locator-basics.test.tskeepsisVisible()immediate at zero matches andLOCATOR_AMBIGUOUSimmediate on several. - Testbed suite: 77 passed, 1 skipped. Full
pnpm testgreen.
Checklist
- Changesets added (
e2eminor,@e2e-dev/webpatch). - No breaking change: a read that used to throw at once now throws after the action timeout; no signature or error code changes.
- Docs updated in the same PR:
docs/reference/screen.mdx,docs/migrate/playwright.mdx,skills/e2e/. - No public type, wire, or engine contract change (one JSDoc line on
textContent).