fix(locator): reads wait for their match until the action timeout

Merged
OK
14d ago
+115-40

What and why

  • textContent(), inputValue(), getAttribute(), isEnabled(), isDisabled(), isChecked(), and boundingBox() now poll for their one match until the action timeout, through the same resolveExactlyOne loop actions use. They used to fail at once with LOCATOR_NOT_FOUND when 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(), and allTextContents() still answer from the current screen, as in Playwright.
  • resolveForRead is gone; LocatorEngine.read is its only caller (packages/e2e/src/locator/engine.ts).
  • expect(browser).toHaveClass reads through getAttribute inside its own poll, bounded by the assertion deadline. A read that waits longer than that deadline turned a never-arriving node into ACTION_FAILED operation timed out. It now samples count() first, so absence stays ASSERTION_FAILED observing no node (packages/web/src/browser.ts). Found by the fresh-context review.
  • Docs: reference/screen prose and table, migrate/playwright row back to the same, core-concepts, migrate/selenium, skills/e2e text; changesets e2e minor and @e2e-dev/web patch.
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 reads textContent().
mainthis 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 late and a read of a node that never arrives fails with LOCATOR_NOT_FOUND both 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 assertions fails with ACTION_FAILED when the browser.ts hunk is reverted, passes here as ASSERTION_FAILED with observed: no node. hung-page.test.ts still ends a class assertion on a frozen renderer at its own timeout.
  • Unit: locator-engine.test.ts polls zero matches then reads, and gives up at the action timeout without a read; locator-basics.test.ts keeps isVisible() immediate at zero matches and LOCATOR_AMBIGUOUS immediate on several.
  • Testbed suite: 77 passed, 1 skipped. Full pnpm test green.

Checklist

  • Changesets added (e2e minor, @e2e-dev/web patch).
  • 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).