mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Voice show_data_layers_menu opens the panel that holds the row, so asking for Directions opens UTILITIES and reports that panel, not a hidden row under Data Layers. In Cockpit the panel stays with Directions alone, as Data Layers does; only Draw and Import step aside there. Draw's options sit flat under its name instead of keeping the card that hung under a DISPLAY button, and its Shape label no longer takes DISPLAY's label column in other languages. Cyber marks Draw on with its red edge. LayerPanel routing, the voice panel choice, and the panel's order, mode and phone-sheet CSS gain tests that fail on the earlier code. The form-field and segmented-control gates open Utilities for Draw's controls, measuring Scenes and Utilities in turn since the left rail fits one tall panel. README, CONTRIBUTING, the PR template, CURRENT-STATE and comments now say UTILITIES for Draw and Import and list the Audio group, and two files outside the format scope lose an unrelated reflow.
325 lines
10 KiB
JavaScript
325 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Run against a dev server or built preview with QA_BASE_URL (default :4179). */
|
|
import fs from 'node:fs';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const browser = await puppeteer.launch({
|
|
executablePath:
|
|
process.env.PUPPETEER_EXECUTABLE_PATH || (await puppeteer.executablePath()),
|
|
headless: true,
|
|
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
|
});
|
|
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 page.waitForFunction(
|
|
() =>
|
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 60000 },
|
|
);
|
|
// 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(() => {
|
|
const groups = [...document.querySelectorAll('[role="radiogroup"]')];
|
|
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),
|
|
);
|
|
|
|
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 browser.close();
|
|
}
|
|
console.log(`RESULT: ${failures.length} failures`);
|
|
process.exitCode = failures.length ? 1 : 0;
|