Files
gods-eye-view/src/streetLevelMarkup.test.mjs
T
Sameh Khamis cd231faae9 feat(panels): build the rail panels on one UiPanel
The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level
and Radio panels each wrote the same frame by hand. They are now one kit
component, UiPanel, with a title, badge/actions/default slots and
v-model:collapsed, drawing Core's panel markup and classes beside the kit's;
props cover the differences (sideways, glow, scroll, railScroller, tag,
toggleWords). RailPanel binds it to the panel chrome store: v-panel-state
writes its state classes at once, and its button's clicks reach PanelChrome.
UiPanelToggle is the collapse button UiPanel and PanelCollapseButton share.

UiPanel's collapsed drives the button and ui-panel--collapsed
only; the owner hides the body by writing the root's collapsed class, since
the rails measure a panel in the task that opens it. PanelCollapseButton
takes startCollapsed instead of initial and initialTitle, so a button shows
its state's glyph and words before startup presents the panel. Titles render
through t() instead of data-i18n, and the unused "Expand panel" string is
pruned. v-panel-state redraws the presented state after a re-render, in
case a root's class binding ever changes.
2026-11-01 08:33:18 -08:00

118 lines
4.5 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8');
const html = await renderApplicationHtml(read('index.html'));
const css = readStylesheet(path.join(ROOT, 'style.css'));
const panelCss = read('src/ui/styles/street-level.css');
const panelChrome = read('src/ui/panelChrome.js');
const layoutController = read('src/ui/panelLayoutController.js');
test('Street Level is a collapsible right-rail panel that starts collapsed', () => {
assert.match(
html,
/<div id="street-level-panel" class="ui-panel panel-collapsible collapsed" data-panel-id="street-level-panel">/,
);
assert.match(
html,
/<button type="button" class="[^"]*\bpanel-collapse-btn\b[^"]*"[^>]*data-collapse-target="street-level-panel"/,
);
assert.match(
html,
/<span class="ui-panel__title panel-title">STREET LEVEL<\/span>/,
);
// Provider-neutral header: no vendor mark; the inner restores its scroll.
assert.doesNotMatch(html, /sl-mark|mly-mark/);
assert.match(
html,
/<div class="street-level-panel-inner ui-scroll" data-rail-scroller>/,
);
assert.doesNotMatch(
html,
/mapillary-dock|sl-minimized|sl-3d-btn|sl-photoreal-btn/,
);
});
test('the panel is registered with panel chrome, cockpit entry and the right rail', () => {
assert.match(panelChrome, /\{ id: 'street-level-panel' \}/);
assert.match(
panelChrome,
/COCKPIT_ENTRY_COLLAPSE_PANEL_IDS = Object\.freeze\(\[[\s\S]*'street-level-panel'/,
);
// A right-rail panel's collapse button opens sideways (◀ / ▶).
assert.match(
read('src/ui/views/panels/StreetLevelPanel.vue'),
/<RailPanel id="street-level-panel"[^>]*\bsideways\b/,
);
assert.match(
layoutController,
/for \(const panel of \[[\s\S]*?this\._streetLevelPanel,[\s\S]*?\]\) \{[\s\S]*?stack\.insertBefore\(panel, globalContextPanel\)/,
);
assert.ok(css.includes('#right-context-rail > #street-level-panel'));
});
test('the key gate holds every filter, with the error line outside it', () => {
const start = html.indexOf('<fieldset id="sl-controls"');
assert.ok(start > 0);
const gate = html.slice(start, html.indexOf('</fieldset>', start));
assert.match(gate, /data-sl-pano="all"/);
assert.match(gate, /id="sl-since"/);
assert.ok(html.indexOf('id="sl-error"') < start);
// The chips stay outside the gate, so their key tooltip is reachable.
assert.match(html, /<div id="sl-provider-chips" class="sl-chips"><\/div>/);
assert.doesNotMatch(gate, /sl-provider-chips/);
assert.match(html, /id="sl-error"[^>]*role="alert"/);
});
test('each radiogroup is one tab stop, on its checked button', () => {
for (const [group, checked] of [
['data-sl-render', 'letterbox'],
['data-sl-pano', 'all'],
]) {
const radios = [
...html.matchAll(
new RegExp(`<button [^>]*${group}="([a-z]+)"[^>]*>`, 'g'),
),
];
assert.ok(radios.length >= 2, group);
for (const [tag, value] of radios)
assert.equal(
/tabindex="-1"/.test(tag),
value !== checked,
`${group}="${value}" roving tab stop`,
);
}
});
test('the Cyber theme frames the Street Level panel like its rail peers (P3)', () => {
const cyber = read('src/ui/styles/cyber.css');
const inner = (cyber.match(/\.recent-imagery-panel-inner,/g) || []).length;
const panel = (cyber.match(/#recent-imagery-panel,/g) || []).length;
assert.ok(inner > 0 && panel > 0);
assert.equal(
(cyber.match(/\.street-level-panel-inner,/g) || []).length,
inner,
);
assert.equal((cyber.match(/#street-level-panel,/g) || []).length, panel);
// A theme header rule must be able to outrank the panel's own.
assert.doesNotMatch(panelCss, /#street-level-panel \.panel-header/);
});
test('OPEN STREET VIEW keeps its width beside a long coverage line', () => {
// .scene-btn grows and shrinks (flex: 1) and loads after this stylesheet:
// only a more specific rule keeps the button from being squeezed.
assert.match(css, /\.scene-btn \{[^}]*flex: 1;/);
assert.match(
panelCss,
/\.sl-meta-row \.sl-nearest-btn \{\s*flex: 0 0 auto;\s*\}/,
);
assert.match(panelCss, /\.sl-meta-row \.sl-meta \{[^}]*min-width: 0;/);
});