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.
349 lines
11 KiB
JavaScript
349 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Run against a dev server or built preview with QA_BASE_URL (default :4179). */
|
|
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();
|
|
const errors = [];
|
|
const failures = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
const check = (name, passed, detail = '') => {
|
|
console.log(
|
|
`[${passed ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`,
|
|
);
|
|
if (!passed) failures.push(name);
|
|
};
|
|
const base = process.env.QA_BASE_URL || 'http://localhost:4179';
|
|
const shots = process.env.QA_SHOTS_DIR || 'qa-shots/segmented-controls';
|
|
fs.mkdirSync(shots, { recursive: true });
|
|
|
|
try {
|
|
await page.setViewport({ width: 1440, height: 900 });
|
|
await page.goto(`${base}/?welcome=0`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
|
|
{ timeout: 60000 },
|
|
);
|
|
await whenSettled(page);
|
|
// DISPLAY holds most segmented controls; Draw's shape group is in UTILITIES.
|
|
await page.evaluate(() => {
|
|
for (const panel of ['pp-toggles', 'utilities-panel'])
|
|
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
});
|
|
|
|
const surface = await page.evaluate(() => {
|
|
// Colour swatches are one-of-many too, but chips, not segments: they are
|
|
// checked on their own below.
|
|
const groups = [
|
|
...document.querySelectorAll('[role="radiogroup"]:not(.draw-colors)'),
|
|
];
|
|
const buttons = groups.flatMap((group) => [
|
|
...group.querySelectorAll('[role="radio"]'),
|
|
]);
|
|
return {
|
|
groups: groups.length,
|
|
buttons: buttons.length,
|
|
shared:
|
|
groups.every((g) => g.classList.contains('gev-segmented')) &&
|
|
buttons.every((b) => {
|
|
const style = getComputedStyle(b);
|
|
return (
|
|
b.classList.contains('gev-segment') &&
|
|
style.fontSize === '9px' &&
|
|
style.minHeight === '24px' &&
|
|
style.borderRadius === '0px'
|
|
);
|
|
}),
|
|
};
|
|
});
|
|
check(
|
|
'all six one-of-many groups use the shared surface',
|
|
surface.groups === 6 && surface.buttons === 15 && surface.shared,
|
|
JSON.stringify(surface),
|
|
);
|
|
|
|
const swatches = await page.evaluate(() => {
|
|
const group = document.getElementById('draw-colors');
|
|
const chips = [...(group?.querySelectorAll('[role="radio"]') || [])];
|
|
return {
|
|
count: chips.length,
|
|
round: chips.every(
|
|
(chip) => getComputedStyle(chip).borderRadius === '50%',
|
|
),
|
|
colours: new Set(
|
|
chips.map((chip) => getComputedStyle(chip).backgroundColor),
|
|
).size,
|
|
checked: chips.filter(
|
|
(chip) => chip.getAttribute('aria-checked') === 'true',
|
|
).length,
|
|
};
|
|
});
|
|
check(
|
|
"Draw's colour swatches are five round chips, one checked",
|
|
swatches.count === 5 &&
|
|
swatches.round &&
|
|
swatches.colours === 5 &&
|
|
swatches.checked === 1,
|
|
JSON.stringify(swatches),
|
|
);
|
|
|
|
await page.evaluate(() =>
|
|
document.getElementById('models3d-mode-all').click(),
|
|
);
|
|
check(
|
|
'model selection reaches both aircraft layers',
|
|
await page.evaluate(() => {
|
|
const dm = window.__godsEyeView.dataManager;
|
|
return (
|
|
['flights', 'military'].every(
|
|
(id) => dm.layers.get(id).module.getParams().models3dMode === 'all',
|
|
) &&
|
|
document
|
|
.getElementById('models3d-mode-all')
|
|
.getAttribute('aria-checked') === 'true'
|
|
);
|
|
}),
|
|
);
|
|
await page.focus('#models3d-mode-all');
|
|
await page.keyboard.press('ArrowLeft');
|
|
check(
|
|
'Display retains its click-only arrow policy',
|
|
await page.evaluate(
|
|
() =>
|
|
document
|
|
.getElementById('models3d-mode-all')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
await page.focus('#models3d-mode-proximity');
|
|
await page.keyboard.press('Space');
|
|
check(
|
|
'native Space selects model coverage',
|
|
await page.evaluate(
|
|
() =>
|
|
document
|
|
.getElementById('models3d-mode-proximity')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
await page.evaluate(() =>
|
|
document.getElementById('detection-allocation-weighted').click(),
|
|
);
|
|
check(
|
|
'allocation selection still updates application tuning',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.styleManager._detectionAllocationPreference ===
|
|
'WEIGHTED' &&
|
|
document
|
|
.getElementById('detection-allocation-weighted')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
|
|
await page.evaluate(() =>
|
|
document.querySelector('[data-shape="pin"]').click(),
|
|
);
|
|
check(
|
|
'drawing shape still uses its existing feature action',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__gevDrawTool?.shape === 'pin' &&
|
|
document
|
|
.querySelector('[data-shape="pin"]')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
|
|
// Off-layer controls still own params; no catalog/provider request is needed.
|
|
await page.evaluate(() =>
|
|
document.querySelector('[data-sl-pano="flat"]').click(),
|
|
);
|
|
check(
|
|
'panorama choice reaches the Street Level layer',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager.layers
|
|
.get('street-level')
|
|
.module.getParams().pano === 'flat',
|
|
),
|
|
);
|
|
await page.evaluate(() =>
|
|
document.querySelector('[data-sl-pano="flat"]').dispatchEvent(
|
|
new KeyboardEvent('keydown', {
|
|
key: 'Home',
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
),
|
|
);
|
|
check(
|
|
'Street Level Home retains selection and roving tab state',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager.layers
|
|
.get('street-level')
|
|
.module.getParams().pano === 'all' &&
|
|
document
|
|
.querySelector('[data-sl-pano="all"]')
|
|
.getAttribute('tabindex') === '0',
|
|
),
|
|
);
|
|
await page.evaluate(() =>
|
|
document.querySelector('[data-sl-render="fill"]').click(),
|
|
);
|
|
check(
|
|
'fit/fill keeps its stored render preference',
|
|
await page.evaluate(
|
|
() =>
|
|
localStorage.getItem('gev:street-level:render-mode') === 'fill' &&
|
|
document
|
|
.querySelector('[data-sl-render="fill"]')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
|
|
await page.evaluate(() =>
|
|
document.querySelector('.ri-mode-btn[data-mode="ab"]').click(),
|
|
);
|
|
check(
|
|
'imagery mode reaches its controlled layer state',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager.layers
|
|
.get('recent-imagery')
|
|
.module.getSnapshot().mode === 'ab',
|
|
),
|
|
);
|
|
await page.evaluate(() =>
|
|
document.getElementById('ri-mode').dispatchEvent(
|
|
new KeyboardEvent('keydown', {
|
|
key: 'ArrowRight',
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}),
|
|
),
|
|
);
|
|
check(
|
|
'imagery arrows wrap from the current selected value',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.dataManager.layers
|
|
.get('recent-imagery')
|
|
.module.getSnapshot().mode === 'image' &&
|
|
document
|
|
.querySelector('.ri-mode-btn[data-mode="image"]')
|
|
.getAttribute('aria-checked') === 'true',
|
|
),
|
|
);
|
|
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
manager._setDetectionAllocation('ELASTIC', {
|
|
syncShare: false,
|
|
persist: false,
|
|
});
|
|
document
|
|
.getElementById('models3d-mode-proximity')
|
|
.scrollIntoView({ block: 'center' });
|
|
});
|
|
await page.screenshot({ path: `${shots}/desktop.png` });
|
|
await page.setViewport({ width: 620, height: 900 });
|
|
await page.evaluate(() =>
|
|
document
|
|
.getElementById('models3d-mode-proximity')
|
|
.scrollIntoView({ block: 'center' }),
|
|
);
|
|
await page.screenshot({ path: `${shots}/narrow.png` });
|
|
check(
|
|
'narrow selectors stay within their panel',
|
|
await page.evaluate(() =>
|
|
[...document.querySelectorAll('.pp-mode-seg')]
|
|
.filter((g) => g.getClientRects().length)
|
|
.every((group) => group.scrollWidth <= group.clientWidth + 1),
|
|
),
|
|
);
|
|
const cyber = await page.evaluate(() => {
|
|
const root = document.documentElement;
|
|
const previous = root.getAttribute('data-ui-theme');
|
|
root.setAttribute('data-ui-theme', 'cyber');
|
|
const selected = [
|
|
...document.querySelectorAll('.gev-segment[aria-checked="true"]'),
|
|
];
|
|
// Check the final theme colors, independent of software-GL frame cadence.
|
|
for (const button of selected)
|
|
for (const animation of button.getAnimations()) animation.finish();
|
|
const backgrounds = selected.map(
|
|
(button) => getComputedStyle(button).backgroundColor,
|
|
);
|
|
const radii = [...document.querySelectorAll('.gev-segmented')].map(
|
|
(group) => getComputedStyle(group).borderRadius,
|
|
);
|
|
const consistent =
|
|
selected.length === 6 &&
|
|
backgrounds.every((color) => color === backgrounds[0]) &&
|
|
radii.every((radius) => radius === '1px');
|
|
if (previous === null) root.removeAttribute('data-ui-theme');
|
|
else root.setAttribute('data-ui-theme', previous);
|
|
return { consistent, count: selected.length, backgrounds, radii };
|
|
});
|
|
check(
|
|
'Cyber theme styles every selected segment consistently',
|
|
cyber.consistent,
|
|
JSON.stringify(cyber),
|
|
);
|
|
await page.evaluate(async () => {
|
|
const { dataManager, styleManager } = window.__godsEyeView;
|
|
await dataManager.setEnabled('recent-imagery', true);
|
|
styleManager.setPanelCollapsed('recent-imagery-panel', false, {
|
|
persist: false,
|
|
syncShare: false,
|
|
});
|
|
});
|
|
for (const width of [1440, 620, 390]) {
|
|
await page.setViewport({ width, height: 900 });
|
|
await page.focus('.ri-mode-btn[aria-checked="true"]');
|
|
const modeLayout = await page.evaluate(() => {
|
|
const group = document.getElementById('ri-mode');
|
|
const bounds = group.getBoundingClientRect();
|
|
const style = getComputedStyle(group);
|
|
const top = bounds.top + parseFloat(style.borderTopWidth);
|
|
const bottom = bounds.bottom - parseFloat(style.borderBottomWidth);
|
|
const buttons = [...group.querySelectorAll('[role="radio"]')];
|
|
return {
|
|
visible: bounds.width > 0 && bounds.height > 0,
|
|
contained: buttons.every((button) => {
|
|
const rect = button.getBoundingClientRect();
|
|
return rect.top >= top - 0.5 && rect.bottom <= bottom + 0.5;
|
|
}),
|
|
groupHeight: bounds.height,
|
|
buttonHeights: buttons.map(
|
|
(button) => button.getBoundingClientRect().height,
|
|
),
|
|
};
|
|
});
|
|
check(
|
|
`imagery mode buttons and focus outline fit their row at ${width}px`,
|
|
modeLayout.visible && modeLayout.contained,
|
|
JSON.stringify(modeLayout),
|
|
);
|
|
}
|
|
check('no uncaught browser errors', errors.length === 0, errors.join(' | '));
|
|
} catch (error) {
|
|
console.error('Browser errors:', errors);
|
|
await page.screenshot({ path: `${shots}/failure.png` }).catch(() => {});
|
|
throw error;
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
console.log(`RESULT: ${failures.length} failures`);
|
|
process.exitCode = failures.length ? 1 : 0;
|