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

+128
View File
@@ -742,6 +742,134 @@ test('a hidden expanded viewer (Clean View, recording, cockpit) holds neither Es
controls.destroy();
}));
/** A MutationObserver stand-in the test fires by hand. */
function fakeMutationObserver() {
const observers = [];
class FakeMutationObserver {
constructor(callback) {
this.callback = callback;
this.target = null;
this.options = null;
observers.push(this);
}
observe(target, options) {
this.target = target;
this.options = options;
}
disconnect() {
this.target = null;
}
}
const fire = () => {
for (const observer of observers)
if (observer.target) observer.callback([], observer);
};
const watching = () => observers.some((observer) => observer.target);
return { FakeMutationObserver, fire, watching };
}
/** Run `fn` with a fake MutationObserver installed. */
async function withObserver(fn) {
const saved = globalThis.MutationObserver;
const fake = fakeMutationObserver();
globalThis.MutationObserver = fake.FakeMutationObserver;
try {
return await fn(fake);
} finally {
if (saved === undefined) delete globalThis.MutationObserver;
else globalThis.MutationObserver = saved;
}
}
/** Every <body> child but the viewer, by id. */
const inertOutside = (dom) =>
dom.document.body.children
.filter((node) => node.id !== 'sl-viewer-wrap')
.map((node) => [node.id, node.inert === true]);
test('the expanded viewer makes the rest of the app inert and gives it back on shrink (IC8 P2)', () =>
withObserver(() =>
withDom(async (dom) => {
const { controls } = stubPanel(dom, uiState({ open: true }));
// Another dialog's own inert backdrop: it is not ours to release.
const owned = dom.document.body.appendChild(
new dom.globe.constructor(dom.document, 'div', { id: 'owned' }),
);
owned.inert = true;
controls.setViewerExpanded(true);
assert.deepEqual(inertOutside(dom), [
['street-level-panel', true],
['cesiumContainer', true],
['location-search', true],
['owned', true],
]);
const wrap = dom.document.body.querySelector('#sl-viewer-wrap');
assert.notEqual(wrap.inert, true, 'the viewer itself stays live');
// Shrinking returns focus into the panel: it must be live again first.
dom.root.querySelector('#sl-status').focus();
controls.setViewerExpanded(false);
assert.deepEqual(inertOutside(dom), [
['street-level-panel', false],
['cesiumContainer', false],
['location-search', false],
['owned', true],
]);
controls.destroy();
}),
));
test('a CSS-hidden expanded viewer releases the app; showing it again, and new children, go inert (IC8 P2)', () =>
withObserver((observer) =>
withDom(async (dom) => {
const { controls } = stubPanel(dom, uiState({ open: true }));
controls.setViewerExpanded(true);
assert.equal(observer.watching(), true);
const wrap = dom.document.body.querySelector('#sl-viewer-wrap');
let visible = true;
wrap.checkVisibility = () => visible;
// Clean View (or recording, or the cockpit) hides the viewer by CSS.
visible = false;
dom.document.body.classList.add('ui-clean-view');
observer.fire();
assert.ok(
inertOutside(dom).every(([, inert]) => !inert),
'a hidden modal leaves nothing inert',
);
// Leaving the mode shows the viewer again: the app is inert once more,
// and so is a child added to <body> meanwhile.
visible = true;
const toast = dom.document.body.appendChild(
new dom.globe.constructor(dom.document, 'div', { id: 'toast' }),
);
observer.fire();
assert.ok(inertOutside(dom).every(([, inert]) => inert));
assert.equal(toast.inert, true);
controls.destroy();
assert.ok(
inertOutside(dom).every(([, inert]) => !inert),
'destroy releases the app',
);
assert.equal(observer.watching(), false, 'and stops watching');
}),
));
test('an image closed while expanded releases the app (IC8 P2)', () =>
withObserver(() =>
withDom(async (dom) => {
const { layer, controls } = stubPanel(dom, uiState({ open: true }));
controls.setViewerExpanded(true);
assert.ok(inertOutside(dom).every(([, inert]) => inert));
layer.publish(uiState({ open: false }));
assert.equal(controls.isViewerExpanded(), false);
assert.ok(inertOutside(dom).every(([, inert]) => !inert));
assert.equal(
dom.document.activeElement,
dom.root.querySelector('#sl-status'),
);
controls.destroy();
}),
));
test('FIT / FILL show the selection while the viewer is expanded (P3)', () =>
withDom(async (dom) => {
const { layer, controls } = stubPanel(dom, uiState({ open: true }));