Files
gods-eye-view/src/ui/panelNavigation.test.mjs
T
Sameh Khamis 3d5d3ef014 feat(ui): add a desktop panel rail to each sidebar
Each side stack hosts a Vue rail listing its panels; selecting one opens it
through PanelChrome. The shell owns one Vue runtime and Pinia instance, and a
panel store mirrors each side's panels from the DOM. The rail hides with its
stack, in Cockpit, clean view and recording, and on phones. Panels without a
catalog entry get a generic item, and Data Layers leaves the rail while the
f shortcut hides it.
2026-10-30 16:41:29 -07:00

141 lines
4.5 KiB
JavaScript

import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { installVueDom } from '../testSupport/vueDom.mjs';
let PanelNavigation;
let createUiRuntime;
let nextTick;
let uninstall;
before(async () => {
uninstall = installVueDom();
({ nextTick } = await import('vue'));
({ PanelNavigation } = await import('./panelNavigation.js'));
({ createUiRuntime } = await import('./vueRuntime.js'));
});
after(() => uninstall());
function panel(id, { collapsed = true, hidden = false, title = '' } = {}) {
const element = document.createElement('div');
element.id = id;
element.dataset.panelId = id;
element.className = collapsed
? 'panel-collapsible collapsed'
: 'panel-collapsible';
// Data Layers shows only while `active` (the `f` shortcut toggles it).
if (id === 'data-panel') element.classList.add('active');
element.hidden = hidden;
if (title) element.innerHTML = `<span class="panel-title">${title}</span>`;
return element;
}
function setup() {
document.body.innerHTML = '';
const left = document.createElement('div');
left.id = 'left-panel-stack';
left.append(panel('data-panel', { collapsed: false }), panel('scene-panel'));
const right = document.createElement('div');
right.id = 'right-context-rail';
right.append(
panel('pp-toggles'),
panel('weather-panel', { hidden: true }),
panel('global-context-panel'),
panel('host-panel', { title: 'HOST' }),
);
document.body.append(left, right);
const calls = [];
const ui = createUiRuntime();
const navigation = new PanelNavigation({
ui,
stacks: { left, right },
setPanelCollapsed: (...args) => calls.push(args),
});
navigation.init();
return { left, right, calls, ui, navigation };
}
const railItems = (stack) =>
[...stack.querySelectorAll('.gev-panel-rail [data-rail-item]')].map(
(button) => [button.dataset.railItem, button.getAttribute('aria-pressed')],
);
test('each stack gets a rail of its offered panels, marking the open one', async () => {
const { left, right, ui, navigation } = setup();
await nextTick();
assert.equal(
left.firstElementChild.className,
'gev-panel-rail gev-panel-rail--left',
);
assert.deepEqual(railItems(left), [
['data-panel', 'true'],
['scene-panel', 'false'],
]);
// Hidden panels are not offered; a host's panel gets a generic item.
assert.deepEqual(railItems(right), [
['pp-toggles', 'false'],
['global-context-panel', 'false'],
['host-panel', 'false'],
]);
assert.equal(
right
.querySelector('[data-rail-item="host-panel"]')
.getAttribute('aria-label'),
'HOST',
);
assert.equal(
left.querySelector('nav').getAttribute('aria-label'),
'Left panels',
);
navigation.destroy();
ui.destroy();
});
test('selecting an item toggles its panel explicitly', async () => {
const { left, calls, ui, navigation } = setup();
await nextTick();
left.querySelector('[data-rail-item="scene-panel"]').click();
left.querySelector('[data-rail-item="data-panel"]').click();
assert.deepEqual(calls, [
['scene-panel', false, { explicit: true }],
['data-panel', true, { explicit: true }],
]);
navigation.destroy();
ui.destroy();
});
test('the rails follow disclosure and availability changes', async () => {
const { left, right, ui, navigation } = setup();
document.getElementById('data-panel').classList.add('collapsed');
document.getElementById('scene-panel').classList.remove('collapsed');
document.getElementById('weather-panel').hidden = false;
await new Promise((resolve) => setTimeout(resolve, 0));
await nextTick();
assert.deepEqual(railItems(left), [
['data-panel', 'false'],
['scene-panel', 'true'],
]);
assert.deepEqual(railItems(right), [
['pp-toggles', 'false'],
['weather-panel', 'false'],
['global-context-panel', 'false'],
['host-panel', 'false'],
]);
// `f` hides Data Layers outright, so the rail stops offering it.
document.getElementById('data-panel').classList.remove('active');
await new Promise((resolve) => setTimeout(resolve, 0));
await nextTick();
assert.deepEqual(railItems(left), [['scene-panel', 'true']]);
navigation.destroy();
ui.destroy();
});
test('destroy removes the rails and stops following the stacks', async () => {
const { left, right, calls, ui, navigation } = setup();
navigation.destroy();
navigation.destroy();
assert.equal(left.querySelector('.gev-panel-rail'), null);
assert.equal(right.querySelector('.gev-panel-rail'), null);
navigation.select('data-panel');
assert.deepEqual(calls, []);
ui.destroy();
});