mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Wait for the application, not the clock, in the strict QA set
The probe exposes settled() and the first-frame outcome; scripts/qa/ready.mjs waits for drawn frames with the camera at rest and for a pick at the click position, with a deadline and its reason in the error. The strict scenarios use these instead of fixed pauses and loading-screen polling, and the hidden-switch scenario checks how the loading screen was lifted instead of how long it took. Each switch scenario now records a draw ledger per engine and runs a contract monitor; both write to the scenario's evidence file only, with an empty allowlist until browser runs show what differs on purpose.
This commit is contained in:
1 parent
9c2a2d6951
commit
60f9de8511
47 files changed
+1788
-312
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import { createSettledState } from '../../src/app/qaProbe.js';
|
||||
|
||||
/** A CPU-ready pick can precede the GPU hit delivered for a real click. */
|
||||
export async function clickUntilSelected(page, click, selected) {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
await click();
|
||||
let handle;
|
||||
try {
|
||||
handle = await page.waitForFunction(selected, { timeout: 2000 });
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.name !== 'TimeoutError') throw error;
|
||||
if (attempt === 4)
|
||||
throw new Error('Readiness timeout: selection after 5 clicks', {
|
||||
cause: error,
|
||||
});
|
||||
} finally {
|
||||
await handle?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Viewport CSS coordinates (or a page-side locator); predicate runs beside the picker. */
|
||||
export async function whenPickable(
|
||||
page,
|
||||
windowPosition,
|
||||
predicate,
|
||||
{ timeoutMs = 30000 } = {},
|
||||
) {
|
||||
const position =
|
||||
typeof windowPosition === 'function'
|
||||
? `(${windowPosition})()`
|
||||
: JSON.stringify(windowPosition);
|
||||
let handle;
|
||||
try {
|
||||
handle = await page.waitForFunction(
|
||||
`(() => {
|
||||
const engine = window.__godsEyeView?.engine;
|
||||
if (!engine || engine.isDestroyed()) return false;
|
||||
engine.render.request('qa-pick-readiness');
|
||||
const position = ${position};
|
||||
if (!position) return false;
|
||||
const box = engine.host.canvas.getBoundingClientRect();
|
||||
const picked = engine.picker.pick({
|
||||
x: position.x - box.left,
|
||||
y: position.y - box.top,
|
||||
});
|
||||
return (${predicate})(picked, position) ? position : false;
|
||||
})()`,
|
||||
{ polling: 50, timeout: timeoutMs },
|
||||
);
|
||||
return await handle.jsonValue();
|
||||
} catch (error) {
|
||||
if (error.name === 'TimeoutError')
|
||||
throw new Error('Readiness timeout: pickable target', { cause: error });
|
||||
throw error;
|
||||
} finally {
|
||||
await handle?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/** Wait for the restored camera over drawn frames, including request-mode globes. */
|
||||
export async function whenSettled(
|
||||
page,
|
||||
{ frames = 3, timeoutMs = 120000 } = {},
|
||||
) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
await page.waitForFunction(() => window.__godsEyeView?.engine, {
|
||||
timeout: timeoutMs,
|
||||
polling: 50,
|
||||
});
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Readiness timeout: engineStarted, initialViewRestored, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable',
|
||||
);
|
||||
}
|
||||
return page.evaluate(
|
||||
`(${waitForSettled})(${createSettledState}, ${JSON.stringify({ frames, timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)) })})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Self-contained so production pages need no QA module. */
|
||||
export function waitForSettled(createState, { frames, timeoutMs }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const state = createState(engine);
|
||||
let finished = false;
|
||||
let remove = () => {};
|
||||
const finish = (error, result) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
state.destroy();
|
||||
if (error) reject(error);
|
||||
else resolve(result);
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
const status = state.settled({ frames });
|
||||
if (window.__godsEyeView?.engine !== engine)
|
||||
status.unmet.push('engineUnchanged');
|
||||
finish(
|
||||
new Error(
|
||||
`Readiness timeout: ${status.unmet.join(', ')}${status.restoreError ? ': ' + status.restoreError : ''}`,
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
remove = engine.frames.postRender.addEventListener(() => {
|
||||
const status = state.settled({ frames });
|
||||
if (window.__godsEyeView?.engine !== engine)
|
||||
return finish(new Error('Readiness interrupted: engineChanged'));
|
||||
if (status.settled) finish(null, status);
|
||||
else
|
||||
queueMicrotask(() => {
|
||||
if (!finished && !engine.isDestroyed())
|
||||
engine.render.request('qa-readiness');
|
||||
});
|
||||
});
|
||||
engine.render.request('qa-readiness');
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user