mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Trim the doc text this PR adds (README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES, SECURITY, DATA_SOURCES, .env.example, the CI job comment), drop code comments that restate the code, cut design-note JSDoc to a line or two, and drop review tags from test names. Comments only; no code changes. Also: README key count (seven keys, two metered), CURRENT-STATE's gate paragraph (CI runs a production build with a dummy token), the stacked doc above meshBoxInRange and the orphaned inMeshRange line in coverage.js, and a duplicate ALPR row in DATA_SOURCES that a merge had brought back. Co-Authored-By: Claude Opus 5.5 <[email protected]>
320 lines
9.4 KiB
JavaScript
320 lines
9.4 KiB
JavaScript
/**
|
|
* Header press-and-verify for the QA gates that lift a rail panel into a
|
|
* floating window and dock it again. The rail animates panel heights, so a
|
|
* point read once can be under another panel by the time the pointer goes
|
|
* down. Only a provable miss (a recorded press outside the header) is
|
|
* retried, with a GitHub `::warning::`. With QA_FAIL_ON_RETRY=1 a miss the
|
|
* panel's own movement cannot explain fails the run instead.
|
|
*/
|
|
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
/** Retries after the first attempt, and the pause before each. */
|
|
export const PRESS_RETRIES = 2;
|
|
const RETRY_PAUSE_MS = 1000;
|
|
|
|
/** Whether a retry fails the run (QA_FAIL_ON_RETRY=1 or --fail-on-retry). */
|
|
export function failOnRetryDefault(
|
|
env = process.env,
|
|
argv = process.argv.slice(2),
|
|
) {
|
|
return env.QA_FAIL_ON_RETRY === '1' || argv.includes('--fail-on-retry');
|
|
}
|
|
|
|
/**
|
|
* A failed attempt whose press landed outside the header. No recorded press
|
|
* is a failure, not a miss: a retry would only hide it.
|
|
* @param {{done: boolean, pressed: {inHeader: boolean}|null}} attempt
|
|
*/
|
|
export function pressMissed(attempt) {
|
|
return !attempt.done && attempt.pressed?.inHeader === false;
|
|
}
|
|
|
|
/** Whether the panel's box changed by more than 2 px between two reads. */
|
|
export function panelMoved(before, after) {
|
|
if (!before || !after) return false;
|
|
return ['left', 'top', 'width', 'height'].some(
|
|
(key) => Math.abs((after[key] ?? NaN) - (before[key] ?? NaN)) > 2,
|
|
);
|
|
}
|
|
|
|
/** The panel's box, rounded, as `left,top widthxheight`. */
|
|
export function formatBox(box) {
|
|
if (!box) return 'unknown';
|
|
const r = (value) => Math.round(value ?? NaN);
|
|
return `${r(box.left)},${r(box.top)} ${r(box.width)}x${r(box.height)}`;
|
|
}
|
|
|
|
function panelBox(page, panelId) {
|
|
return page.evaluate((id) => {
|
|
const r = document.getElementById(id).getBoundingClientRect();
|
|
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
}, panelId);
|
|
}
|
|
|
|
export function headerPoint(page, panelId) {
|
|
return page.evaluate((id) => {
|
|
const title = document.querySelector(`#${id} .panel-header .panel-title`);
|
|
const r = title.getBoundingClientRect();
|
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
}, panelId);
|
|
}
|
|
|
|
/** Name the element at a point, and whether it is the panel's header. */
|
|
export function hitAt(page, point, panelId) {
|
|
return page.evaluate(
|
|
({ x, y, id }) => {
|
|
const node = document.elementFromPoint(x, y);
|
|
if (!node) return { target: 'nothing', inHeader: false };
|
|
return {
|
|
target: `${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}.${[...node.classList].join('.')}`,
|
|
inHeader: Boolean(node.closest(`#${id} .panel-header`)),
|
|
};
|
|
},
|
|
{ ...point, id: panelId },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Re-resolve the header point until it hits the header or the wait runs out
|
|
* (the press then records the miss).
|
|
*/
|
|
export async function headerTarget(
|
|
page,
|
|
panelId,
|
|
{ tries = 20, intervalMs = 500 } = {},
|
|
) {
|
|
let point = await headerPoint(page, panelId);
|
|
let hit = await hitAt(page, point, panelId);
|
|
for (let i = 0; i < tries && !hit.inHeader; i++) {
|
|
await delay(intervalMs);
|
|
point = await headerPoint(page, panelId);
|
|
hit = await hitAt(page, point, panelId);
|
|
}
|
|
const box = await panelBox(page, panelId);
|
|
return { point, hit, timedOut: !hit.inHeader, box };
|
|
}
|
|
|
|
/**
|
|
* Best effort: wait until the panel's box has held still for `stillMs`, so
|
|
* the header does not move between the hit check and the press.
|
|
*/
|
|
export async function waitForStill(
|
|
page,
|
|
panelId,
|
|
{ stillMs = 300, timeout = 5000 } = {},
|
|
) {
|
|
try {
|
|
await page.waitForFunction(
|
|
(id, ms) => {
|
|
const r = document.getElementById(id).getBoundingClientRect();
|
|
const box = [r.left, r.top, r.width, r.height].map(Math.round).join();
|
|
const now = performance.now();
|
|
if (window.__qaStillBox !== box) {
|
|
window.__qaStillBox = box;
|
|
window.__qaStillSince = now;
|
|
}
|
|
return now - window.__qaStillSince >= ms;
|
|
},
|
|
{ polling: 100, timeout },
|
|
panelId,
|
|
stillMs,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
if (error?.name !== 'TimeoutError') throw error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Record the next pointerdown's target and the panel's box at that moment. */
|
|
export function recordNextPress(page, panelId) {
|
|
return page.evaluate((id) => {
|
|
window.__qaPressBox = null;
|
|
window.addEventListener(
|
|
'pointerdown',
|
|
(event) => {
|
|
const r = document.getElementById(id).getBoundingClientRect();
|
|
const target = event.target;
|
|
window.__qaPressBox = {
|
|
left: r.left,
|
|
top: r.top,
|
|
width: r.width,
|
|
height: r.height,
|
|
target: `${target.tagName?.toLowerCase()}${target.id ? `#${target.id}` : ''}.${[...(target.classList || [])].join('.')}`,
|
|
inHeader: Boolean(target.closest?.(`#${id} .panel-header`)),
|
|
};
|
|
},
|
|
{ capture: true, once: true },
|
|
);
|
|
}, panelId);
|
|
}
|
|
|
|
function readPress(page) {
|
|
return page.evaluate(() => window.__qaPressBox ?? null);
|
|
}
|
|
|
|
export async function waitForFloating(
|
|
page,
|
|
panelId,
|
|
floating,
|
|
{ timeout = 3000 } = {},
|
|
) {
|
|
try {
|
|
await page.waitForFunction(
|
|
(id, want) =>
|
|
document.getElementById(id).classList.contains('panel-floating') ===
|
|
want,
|
|
{ polling: 50, timeout },
|
|
panelId,
|
|
floating,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
if (error?.name !== 'TimeoutError') throw error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Two paints: by then every layout change an input caused is on screen. */
|
|
export function nextFrames(page) {
|
|
return page.evaluate(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function dragPointer(page, from, dx, dy, { steps = 6 } = {}) {
|
|
await page.mouse.move(from.x, from.y);
|
|
await page.mouse.down();
|
|
await page.mouse.move(from.x + dx / 2, from.y + dy / 2, { steps });
|
|
await page.mouse.move(from.x + dx, from.y + dy, { steps });
|
|
await page.mouse.up();
|
|
await nextFrames(page);
|
|
}
|
|
|
|
export function describeRetry(
|
|
panelId,
|
|
attempt,
|
|
{ round, retries, extra = '' },
|
|
) {
|
|
const { hit, pressed, timedOut, still, box } = attempt;
|
|
return [
|
|
`retry ${round + 1}/${retries}: the press missed the ${panelId} header (on ${pressed?.target ?? hit.target})`,
|
|
`pre-press hit check ${timedOut ? `timed out (on ${hit.target})` : 'passed'}`,
|
|
`stillness ${still === false ? 'timed out' : 'held'}`,
|
|
`panel box at hit check ${formatBox(box)} -> at press ${formatBox(pressed)}`,
|
|
]
|
|
.concat(extra ? [extra] : [])
|
|
.join('; ');
|
|
}
|
|
|
|
/** Retry `press` only while it provably misses the header. */
|
|
async function pressUntilDecided(
|
|
page,
|
|
panelId,
|
|
press,
|
|
{
|
|
retries,
|
|
log,
|
|
note,
|
|
pauseMs = RETRY_PAUSE_MS,
|
|
failOnRetry = failOnRetryDefault(),
|
|
},
|
|
) {
|
|
for (let round = 0; ; round++) {
|
|
const still = await waitForStill(page, panelId);
|
|
await recordNextPress(page, panelId);
|
|
const { point, hit, timedOut, box } = await headerTarget(page, panelId);
|
|
const done = await press(point);
|
|
const attempt = {
|
|
point,
|
|
hit,
|
|
timedOut,
|
|
still,
|
|
box,
|
|
done,
|
|
pressed: await readPress(page),
|
|
retries: round,
|
|
};
|
|
if (round >= retries || !pressMissed(attempt)) return attempt;
|
|
const why = describeRetry(panelId, attempt, {
|
|
round,
|
|
retries,
|
|
extra: note ? await note() : '',
|
|
});
|
|
if (failOnRetry && !panelMoved(box, attempt.pressed))
|
|
throw new Error(
|
|
`QA_FAIL_ON_RETRY=1 turns this retry into a failure: ${why}`,
|
|
);
|
|
log(`note: ${why}; retrying`);
|
|
log(`::warning title=Panel press retried::${why}`);
|
|
await delay(pauseMs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lift a docked panel into a floating window with a header drag.
|
|
* @returns {Promise<{point: {x: number, y: number}, hit: {target: string, inHeader: boolean}, done: boolean, pressed: object|null}>}
|
|
*/
|
|
export function liftPanelByHeader(
|
|
page,
|
|
panelId,
|
|
{
|
|
dx,
|
|
dy,
|
|
retries = PRESS_RETRIES,
|
|
log = console.log,
|
|
note,
|
|
pauseMs,
|
|
failOnRetry,
|
|
} = {},
|
|
) {
|
|
return pressUntilDecided(
|
|
page,
|
|
panelId,
|
|
async (point) => {
|
|
await dragPointer(page, point, dx, dy);
|
|
return waitForFloating(page, panelId, true);
|
|
},
|
|
{ retries, log, note, pauseMs, failOnRetry },
|
|
);
|
|
}
|
|
|
|
/** Dock a floating panel with a header double-click; same result as lifting. */
|
|
export function dockPanelByDoubleClick(
|
|
page,
|
|
panelId,
|
|
{
|
|
retries = PRESS_RETRIES,
|
|
log = console.log,
|
|
note,
|
|
pauseMs,
|
|
failOnRetry,
|
|
} = {},
|
|
) {
|
|
return pressUntilDecided(
|
|
page,
|
|
panelId,
|
|
async (point) => {
|
|
await page.mouse.click(point.x, point.y, { clickCount: 1 });
|
|
await page.mouse.click(point.x, point.y, { clickCount: 2 });
|
|
return waitForFloating(page, panelId, false);
|
|
},
|
|
{ retries, log, note, pauseMs, failOnRetry },
|
|
);
|
|
}
|
|
|
|
/** Where an attempt pressed, for an assertion message. */
|
|
export function describePress(attempt) {
|
|
const { point, hit, pressed } = attempt;
|
|
const where = `pressed ${Math.round(point.x)},${Math.round(point.y)}`;
|
|
if (!pressed)
|
|
return `${where}: no pointerdown reached the page (hit check on ${hit.target}, in header: ${hit.inHeader})`;
|
|
const n = attempt.retries || 0;
|
|
const retried = n ? `, after ${n} ${n === 1 ? 'retry' : 'retries'}` : '';
|
|
return `${where} on ${pressed.target}, in header: ${pressed.inHeader}${retried}`;
|
|
}
|