mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Each script launches and closes Chrome through scripts/qa/browser.mjs, waits on the application through scripts/qa/ready.mjs instead of the loading screen and fixed pauses, and reads the contract instead of Cesium where the check is not about Cesium itself.
279 lines
9.8 KiB
JavaScript
279 lines
9.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Badge acceptance on dev/preview. Camera state is a disposable controller fixture. */
|
|
import { whenSettled } from './qa/ready.mjs';
|
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
|
import fs from 'node:fs';
|
|
|
|
const browser = await launchBrowser({
|
|
headless: true,
|
|
args: [],
|
|
});
|
|
const page = await browser.newPage();
|
|
page.setDefaultNavigationTimeout(60000);
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
let checks = 0;
|
|
let failures = 0;
|
|
const check = (name, passed, detail) => {
|
|
checks++;
|
|
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
|
|
if (!passed) {
|
|
failures++;
|
|
console.log(JSON.stringify(detail));
|
|
}
|
|
};
|
|
const out = 'qa-shots/status-badges';
|
|
fs.mkdirSync(out, { recursive: true });
|
|
try {
|
|
for (const lang of ['en', 'ru']) {
|
|
await page.setViewport({ width: 1440, height: 900 });
|
|
await page.goto(
|
|
`${process.env.QA_BASE_URL || 'http://127.0.0.1:4189'}/?welcome=0&lang=${lang}`,
|
|
{ waitUntil: 'domcontentloaded' },
|
|
);
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView?.styleManager?._cctvControls,
|
|
{ timeout: 60000 },
|
|
);
|
|
await whenSettled(page);
|
|
await page.evaluate(async () => {
|
|
const { dataManager, styleManager: manager } = window.__godsEyeView;
|
|
await dataManager.setEnabled('wind', true);
|
|
manager.setPanelCollapsed('weather-panel', false, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
manager.setPanelCollapsed('cctv-panel', false, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
const owner = manager._cctvControls;
|
|
owner._cctvUnsubscribe?.();
|
|
owner._cctvUnsubscribe = null;
|
|
owner.actions = { ...owner.actions, isEnabled: () => true };
|
|
owner._renderCctvState({
|
|
enabled: true,
|
|
activeCameraId: 'badge-fixture',
|
|
cameras: [{ id: 'badge-fixture', city: 'QA', name: 'Badge fixture' }],
|
|
activeCamera: {
|
|
id: 'badge-fixture',
|
|
city: 'QA',
|
|
headingDeg: 30,
|
|
pitchDeg: -10,
|
|
fovDeg: 45,
|
|
rangeM: 120,
|
|
mountHeightM: 8,
|
|
sourceKind: 'snapshot',
|
|
sourceStatus: 'degraded',
|
|
calBadge: 'curated',
|
|
calDirty: false,
|
|
},
|
|
});
|
|
});
|
|
await page.waitForSelector('.weather-coverage.gev-status-badge');
|
|
const inventory = await page.evaluate(() => {
|
|
const source = document.getElementById('cctv-source-badge');
|
|
const quality = document.getElementById('cctv-quality-chip');
|
|
const coverage = [...document.querySelectorAll('.weather-coverage')];
|
|
return {
|
|
shared: [source, quality, ...coverage].every((node) =>
|
|
node.classList.contains('gev-status-badge'),
|
|
),
|
|
live:
|
|
source.getAttribute('role') === 'status' &&
|
|
source.getAttribute('aria-live') === 'polite',
|
|
silent: [quality, ...coverage].every(
|
|
(node) =>
|
|
!node.hasAttribute('aria-live') &&
|
|
!node.hasAttribute('role') &&
|
|
!node.hasAttribute('tabindex'),
|
|
),
|
|
coverage:
|
|
coverage.length > 0 &&
|
|
coverage.every(
|
|
(node) =>
|
|
node.textContent &&
|
|
!node.hidden &&
|
|
node.classList.contains('gev-status-badge--plain'),
|
|
),
|
|
source: source.textContent,
|
|
};
|
|
});
|
|
check(
|
|
`${lang}: CCTV and populated Weather badges share the view`,
|
|
inventory.shared && inventory.coverage,
|
|
inventory,
|
|
);
|
|
check(
|
|
`${lang}: existing announcement and read-only semantics remain intact`,
|
|
inventory.live && inventory.silent,
|
|
inventory,
|
|
);
|
|
check(
|
|
`${lang}: source wording uses the active locale`,
|
|
lang === 'en'
|
|
? inventory.source === 'SNAPSHOT · DEGRADED'
|
|
: inventory.source !== 'SNAPSHOT · DEGRADED',
|
|
inventory,
|
|
);
|
|
|
|
for (const state of ['loading', 'error', 'retained', 'ready', 'idle']) {
|
|
const result = await page.evaluate(async (state) => {
|
|
const owner = window.__godsEyeView.styleManager._cctvControls;
|
|
const frame = owner._frame;
|
|
owner._frame = {
|
|
...frame,
|
|
loading: state === 'loading',
|
|
error: ['error', 'retained'].includes(state),
|
|
hasFrame: state === 'retained',
|
|
};
|
|
owner._syncCctvSourceBadge(
|
|
owner._cctvState.activeCamera,
|
|
state !== 'idle',
|
|
);
|
|
owner._frame = frame;
|
|
// The panel view renders the badge on Vue's next flush.
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const badge = document.getElementById('cctv-source-badge');
|
|
return {
|
|
state: badge.dataset.frameState,
|
|
text: badge.textContent,
|
|
color: getComputedStyle(badge).color,
|
|
};
|
|
}, state);
|
|
const expected = {
|
|
loading: ['loading', 'rgb(255, 229, 155)'],
|
|
error: ['error', 'rgb(255, 176, 176)'],
|
|
retained: ['ready', 'rgb(157, 239, 255)'],
|
|
ready: ['ready', 'rgb(157, 239, 255)'],
|
|
idle: ['idle', 'rgb(157, 239, 255)'],
|
|
}[state];
|
|
check(
|
|
`${lang}: ${state} keeps its frame state and palette`,
|
|
result.state === expected[0] &&
|
|
result.color === expected[1] &&
|
|
(!['ready', 'retained'].includes(state) ||
|
|
result.text === inventory.source),
|
|
result,
|
|
);
|
|
}
|
|
for (const [calBadge, dirty, color] of [
|
|
['raw-prior', false, 'rgb(143, 231, 255)'],
|
|
['curated', false, 'rgb(255, 230, 162)'],
|
|
['calibrated', false, 'rgb(173, 255, 191)'],
|
|
['curated', true, 'rgb(255, 208, 138)'],
|
|
]) {
|
|
const result = await page.evaluate(
|
|
async ({ calBadge, dirty }) => {
|
|
const owner = window.__godsEyeView.styleManager._cctvControls;
|
|
document.getElementById('cctv-next-btn').focus();
|
|
owner._renderCctvState({
|
|
...owner._cctvState,
|
|
activeCamera: {
|
|
...owner._cctvState.activeCamera,
|
|
calBadge,
|
|
calDirty: dirty,
|
|
},
|
|
});
|
|
// The panel view renders the chip on Vue's next flush.
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const badge = document.getElementById('cctv-quality-chip');
|
|
return {
|
|
state: badge.dataset.calBadge,
|
|
color: getComputedStyle(badge).color,
|
|
focus: document.activeElement.id,
|
|
};
|
|
},
|
|
{ calBadge, dirty },
|
|
);
|
|
check(
|
|
`${lang}: ${dirty ? 'edited' : calBadge} keeps provenance color and control focus`,
|
|
result.state === (dirty ? 'edited' : calBadge) &&
|
|
result.color === color &&
|
|
result.focus === 'cctv-next-btn',
|
|
result,
|
|
);
|
|
}
|
|
for (const theme of ['tactical', 'cyber']) {
|
|
await page.evaluate(
|
|
(theme) => window.__godsEyeView.styleManager._setHudVariant(theme),
|
|
theme,
|
|
);
|
|
for (const width of [1440, 620, 390]) {
|
|
await page.setViewport({ width, height: 900 });
|
|
for (const panelId of ['cctv-panel', 'weather-panel']) {
|
|
await page.evaluate(async (panelId) => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
for (const other of ['cctv-panel', 'weather-panel']) {
|
|
if (other !== panelId)
|
|
manager.setPanelCollapsed(other, true, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
}
|
|
manager.setPanelCollapsed(panelId, false, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
await new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
}, panelId);
|
|
const layout = await page.evaluate((panelId) => {
|
|
const selector =
|
|
panelId === 'cctv-panel'
|
|
? '#cctv-source-badge, #cctv-quality-chip'
|
|
: '.weather-coverage';
|
|
return [...document.querySelectorAll(selector)].map((node) => {
|
|
const rect = node.getBoundingClientRect();
|
|
const panel = node.closest(`#${panelId}`).getBoundingClientRect();
|
|
return {
|
|
id: node.id || 'weather-coverage',
|
|
text: node.textContent,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
scroll: node.scrollWidth,
|
|
client: node.clientWidth,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
panelLeft: panel.left,
|
|
panelRight: panel.right,
|
|
fits:
|
|
rect.width > 0 &&
|
|
rect.height > 0 &&
|
|
rect.left >= panel.left - 1 &&
|
|
rect.right <= panel.right + 1 &&
|
|
node.scrollWidth <= node.clientWidth + 1,
|
|
};
|
|
});
|
|
}, panelId);
|
|
check(
|
|
`${lang}/${theme}: ${panelId} badges fit at ${width}px`,
|
|
layout.length >= (panelId === 'cctv-panel' ? 2 : 1) &&
|
|
layout.every((node) => node.fits),
|
|
layout,
|
|
);
|
|
if (width === 390)
|
|
await page.screenshot({
|
|
path: `${out}/${lang}-${theme}-${panelId}-${width}.png`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const disposed = await page.evaluate(() => {
|
|
const owner = window.__godsEyeView.styleManager._cctvControls;
|
|
const badge = document.getElementById('cctv-quality-chip');
|
|
const text = badge.textContent;
|
|
owner.destroy();
|
|
owner._renderCctvState(null);
|
|
return badge.textContent === text;
|
|
});
|
|
check(`${lang}: disposal prevents late state painting`, disposed);
|
|
}
|
|
check('no uncaught browser errors', errors.length === 0, errors);
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
console.log(`${checks} checks; ${failures} failures`);
|
|
process.exitCode = failures ? 1 : 0;
|