mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
The frame lifecycle keeps its request state in an object instead of the image's dataset and publishes it; the panel binds the image's src, class and data attributes and the wrapper's loading and has-frame classes from it, so preload tokens, kept pixels on a failed refresh and clearing on a camera change are unchanged. The source badge is the pure presentCctvSourceBadge(), the live switch and the typed summary are published too. The video surface still draws into the panel's canvas.
279 lines
9.9 KiB
JavaScript
279 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Badge acceptance on dev/preview. Camera state is a disposable controller fixture. */
|
|
import fs from 'node:fs';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
|
});
|
|
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 &&
|
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 60000 },
|
|
);
|
|
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 browser.close();
|
|
}
|
|
console.log(`${checks} checks; ${failures} failures`);
|
|
process.exitCode = failures ? 1 : 0;
|