fix(street-level): modal viewer, pinned redirects, real-route CI checks

From the IC8 review of #768, each with tests that fail before:

- P2: the expanded viewer is a real modal: while it is on screen every
  other <body> child is inert (only the ones it set are restored, on
  shrink, close and destroy, and when Clean View/recording/cockpit hide
  it). The voice pill and toasts are not clickable while it is expanded.
- P2: the tile proxy follows redirects by hand, at most 3 hops, each one
  HTTPS on exactly https://tiles.mapillary.com without credentials;
  anything else is a 502 and the token never leaves that origin.
- P2: the real dev and preview server smoke tests request
  /api/mapillary/status and a tile, and the browser gate first checks the
  server's real status route before any interception, so CI fails if the
  Mapillary plugin is not registered.
- P2: both gates press panel headers through one shared helper
  (scripts/qa-panelDrag.mjs): re-resolve the header until it is under the
  pointer, record what the press hit, wait on the real floating/docked
  state, and retry only a press that provably missed the header.

Verified: unit 5910 pass; fixture gate 17/17; panel gate 5/5; live gate
with a real Mapillary token 22/22.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 15:16:57 +01:00
1 parent bd97d38a99
commit 038d8facc2
12 files changed
+881 -97

No files matched your search

+79 -15
View File
@@ -13,13 +13,22 @@
* is needed. The steps that open a photo need real imagery and are skipped.
* Fixture runs add what a live run cannot stage: a key Mapillary rejects, and
* a second page whose server has no key (the keyless gate most installs see).
* Filter assertions are stricter because the fixture data is known.
* Filter assertions are stricter because the fixture data is known. Every run
* first asks the server's real status route from Node, before the browser
* intercepts anything, so a server without the Mapillary routes fails.
*/
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
import { LAYER_STATE_REGISTRY } from '../src/data/layerState.js';
import { tileBounds } from '../src/layers/streetLevel/tileMath.js';
import { encodeCoverageTile } from '../src/layers/streetLevel/providers/mapillary/coverageFixture.mjs';
import {
describePress,
dockPanelByDoubleClick,
liftPanelByHeader,
} from './qa-panelDrag.mjs';
const PANEL_ID = 'street-level-panel';
/** Viewports every layout assertion runs at. */
export const VIEWPORTS = Object.freeze([
@@ -105,6 +114,49 @@ export function fixtureTile(z, x, y, now = Date.now()) {
return encodeCoverageTile(tile, { sequences });
}
/**
* Ask the server's real Mapillary status route, outside the browser's
* interception, and require its JSON shape. Fixture runs answer the route in
* the page, so without this a server that never registered it would pass.
* Node sends no Origin or Sec-Fetch-Site, so the same-site gate admits it.
* @param {string} url the application's base URL
* @param {typeof fetch} [fetchImpl]
* @returns {Promise<{configured: boolean}>}
*/
export async function assertRealStatusRoute(url, fetchImpl = fetch) {
const route = new URL('/api/mapillary/status', url).href;
let response;
try {
response = await fetchImpl(route, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`${route} is unreachable: ${error?.message || error}`, {
cause: error,
});
}
const type = response.headers.get('content-type') || '';
const text = await response.text();
assert.equal(
response.status,
200,
`${route} must be the server's Mapillary route (HTTP ${response.status}: ${text.slice(0, 120)})`,
);
assert.match(type, /application\/json/, `${route} answers JSON, not ${type}`);
let body;
try {
body = JSON.parse(text);
} catch {
assert.fail(`${route} answered unparseable JSON: ${text.slice(0, 120)}`);
}
assert.equal(
typeof body?.configured,
'boolean',
`${route} reports a boolean \`configured\`: ${text.slice(0, 120)}`,
);
return body;
}
/**
* Answer the page's Mapillary requests from fixtures; nothing reaches
* mapillary.com. `fixture` is live: `configured` is what the status route
@@ -277,6 +329,14 @@ async function main() {
});
};
try {
// Before any interception: fixture runs answer this route in the page.
await step(
'the server registers the real Mapillary status route',
async () => {
const real = await assertRealStatusRoute(url);
console.log(` (server status: configured=${real.configured})`);
},
);
const page = await browser.newPage();
const fixture = { configured: true, tiles: 'ok', tileRequests: 0 };
if (fixtures) await serveFixtures(page, fixture);
@@ -865,19 +925,21 @@ async function main() {
const r = node.getBoundingClientRect();
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
});
// Lifting and docking re-resolve the header and retry only a press that
// provably missed it: right after a dock the rail is still animating, and
// a title point read once can be under another panel (scripts/
// qa-panelDrag.mjs, shared with the panel-resize gate).
const floatPanel = async () => {
const title = await center('#street-level-panel .panel-title');
await page.mouse.move(title.x, title.y);
await page.mouse.down();
await page.mouse.move(title.x - 200, title.y + 40, { steps: 8 });
await page.mouse.move(title.x - 400, title.y + 80, { steps: 8 });
await page.mouse.up();
await sleep(400);
const attempt = await liftPanelByHeader(page, PANEL_ID, {
dx: -400,
dy: 80,
});
assert.equal(
await floating(),
attempt.done,
true,
'a header drag lifts the panel out of the rail',
`a header drag lifts the panel out of the rail (${describePress(attempt)})`,
);
assert.equal(await floating(), true);
};
await photoStep(
'the panel floats on a header drag, resizes, and the viewer takes the room',
@@ -946,11 +1008,13 @@ async function main() {
);
await step('a header double-click docks a floating window', async () => {
await floatPanel();
const title = await center('#street-level-panel .panel-title');
await page.mouse.click(title.x, title.y, { clickCount: 1 });
await page.mouse.click(title.x, title.y, { clickCount: 2 });
await sleep(400);
assert.equal(await floating(), false, 'docked again');
// Waits on the docked state itself, not a fixed sleep.
const attempt = await dockPanelByDoubleClick(page, PANEL_ID);
assert.equal(
attempt.done,
true,
`docked again (${describePress(attempt)})`,
);
});
await step(
'collapsing a floating window docks it as the rail strip',