mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #917 from kvnloo/fix/recent-imagery-qa-relative-layout-20261003
test: make Recent Imagery live gate rail-reflow safe
This commit is contained in:
2 files changed
+26
-11
No files matched your search
@@ -4451,7 +4451,7 @@ are omitted rather than framing the wrong part of the globe.
|
||||
- **Esri surface and the swipe.** While any image is on the map (a preview included) the layer holds the map lease (`acquireImageryComparison`, owner `recent-imagery`, `esri-if-photoreal`): on Google 3D the map switches to Esri, because 30 m pixels over 3D buildings do not read, and Cesium's split only works on the globe. The lease is handed back, and Google 3D restored unless the operator changed maps meanwhile, when the last image leaves the map or the layer is disabled; the notice line says so while it holds. While a day is shown the imagery keeps Esri: a manual switch that lands on Google 3D is answered at once with a new lease under the same policy (`keepEsri`), the notice reads `Imagery stays on Esri · CLEAR to use Google 3D`, and the divider never suspends because of a map switch. It re-leases once per switch generation and only for a manual one (`MapSourceController#getSwitchOrigin()` is `manual` for an outside `setStack`, `automatic` for the controller's own fallback or recovery), never for the lease's own switch and never while releasing, so CLEAR and disable still return to Google 3D; a lease another owner holds stays refused. When Esri fails and the controller falls back to Google 3D on its own, the layer lets the lease go without restoring anything, both days drape on the tileset without a swipe and the guidance reads `Esri map unavailable · no swipe`; only the next manual switch to Google 3D takes Esri again, so a failing Esri map is never retried in a loop. Switching between globe stacks (Esri, OSM, Bing) only rebinds. On a tileset host both images drape without a swipe. The split control (`createImagerySplit`, labels A / B, or IMAGE / BASEMAP) exists only while a swipe is live, recentres on every new comparison (not while scrubbing the second day), except once for a split restored from a link. There is no keyboard shortcut (Space is the app's push-to-talk): SWAP (`#ri-swap`, in the controls row, its slot reserved and the button disabled while no swipe is live) trades the two sides, A to the right and B to the left, or the image to the right of the basemap, and a second click restores; the hint reads `Drag the divider · SWAP trades sides`. The swap is not in the share link, and every new comparison starts unswapped. One opacity applies to both drapes. Another owner holding the lease is reported and the images still drape.
|
||||
- **Panel.** `#recent-imagery-panel` is static rail markup in `src/ui/templates/context.html` beside `#weather-panel`, registered in `PanelLayoutController`, `PanelChrome` and the share panel state (`ui` token `i`), hidden while the layer is off and opened on first appearance unless a stored or shared collapse choice exists (`dataset.collapsedPreference`). `LayerPanel.attachRecentImagery(factory)` mounts the readout (`createRecentImageryPanel` in `src/ui/recentImagery.js`) into its `data-rail-scroller` body, as the weather readout is mounted. The body is a fixed stack in which no block changes height and no control moves in any state: `#ri-actions` (SELECT BOX · USE VIEW · CLEAR, rail-card actions), `#ri-notice` (one line: `#ri-notice-text` with the refusal, error, CLEAR notice or the Esri note, and `#ri-zoom-in` in a fixed slot at its right end, reserved unless an oversized box or view can be fitted), `#ri-strip` (day cards newest first: thumbnail, date, sensor, cloud, START HERE, PREVIEW, and a SHOW chip or A and B chips; ← → Home End move, S / A / B pin, Enter previews), `#ri-hint` (one line: the next step), `#ri-selection` (`#ri-mode` IMAGE · VS BASEMAP · A / B, then `#ri-slot-a` and `#ri-slot-b` with `#ri-unpin-a` / `#ri-unpin-b` kept in place when empty), `#ri-controls` (`#ri-opacity`, EXPORT / EXPORT A and EXPORT B) and `#ri-details`, a collapsed rail card holding the focused day's acquisition, coverage and granules, catalog notes, the empty-days toggle and the imagery credit. Absent controls are reserved (invisible), never removed. Every render preserves the body's `scrollTop`.
|
||||
- **Share links.** Layer options (token `1`): box edges as integers at degrees × 100000, pins `a` and `b` as compact days (`S20260918`), `m` mode (0 image, 1 basemap, 2 A / B), `p` split percent (share-link only: never stored locally) and `v` for the daily overview. Row chips: `MORE DETAIL · 30 m` (on) and `DAILY OVERVIEW · 250 m` (off).
|
||||
- Regression surface: the colocated `*.test.mjs` beside each module (shared doubles in `src/layers/recentImagery/testDoubles.mjs`; `src/ui/recentImagery.test.mjs` includes a DOM-structure guard: every control keeps its place in the tree, nothing is hidden, and every fixed-height block keeps the class the stylesheet fixes its height by; it has no layout, so it cannot see movement), `src/ui/layerPanel.test.mjs` (mount), `src/maps/controller.test.mjs` (`subscribe`), `src/data/layerState.test.mjs` (codec), `src/sharelink.celestial.test.mjs` (panel token), and the live gate `scripts/qa-recent-imagery.mjs`, which also measures every control's rect across the transitions within 1 px; that measurement is the real layout-shift check.
|
||||
- Regression surface: the colocated `*.test.mjs` beside each module (shared doubles in `src/layers/recentImagery/testDoubles.mjs`; `src/ui/recentImagery.test.mjs` includes a DOM-structure guard: every control keeps its place in the tree, nothing is hidden, and every fixed-height block keeps the class the stylesheet fixes its height by; it has no layout, so it cannot see movement), `src/ui/layerPanel.test.mjs` (mount), `src/maps/controller.test.mjs` (`subscribe`), `src/data/layerState.test.mjs` (codec), `src/sharelink.celestial.test.mjs` (panel token), and the live gate `scripts/qa-recent-imagery.mjs`, which also measures every control's rect, relative to the panel, across the transitions within 1 px; that measurement is the real layout-shift check. `QA_RECENT_IMAGERY_SLOW_RENDER=1` gives loaded software renderers a 4 s zoom deadline and an 8-sample stability tail; the default stays 2 s and 12 samples.
|
||||
|
||||
### 3D Aircraft + Tracking (June 2026)
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
* Run: start the app (`./scripts/dev-fresh.sh`) and
|
||||
* node scripts/qa-recent-imagery.mjs
|
||||
* Env: QA_BASE_URL (default http://localhost:4173), QA_SHOT_DIR (default
|
||||
* qa-shots/recent-imagery), QA_HEADFUL=1. Every wait is bounded. Without
|
||||
* network access to NASA the run skips with exit code 0.
|
||||
* qa-shots/recent-imagery), QA_HEADFUL=1.
|
||||
* QA_RECENT_IMAGERY_SLOW_RENDER=1 permits loaded SwiftShader CI to use a
|
||||
* 4 s ZOOM IN wait and 0.8 s height-stability tail (defaults: 2 s / 1.2 s).
|
||||
* Every wait is bounded. Without NASA access the run skips with code 0.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -22,6 +24,9 @@ import puppeteer from 'puppeteer';
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const SHOT_DIR = process.env.QA_SHOT_DIR || 'qa-shots/recent-imagery';
|
||||
const HEADFUL = process.env.QA_HEADFUL === '1';
|
||||
const SLOW_RENDER = process.env.QA_RECENT_IMAGERY_SLOW_RENDER === '1';
|
||||
const ZOOM_TIMEOUT_MS = SLOW_RENDER ? 4_000 : 2_000;
|
||||
const HEIGHT_TAIL_SAMPLES = SLOW_RENDER ? 8 : 12;
|
||||
const STEP_TIMEOUT = 20_000;
|
||||
const AUSTIN = { lon: -97.74, lat: 30.27, height: 12_000 };
|
||||
const PANEL = '#recent-imagery-panel';
|
||||
@@ -308,15 +313,21 @@ const CONTROLS = {
|
||||
};
|
||||
const measure = () =>
|
||||
page.evaluate((controls) => {
|
||||
const panel = document.getElementById('recent-imagery-panel');
|
||||
const body = document.getElementById('recent-imagery-panel-body');
|
||||
const panelRect = panel?.getBoundingClientRect();
|
||||
const out = { bodyScroll: body?.scrollTop ?? null, rects: {} };
|
||||
for (const [name, selector] of Object.entries(controls)) {
|
||||
const rect = document.querySelector(selector)?.getBoundingClientRect();
|
||||
out.rects[name] = rect
|
||||
? [rect.left, rect.top, rect.width, rect.height].map(
|
||||
(v) => Math.round(v * 10) / 10,
|
||||
)
|
||||
: null;
|
||||
out.rects[name] =
|
||||
rect && panelRect
|
||||
? [
|
||||
rect.left - panelRect.left,
|
||||
rect.top - panelRect.top,
|
||||
rect.width,
|
||||
rect.height,
|
||||
].map((v) => Math.round(v * 10) / 10)
|
||||
: null;
|
||||
}
|
||||
return out;
|
||||
}, CONTROLS);
|
||||
@@ -372,6 +383,9 @@ try {
|
||||
const initialStack = await page.evaluate(() => window.__riQa.activeStack());
|
||||
const photoreal = initialStack === 'photoreal';
|
||||
console.log(`initial map stack: ${initialStack}`);
|
||||
console.log(
|
||||
`stability: ${SLOW_RENDER ? 'slow-render' : 'default'} (zoom ${ZOOM_TIMEOUT_MS} ms, last ${HEIGHT_TAIL_SAMPLES} samples)`,
|
||||
);
|
||||
const onPhotorealOnly = (name, passed, detail) =>
|
||||
photoreal
|
||||
? check(name, passed, detail)
|
||||
@@ -1042,13 +1056,13 @@ try {
|
||||
) <=
|
||||
target * 0.01,
|
||||
fitHeight,
|
||||
2_000,
|
||||
ZOOM_TIMEOUT_MS,
|
||||
);
|
||||
const zoomedHeight = await page.evaluate(
|
||||
() => window.__godsEyeView.viewer.camera.positionCartographic.height,
|
||||
);
|
||||
check(
|
||||
`ZOOM IN flies to ${Math.round(fitHeight / 1000)} km within 2 s`,
|
||||
`ZOOM IN reaches target within ${ZOOM_TIMEOUT_MS / 1_000} s`,
|
||||
zoomed,
|
||||
`height=${Math.round(zoomedHeight)} target=${Math.round(fitHeight)}`,
|
||||
);
|
||||
@@ -1258,7 +1272,8 @@ try {
|
||||
);
|
||||
await wait(100);
|
||||
}
|
||||
const tail = (key) => new Set(samples.slice(8).map((s) => s[key]));
|
||||
const tail = (key) =>
|
||||
new Set(samples.slice(-HEIGHT_TAIL_SAMPLES).map((s) => s[key]));
|
||||
return {
|
||||
settled: tail('imagery').size === 1 && tail('cctv').size === 1,
|
||||
detail: `imagery=${samples.map((s) => s.imagery).join(',')} cctv=${samples.map((s) => s.cctv).join(',')}`,
|
||||
|
||||
Reference in new issue
Block a user