Restore the conditions eight converted QA scripts were waiting for

The harness conversion replaced pauses that stood in for real conditions
with camera readiness: the cables and infrastructure idle checks started
measuring while the overlay was still fading in after moveEnd, directions
sampled before its maneuver dots had ground cells, three scripts clicked
before the loading cover was gone, the credits check hit-tested during UI
transitions, and the oblique traffic flight lost its height cap. Each now
waits for its condition; idle checks observe without requesting frames
and record the reasons of the frames they count; readiness waits for the
startup cover and no longer requests frames while polling.
This commit is contained in:
German Khokhlov committed 2026-11-10 20:14:50 +04:00
1 parent 2c62e1e378
commit b5cc4feacb
11 files changed
+480 -51

No files matched your search

+3 -6
View File
@@ -2,6 +2,7 @@
// Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter. // Cesium-only probes: native DataSource counts and scene-render timing measure the Cesium cable adapter.
import { evaluateWithGeo } from './qa/geometry.mjs'; import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled } from './qa/ready.mjs'; import { whenSettled } from './qa/ready.mjs';
import { measureIdleRender } from './qa/idle.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs'; import { launchBrowser, closeBrowser } from './qa/browser.mjs';
/** /**
* qa-cables-overlay — submarine-cable label path measurement + contract gate. * qa-cables-overlay — submarine-cable label path measurement + contract gate.
@@ -184,12 +185,8 @@ try {
: 'scene.render cost, cables ON, 10s orbit', frameCost); : 'scene.render cost, cables ON, 10s orbit', frameCost);
// ── d. parked idle honesty (labels must not force continuous render) ── // ── d. parked idle honesty (labels must not force continuous render) ──
await whenSettled(page); // orbit stop + fades settle await whenSettled(page);
const idle = await page.evaluate(() => new Promise((resolve) => { const idle = await measureIdleRender(page);
let renders = 0;
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => { renders += 1; });
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
}));
report(control report(control
? 'parked idle, control (postRender fires / 5s)' ? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with cables ON (postRender fires / 5s)', idle); : 'parked idle with cables ON (postRender fires / 5s)', idle);
+9 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
// Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry. // Cesium-only probes: native primitive inspection independently verifies the Cesium route geometry.
import { evaluateWithGeo } from './qa/geometry.mjs'; import { evaluateWithGeo } from './qa/geometry.mjs';
import { whenSettled, whenUiSettled, whenTilesLoaded } from './qa/ready.mjs'; import { whenSettled, whenUiSettled, pollPage } from './qa/ready.mjs';
/** /**
* Browser proof of the Directions layer: real chip clicks, real globe clicks, * Browser proof of the Directions layer: real chip clicks, real globe clicks,
* a real route from the real proxy, a real camera flight. * a real route from the real proxy, a real camera flight.
@@ -779,7 +779,14 @@ try {
if (placed) { if (placed) {
await waitForRoute(); await waitForRoute();
// Let the shared ground-floor cells warm and the dots re-anchor. // Let the shared ground-floor cells warm and the dots re-anchor.
await whenTilesLoaded(page); await pollPage(
page,
() => {
const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
return stepCount > 0 && stepsShown.length === stepCount;
},
{ timeoutMs: 60_000 },
);
const denver = await evaluateWithGeo(page, async (qaGeo) => { const denver = await evaluateWithGeo(page, async (qaGeo) => {
const Cartographic = qaGeo.cartographic; const Cartographic = qaGeo.cartographic;
const { stepCount, stepsShown } = window.__gevQaDirections.drawn(); const { stepCount, stepsShown } = window.__gevQaDirections.drawn();
+2 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/** Persistent inline attribution through actual OSM layer display lifetimes. */ /** Persistent inline attribution through actual OSM layer display lifetimes. */
import { whenSettled } from './qa/ready.mjs'; import { whenSettled, whenUiSettled } from './qa/ready.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs'; import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
@@ -53,6 +53,7 @@ try {
); );
const shot = async (name, osm, tiles) => { const shot = async (name, osm, tiles) => {
await whenSettled(page); await whenSettled(page);
await whenUiSettled(page);
const state = await page.$eval('#cesium-credits', (e) => { const state = await page.$eval('#cesium-credits', (e) => {
const nodes = [ const nodes = [
...e.querySelectorAll('a,img,.cesium-credit-expand-link'), ...e.querySelectorAll('a,img,.cesium-credit-expand-link'),
+2 -21
View File
@@ -27,6 +27,7 @@ import { evaluateWithGeo } from './qa/geometry.mjs';
* FAIL. Commits nothing. * FAIL. Commits nothing.
*/ */
import { whenSettled } from './qa/ready.mjs'; import { whenSettled } from './qa/ready.mjs';
import { measureIdleRender } from './qa/idle.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs'; import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -297,27 +298,7 @@ try {
// ── parked-idle honesty: the LOD walk must not force continuous render ─ // ── parked-idle honesty: the LOD walk must not force continuous render ─
await whenSettled(page); await whenSettled(page);
const idle = await page.evaluate(async () => { const idle = await measureIdleRender(page);
// The debug handle's own governor read (no /src import: runs on a build).
const { getRenderGovernorDiagnostics } = window.__godsEyeView;
return new Promise((resolve) => {
const startedAt = Date.now();
const tilesLoadedBefore = window.__godsEyeView.engine.surface.tilesLoaded();
let renders = 0;
let framesWithPendingTiles = 0;
const remove = window.__godsEyeView.engine.frames.postRender.addEventListener(() => {
renders += 1;
if (!window.__godsEyeView.engine.surface.tilesLoaded()) framesWithPendingTiles += 1;
});
setTimeout(() => {
remove();
const governor = getRenderGovernorDiagnostics();
resolve({ renders, tilesLoadedBefore, tilesLoadedAfter: window.__godsEyeView.engine.surface.tilesLoaded(),
framesWithPendingTiles, mode: governor.mode, holds: governor.holds,
requests: governor.recentRequests.filter(request => request.at >= startedAt) });
}, 5_000);
});
});
report(CONTROL report(CONTROL
? 'parked idle, control (postRender fires / 5s)' ? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with infra ON (postRender fires / 5s)', idle); : 'parked idle with infra ON (postRender fires / 5s)', idle);
+10 -14
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env node #!/usr/bin/env node
/** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */ /** Reticle coverage on oblique arrivals: source requests, toggle oracle and zoom revisits. */
import { whenSettled } from './qa/ready.mjs'; import { whenSettled } from './qa/ready.mjs';
import {
moveTrafficCamera as moveCamera,
whenTrafficSettled,
} from './qa/traffic.mjs';
import { launchBrowser, closeBrowser } from './qa/browser.mjs'; import { launchBrowser, closeBrowser } from './qa/browser.mjs';
import fs from 'node:fs'; import fs from 'node:fs';
import { import {
moveCamera,
createScreencast, createScreencast,
installFrameProbe, installFrameProbe,
} from './qa-journey-recorder.mjs'; } from './qa-journey-recorder.mjs';
@@ -96,19 +99,7 @@ try {
}; };
}); });
const settle = async () => { const settle = async () => {
await whenSettled(page); await whenTrafficSettled(page);
await page
.waitForFunction(
() => {
const s = window.__godsEyeView.dataManager.layers
.get('traffic')
.module.getStats();
return !s.loading;
},
{ timeout: 15000 },
)
.catch(() => {});
await whenSettled(page);
return snap(); return snap();
}; };
for (const [name, lat, lon, height] of [ for (const [name, lat, lon, height] of [
@@ -306,6 +297,11 @@ try {
fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2)); fs.writeFileSync(`${out}/result.json`, JSON.stringify(result, null, 2));
await closeBrowser(browser); await closeBrowser(browser);
} }
console.log(JSON.stringify({
failures: result.failures,
errors: result.errors,
forbidden: result.forbidden,
}));
if ( if (
!baseline && !baseline &&
(result.failures.length || result.errors.length || result.forbidden.length) (result.failures.length || result.errors.length || result.forbidden.length)
+92
View File
@@ -0,0 +1,92 @@
/** Observe a quiet request-mode globe, then measure it without requesting frames. */
export function measureIdleRender(
page,
{ quietMs = 600, sampleMs = 5000, timeoutMs = 15000 } = {},
) {
return page.evaluate(
({ quietMs, sampleMs, timeoutMs }) =>
new Promise((resolve, reject) => {
const { engine } = window.__godsEyeView;
const started = performance.now();
let lastActivity = started;
let measuringAt = null;
let renders = 0;
let framesWithPendingTiles = 0;
let tilesLoadedBefore = false;
const settlingRequests = {};
const requests = {};
const original = engine.render.request;
const observedRequest = function (reason = 'globe-engine') {
const now = performance.now();
lastActivity = now;
const counts = measuringAt === null ? settlingRequests : requests;
const row = (counts[reason] ??= {
count: 0,
firstMs: now - started,
lastMs: 0,
});
row.count++;
row.lastMs = now - started;
return original.apply(this, arguments);
};
engine.render.request = observedRequest;
const remove = engine.frames.postRender.addEventListener(() => {
lastActivity = performance.now();
if (measuringAt === null) return;
renders++;
if (!engine.surface.tilesLoaded()) framesWithPendingTiles++;
});
const cleanup = () => {
clearInterval(timer);
remove();
if (engine.render.request === observedRequest)
engine.render.request = original;
};
const timer = setInterval(() => {
if (window.__godsEyeView.engine !== engine || engine.isDestroyed()) {
cleanup();
reject(new Error('Idle observation interrupted: engine changed'));
return;
}
const now = performance.now();
const tilesLoaded = engine.surface.tilesLoaded();
if (measuringAt === null) {
if (
!tilesLoaded ||
engine.camera.flightActive() ||
engine.render.isContinuous()
)
lastActivity = now;
if (now - lastActivity >= quietMs) {
measuringAt = now;
tilesLoadedBefore = tilesLoaded;
} else if (now - started >= timeoutMs) {
cleanup();
reject(
new Error(
`Readiness timeout: idle render; ${JSON.stringify({ tilesLoaded, settlingRequests })}`,
),
);
}
return;
}
if (now - measuringAt < sampleMs) return;
cleanup();
const governor =
window.__godsEyeView.getRenderGovernorDiagnostics?.();
resolve({
renders,
settleMs: Math.round(measuringAt - started),
settlingRequests,
requests,
tilesLoadedBefore,
tilesLoadedAfter: tilesLoaded,
framesWithPendingTiles,
mode: governor?.mode,
holds: governor?.holds,
});
}, 50);
}),
{ quietMs, sampleMs, timeoutMs },
);
}
+112
View File
@@ -0,0 +1,112 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { measureIdleRender } from './idle.mjs';
function fixture(t) {
const engine = createFakeEngine();
const previous = globalThis.window;
globalThis.window = { __godsEyeView: { engine } };
let now = 0;
t.mock.method(performance, 'now', () => now);
t.mock.timers.enable({ apis: ['setInterval'] });
t.after(() => {
if (previous === undefined) delete globalThis.window;
else globalThis.window = previous;
engine.destroy();
});
return {
engine,
page: { evaluate: (fn, options) => fn(options) },
tick(ms) {
now += ms;
t.mock.timers.tick(ms);
},
};
}
test('idle measurement waits out fades, records reasons, and never requests a frame', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page, { quietMs: 100, sampleMs: 200 });
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
tick(50);
engine.render.request('hud');
engine.fake.frame();
tick(200);
const result = await pending;
assert.equal(result.settleMs, 150);
assert.equal(result.renders, 1);
assert.equal(result.settlingRequests['world-overlay'].count, 2);
assert.equal(result.requests.hud.count, 1);
assert.equal(result.framesWithPendingTiles, 0);
assert.equal(engine.fake.callsOf('render.request').length, 3);
assert.equal(engine.render.request, original);
assert.equal(engine.frames.postRender.numberOfListeners, 0);
tick(1000);
assert.equal(engine.fake.callsOf('render.request').length, 3);
});
test('persistent render demand fails with its reason and releases instrumentation', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page, { quietMs: 100, timeoutMs: 300 });
const rejected = assert.rejects(pending, /idle render.*world-overlay/);
for (let i = 0; i < 6; i++) {
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
}
await rejected;
assert.equal(engine.render.request, original);
assert.equal(engine.frames.postRender.numberOfListeners, 0);
assert.equal(engine.fake.callsOf('render.request').length, 6);
});
test('continuous mode and pending tiles cannot masquerade as idle', async (t) => {
for (const condition of ['continuous', 'tiles']) {
await t.test(condition, async (t) => {
const { engine, page, tick } = fixture(t);
if (condition === 'continuous') engine.render.setContinuous(true);
else t.mock.method(engine.surface, 'tilesLoaded', () => false);
const pending = measureIdleRender(page, { quietMs: 100, timeoutMs: 300 });
const rejected = assert.rejects(
pending,
/Readiness timeout: idle render/,
);
tick(300);
await rejected;
assert.equal(engine.fake.callsOf('render.request').length, 0);
});
}
});
test('engine destruction stops the idle observer', async (t) => {
const { engine, page, tick } = fixture(t);
const original = engine.render.request;
const pending = measureIdleRender(page);
const rejected = assert.rejects(pending, /engine changed/);
engine.destroy();
tick(50);
await rejected;
assert.equal(engine.render.request, original);
});
test('renders during the sample stay counted even when demand becomes continuous', async (t) => {
const { engine, page, tick } = fixture(t);
const pending = measureIdleRender(page, { quietMs: 100, sampleMs: 500 });
tick(100);
for (let i = 0; i < 10; i++) {
engine.render.request('world-overlay');
engine.fake.frame();
tick(50);
}
const result = await pending;
assert.equal(result.renders, 10);
assert.equal(result.requests['world-overlay'].count, 10);
});
+10 -7
View File
@@ -66,13 +66,18 @@ export async function whenSettled(
) { ) {
const started = Date.now(); const started = Date.now();
try { try {
await page.waitForFunction(() => window.__godsEyeView?.engine, { const handle = await page.waitForFunction(
timeout: timeoutMs, () => {
polling: 50, if (!window.__godsEyeView?.engine) return false;
}); const cover = globalThis.document?.querySelector('#loading-screen');
return !cover || getComputedStyle(cover).visibility === 'hidden';
},
{ timeout: timeoutMs, polling: 50 },
);
await handle?.dispose();
} catch { } catch {
throw new Error( throw new Error(
'Readiness timeout: engineStarted, initialViewRestored, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable; observed: inputsEnabled=unknown, following=unknown, cockpitActive=unknown, lastPoseDelta=null', 'Readiness timeout: engineStarted, initialViewRestored, startupUiReady, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable; observed: inputsEnabled=unknown, following=unknown, cockpitActive=unknown, lastPoseDelta=null',
); );
} }
return page.evaluate( return page.evaluate(
@@ -205,7 +210,6 @@ export async function whenTilesLoaded(page, { timeoutMs = 30000 } = {}) {
() => { () => {
const engine = window.__godsEyeView?.engine; const engine = window.__godsEyeView?.engine;
if (!engine || engine.isDestroyed()) return false; if (!engine || engine.isDestroyed()) return false;
engine.render.request('qa-tile-readiness');
return engine.surface.tilesLoaded(); return engine.surface.tilesLoaded();
}, },
{ timeout: timeoutMs, polling: 100 }, { timeout: timeoutMs, polling: 100 },
@@ -217,7 +221,6 @@ export async function whenUiSettled(page) {
const handle = await page.waitForFunction( const handle = await page.waitForFunction(
() => { () => {
const app = window.__godsEyeView; const app = window.__godsEyeView;
app?.engine?.render.request('qa-ui-readiness');
return ( return (
!app?.styleManager?.transitions?.size && !app?.styleManager?.transitions?.size &&
document document
+55
View File
@@ -4,6 +4,7 @@ import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { createSettledState } from '../../src/app/qaProbe.js'; import { createSettledState } from '../../src/app/qaProbe.js';
import { import {
waitForSettled, waitForSettled,
whenSettled,
whenFramesDrawn, whenFramesDrawn,
whenTilesLoaded, whenTilesLoaded,
whenUiSettled, whenUiSettled,
@@ -252,3 +253,57 @@ test('scenario polling waits for evidence and identifies an unmet condition', as
/broken predicate/, /broken predicate/,
); );
}); });
test('camera readiness also waits for the startup cover to stop intercepting clicks', async (t) => {
pageEngine(t);
const previous = {
document: globalThis.document,
getComputedStyle: globalThis.getComputedStyle,
};
let visibility = 'visible';
globalThis.document = { querySelector: () => ({}) };
globalThis.getComputedStyle = () => ({ visibility });
t.after(() => {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete globalThis[key];
else globalThis[key] = value;
}
});
let disposed = false;
await whenSettled({
async waitForFunction(predicate) {
assert.equal(predicate(), false, 'a started engine still has a cover');
visibility = 'hidden';
assert.equal(predicate(), true);
return {
dispose: async () => {
disposed = true;
},
};
},
async evaluate() {
assert.equal(disposed, true);
},
});
});
test('tile and UI polling never request globe frames', async (t) => {
const { engine } = pageEngine(t);
const previous = globalThis.document;
globalThis.document = { getAnimations: () => [] };
t.after(() => {
if (previous === undefined) delete globalThis.document;
else globalThis.document = previous;
});
t.mock.method(engine.render, 'request', () => {
assert.fail('passive readiness requested a frame');
});
const page = {
async waitForFunction(predicate) {
assert.equal(predicate(), true);
return { dispose: async () => {} };
},
};
await whenTilesLoaded(page);
await whenUiSettled(page);
});
+69
View File
@@ -0,0 +1,69 @@
import { evaluateWithGeo } from './geometry.mjs';
import { whenSettled, whenTilesLoaded } from './ready.mjs';
/** Preserve the oblique fixture's native collision and flight-arc settings. */
export function moveTrafficCamera(page, view, seconds) {
return evaluateWithGeo(
page,
async (geo, view, duration) => {
const { engine, viewer } = window.__godsEyeView;
const native = engine.kind === 'cesium';
const camera = native ? viewer.camera : engine.camera;
// These low-altitude fixture poses must not be lifted by native collision.
if (native)
viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
camera.cancelFlight();
await new Promise((resolve) =>
camera.flyTo({
destination: geo.cartesian3.fromDegrees(
view.lon,
view.lat,
view.height,
),
orientation: {
heading: ((view.heading ?? 0) * Math.PI) / 180,
pitch: ((view.pitch ?? -45) * Math.PI) / 180,
roll: 0,
},
duration,
// An automatic high arc crosses the traffic layer's activation ceiling.
maximumHeight: Math.max(view.height, engine.camera.height()),
complete: resolve,
cancel: resolve,
}),
);
},
view,
seconds,
);
}
/** A quiet loader can still be waiting for its camera-change debounce. */
export async function whenTrafficSettled(page) {
await whenTilesLoaded(page);
await whenSettled(page);
const handle = await page.waitForFunction(
() => {
const { engine, dataManager } = window.__godsEyeView;
const pose = engine.camera.pose();
const viewKey = [
pose.cartographic.latitude,
pose.cartographic.longitude,
pose.cartographic.height / 10000,
pose.heading,
pose.pitch,
]
.map((value) => Number(value || 0).toFixed(7))
.join(',');
const stats = dataManager.layers.get('traffic').module.getStats();
return (
stats.roadBounds?.coverage?.viewKey === viewKey &&
stats.count > 0 &&
!stats.loading &&
!stats.surfacePending
);
},
{ timeout: 60000, polling: 100 },
);
await handle.dispose();
}
+116
View File
@@ -0,0 +1,116 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
import { moveTrafficCamera, whenTrafficSettled } from './traffic.mjs';
function fixture(t) {
const engine = createFakeEngine({ kind: 'cesium' });
engine.camera.setView({
destination: { x: 6379137, y: 0, z: 0 },
orientation: { heading: 0, pitch: -Math.PI / 12, roll: 0 },
});
const previousWindow = globalThis.window;
const previousDocument = globalThis.document;
globalThis.window = { __godsEyeView: { engine } };
globalThis.document = { querySelector: () => null };
t.after(() => {
if (previousWindow === undefined) delete globalThis.window;
else globalThis.window = previousWindow;
if (previousDocument === undefined) delete globalThis.document;
else globalThis.document = previousDocument;
engine.destroy();
});
return globalThis.window.__godsEyeView;
}
test('oblique fixture preserves collision and maximum flight height on Cesium', async (t) => {
const app = fixture(t);
let flight;
let cancelled = false;
app.viewer = {
scene: { screenSpaceCameraController: { enableCollisionDetection: true } },
camera: {
cancelFlight() {
cancelled = true;
},
flyTo(options) {
flight = options;
options.complete();
},
},
};
await moveTrafficCamera(
{ evaluate: (fn, ...args) => fn(...args) },
{ lon: 55.037, lat: 24.964, height: 276, pitch: -15 },
1.4,
);
assert.equal(cancelled, true);
assert.equal(
app.viewer.scene.screenSpaceCameraController.enableCollisionDetection,
false,
);
assert.ok(Math.abs(flight.maximumHeight - 1000) < 1e-6);
assert.equal(flight.duration, 1.4);
assert.equal(flight.orientation.pitch, -Math.PI / 12);
});
test('traffic readiness rejects old coverage, pending debounce and partial populations', async (t) => {
const app = fixture(t);
const stats = {
count: 0,
loading: false,
surfacePending: 0,
roadBounds: null,
};
app.dataManager = {
layers: new Map([['traffic', { module: { getStats: () => stats } }]]),
};
let calls = 0;
let disposed = 0;
await whenTrafficSettled({
evaluate: async () => {},
async waitForFunction(predicate) {
if (++calls < 3) assert.equal(predicate(), true);
else {
assert.equal(
predicate(),
false,
'idle before the debounce is not loaded',
);
stats.count = 100;
stats.roadBounds = { coverage: { viewKey: 'old view' } };
assert.equal(predicate(), false, 'old roads do not cover this camera');
stats.roadBounds.coverage.viewKey =
'0.0000000,0.0000000,0.1000000,0.0000000,-0.2617994';
stats.loading = true;
assert.equal(predicate(), false, 'partial road population');
stats.loading = false;
stats.surfacePending = 1;
assert.equal(predicate(), false, 'unresolved surface');
stats.surfacePending = 0;
assert.equal(predicate(), true);
}
return {
dispose: async () => {
disposed++;
},
};
},
});
assert.equal(disposed, 3);
});
test('traffic loading timeouts fail the scenario', async (t) => {
fixture(t);
let calls = 0;
await assert.rejects(
whenTrafficSettled({
evaluate: async () => {},
async waitForFunction() {
if (++calls === 3) throw new Error('traffic loading timeout');
return { dispose: async () => {} };
},
}),
/traffic loading timeout/,
);
});