mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 05:12:23 +00:00
Layer display tests no longer re-check Cesium primitives in every layer: they run on the fake engine, and one Cesium translation check per ground kind lives in the Cesium engine's ground tests. Their harnesses are shared, and the fake-engine, Cesium-engine and flights tests that repeated the contract suite are gone, 340 tests in all. The press-travel measurement is shared by the selection dispatcher and CCTV, which loses its ScreenSpaceEventHandler shim and world-click module. ALPR, installations and bikeshare share one selection keeper and selected marker write. Layers with their own click rules keep their handlers. Navara's pulse is a class like the ground pass, the ground and imagery passes share the surface uniforms, and the colour, log-depth and occlusion GLSL lives in one chunk module, each caller keeping its depth bias. The ground suite runs inside the contract suite (--only=ground:), and fixture and script names say what they test. Behaviour is unchanged on both engines.
246 lines
7.5 KiB
JavaScript
246 lines
7.5 KiB
JavaScript
/**
|
|
* Evidence the browser QA gates collect while they run and save under
|
|
* `qa-artifacts/<gate>/` when they fail (CI uploads that directory). Tokens
|
|
* and keys are masked before anything is printed or saved.
|
|
*/
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
/** Console messages and failed requests kept per page. */
|
|
const EVIDENCE_LIMIT = 200;
|
|
|
|
/** Cesium's console line when the render loop throws (CesiumWidget). */
|
|
const RENDER_ERROR_PATTERN = /An error occurred while rendering/i;
|
|
|
|
/** Mask tokens in a URL or message before it is printed or saved. */
|
|
export function redact(text) {
|
|
return String(text ?? '')
|
|
.replace(
|
|
/([?&](?:access_token|token|key|api_key|apikey|client_secret|signature)=)[^&\s"']*/gi,
|
|
'$1REDACTED',
|
|
)
|
|
.replace(/MLY\|[^\s&"',)]+/g, 'MLY|REDACTED')
|
|
.replace(/AIza[\w-]{30,}/g, 'AIza…REDACTED');
|
|
}
|
|
|
|
/** The standalone Vite preview has no setup API; opt in explicitly. */
|
|
export function isExpectedPreviewSetupError(
|
|
message,
|
|
{ preview = false, baseUrl } = {},
|
|
) {
|
|
if (
|
|
!preview ||
|
|
message.type() !== 'error' ||
|
|
!/^Failed to load resource: .*\b404\b/.test(message.text())
|
|
)
|
|
return false;
|
|
try {
|
|
const url = new URL(message.location()?.url);
|
|
return (
|
|
url.origin === new URL(baseUrl).origin &&
|
|
url.pathname === '/api/setup/status'
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The runtime evidence gate shares one strict runtime verdict across its main page and
|
|
// every share-link page. Response diagnostics never retain query credentials.
|
|
export function watchBrowserPage(
|
|
page,
|
|
report,
|
|
{ name, preview = false, baseUrl },
|
|
) {
|
|
const errors = (report.browserErrors ??= []);
|
|
report.expectedPreviewErrors ??= [];
|
|
report.failedResponses ??= [];
|
|
(report.runtimePages ??= []).push(name);
|
|
const resourcePath = (value) => {
|
|
try {
|
|
const url = new URL(value);
|
|
return redact(url.origin + url.pathname);
|
|
} catch {
|
|
return '(location unavailable)';
|
|
}
|
|
};
|
|
page.on('console', (message) => {
|
|
if (isExpectedPreviewSetupError(message, { preview, baseUrl })) {
|
|
report.expectedPreviewErrors.push('/api/setup/status: 404');
|
|
return;
|
|
}
|
|
if (message.type() === 'error')
|
|
errors.push(
|
|
`[${name}] ${redact(message.text())} — ${resourcePath(message.location()?.url)}`,
|
|
);
|
|
});
|
|
for (const event of ['pageerror', 'error'])
|
|
page.on(event, (error) =>
|
|
errors.push(`[${name}] ${redact(error.message)}`),
|
|
);
|
|
page.on('response', (response) => {
|
|
if (
|
|
response.status() >= 400 &&
|
|
report.failedResponses.length < EVIDENCE_LIMIT
|
|
)
|
|
report.failedResponses.push({
|
|
page: name,
|
|
status: response.status(),
|
|
resource: resourcePath(response.url()),
|
|
});
|
|
});
|
|
}
|
|
|
|
function push(list, entry) {
|
|
list.push(entry);
|
|
if (list.length > EVIDENCE_LIMIT)
|
|
list.splice(0, list.length - EVIDENCE_LIMIT);
|
|
}
|
|
|
|
/**
|
|
* Subscribe to `viewer.scene.renderError` in every document the page loads
|
|
* (reloads included), into `window.__qaRenderErrors`. Call before `goto`.
|
|
*/
|
|
export function hookRenderErrors(page) {
|
|
return page.evaluateOnNewDocument(() => {
|
|
window.__qaRenderErrors = [];
|
|
const hook = () => {
|
|
const scene = window.__godsEyeView?.viewer?.scene;
|
|
if (!scene?.renderError) return false;
|
|
scene.renderError.addEventListener((_, error) =>
|
|
window.__qaRenderErrors.push(
|
|
String(error?.stack || error?.message || error),
|
|
),
|
|
);
|
|
return true;
|
|
};
|
|
const timer = setInterval(() => {
|
|
if (hook()) clearInterval(timer);
|
|
}, 250);
|
|
});
|
|
}
|
|
|
|
export async function readRenderErrors(page) {
|
|
try {
|
|
return await page.evaluate(() => window.__qaRenderErrors ?? []);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watch one page's console, errors and failed requests. `errors` is shared
|
|
* across pages; `isError(text)` adds console lines a gate treats as errors.
|
|
* @returns {{name: string, page: object, console: Array<object>, failed: Array<object>, renderErrors: Array<string>}}
|
|
*/
|
|
export function watchPage(page, { name, errors, isError = () => false }) {
|
|
const monitor = { name, page, console: [], failed: [], renderErrors: [] };
|
|
page.on('console', (message) => {
|
|
const text = redact(message.text());
|
|
push(monitor.console, { type: message.type(), text });
|
|
if (message.type() === 'error' && RENDER_ERROR_PATTERN.test(text)) {
|
|
monitor.renderErrors.push(text);
|
|
errors.push(`[${name}] render loop: ${text}`);
|
|
} else if (isError(text)) errors.push(`[${name}] ${text}`);
|
|
});
|
|
page.on('pageerror', (error) => {
|
|
const text = redact(error?.stack || error?.message || error);
|
|
push(monitor.console, { type: 'pageerror', text });
|
|
errors.push(`[${name}] ${text}`);
|
|
});
|
|
page.on('requestfailed', (request) =>
|
|
push(monitor.failed, {
|
|
url: redact(request.url()),
|
|
method: request.method(),
|
|
failure: request.failure()?.errorText ?? 'failed',
|
|
}),
|
|
);
|
|
page.on('response', (response) => {
|
|
if (response.status() < 400) return;
|
|
push(monitor.failed, {
|
|
url: redact(response.url()),
|
|
method: response.request().method(),
|
|
status: response.status(),
|
|
});
|
|
});
|
|
return monitor;
|
|
}
|
|
|
|
const withTimeout = (promise, ms, label) =>
|
|
Promise.race([
|
|
promise,
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error(`${label} timed out`)), ms),
|
|
),
|
|
]);
|
|
|
|
function streetLevelState(page) {
|
|
return page.evaluate(() => {
|
|
const module =
|
|
window.__godsEyeView?.dataManager?.layers?.get('street-level')?.module;
|
|
return module?.getUIState ? module.getUIState() : null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save a screenshot and JSON record per page, plus the error. A page that
|
|
* cannot be captured records why instead.
|
|
* @returns {Promise<string>} the directory written
|
|
*/
|
|
export async function saveFailureArtifacts({ dir, browser, monitors, error }) {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(dir, 'error.txt'),
|
|
redact(error?.stack || error?.message || String(error)),
|
|
);
|
|
const known = new Map(monitors.map((monitor) => [monitor.page, monitor]));
|
|
let pages = [];
|
|
try {
|
|
pages = await browser.pages();
|
|
} catch {
|
|
pages = monitors.map((monitor) => monitor.page);
|
|
}
|
|
let index = 0;
|
|
for (const page of pages) {
|
|
const monitor = known.get(page);
|
|
// The browser's first blank tab holds nothing worth saving.
|
|
if (!monitor && page.url() === 'about:blank') continue;
|
|
const base = `${String(index++).padStart(2, '0')}-${monitor?.name ?? 'page'}`;
|
|
const record = { url: redact(page.url()) };
|
|
try {
|
|
await withTimeout(
|
|
page.screenshot({
|
|
path: path.join(dir, `${base}.png`),
|
|
fullPage: true,
|
|
}),
|
|
30_000,
|
|
'screenshot',
|
|
);
|
|
} catch (shotError) {
|
|
record.screenshotError = String(shotError?.message || shotError);
|
|
}
|
|
try {
|
|
record.streetLevel = await withTimeout(
|
|
streetLevelState(page),
|
|
10_000,
|
|
'getUIState',
|
|
);
|
|
} catch (stateError) {
|
|
record.streetLevelError = String(stateError?.message || stateError);
|
|
}
|
|
record.renderErrors = [
|
|
...(monitor?.renderErrors ?? []),
|
|
...(await withTimeout(readRenderErrors(page), 10_000, 'render errors')
|
|
.then((list) => list.map((text) => `scene.renderError: ${text}`))
|
|
.catch(() => [])),
|
|
].map(redact);
|
|
record.console = monitor?.console ?? [];
|
|
record.failedRequests = monitor?.failed ?? [];
|
|
await fs.writeFile(
|
|
path.join(dir, `${base}.json`),
|
|
redact(JSON.stringify(record, null, 2)),
|
|
);
|
|
}
|
|
return dir;
|
|
}
|