Playwright page
Poll budget in ms, not a hard ceiling on the call. The
preliminary wait above consumes at most max(1, min(timeout, 10000)) of it —
floored at 1 ms because Playwright reads timeout: 0 as NO timeout, so a
non-positive budget would otherwise wait there for the whole test — and the
do/while below then always runs at least one probe, bounded by its own 45 s
deadline rather than by this budget, so a call can overrun timeout by a whole
probe (both end-of-budget reports state the measured elapsed time alongside
this budget for exactly that reason). Probing at least once matters because the
preliminary wait can spend the whole budget on its own, and reporting "no probe
answered" without having probed would be a false diagnostic.
Wait for nD navigation to complete: poll until
getState().isLoadingis false.There is no "wait for the navigation to START" phase — the helper only ever waits for
isLoadingto be false, so it resolves on the first poll if the loader is already idle. That is the intended behaviour on both sides of the race: a same-task trigger (a keyboard nav) sets the flag synchronously —SceneLoader.updateViewtakes the lock before its first await — so it is already true before this helper gets to poll, while a fully cache-served pass may never be OBSERVED true at all: polling is discrete (waitForFunctionon rAF below, then a 100 ms loop), so such a pass can start and finish between two polls. That is why the callers inspatial-index-accuracy.spec.tsprefer this silent variant. Waiting fortruefirst would hang in the second case.The only preliminary wait is for the flag to EXIST (
typeof isLoading === 'boolean'), i.e. for the debug interface to be installed. That wait failing no longer returns on its own (#1726): it FALLS THROUGH into the poll loop below, so the one end-of-budget decision governs. Its old comment claimed the failure meant "navigation completed before we start polling", which does not actually explain a missingisLoading— the flag exists whenever the debug interface does (getState()always returns a real boolean, seecore/app/debug/debug-state.ts) — so returning there returned on nothing. Falling through gives the state probe a chance to answer, or to produce a diagnostic of its own. The preliminary failure is still worth REPORTING — "the flag was unreadable for the first N ms" is exactly what explains a 15 s budget that never sawisLoadingclear — so its duration (deterministically the clamped preliminary budget) is interpolated into the answered-but-unsettled warn. It is NOT a probe failure: it stays out of the probe counts, and out of the throw, where the carried probe rejection is both fresher and the better headline.Silent only on the tolerated case. The give-up is split in two:
isLoadingnever cleared withintimeout: returns normally, after oneconsole.warnnaming the helper, how many probes went out and how many yielded no usable state, and the preliminary failure if there was one. This is what the nine call sites rely on — several deliberately pick this variant because their action never togglesisLoadingat all, and they succeed on the first probe.The gain is ATTRIBUTION, not a rescued pass: at all nine call sites the statement following the wait is itself a page probe (
getLuxarState, a barepage.evaluate, orgetDimensionValue, which answers-1and then fails its ownexpect), so the old silence already surfaced as a CONFUSING FAILURE one statement later — never as a pass. What changes is the name on it, and only when the decision is reached at all: the fall-through can take a starved page ~55 s past the start of the wait, which at the three sites whose next statement is an unboundedpage.evaluateusually lands beyond the 60 s per-testtimeoutinplaywright.config.ts— and beyond it at the six boundedspatial-index-accuracy.spec.tssites too, since the spec has already spent seconds reaching the wait. At those six the THROW is additionally near-unreachable: they sit right after awaitForSpatialQueryOrThrow, which cannot itself have succeeded unless the page answered within the last 8 s, so what they get from this is the attributed warn. Closing the vacuous pass is therefore about the CONTRACT, for a future caller that does not happen to re-probe straight afterwards. What is closed is a swallowed probe FAILURE, not a swallowed timeout; seewaitForPointsLoadedfor why the probes here are not clamped to the loop's budget.Use waitForNavigationCompleteOrThrow for tests that depend on navigation actually finishing.
Unit-tested in
src/tests/unit/tests/e2e-helpers-silent-waits.test.ts.