import { readShellSource } from './testSupport/readShellSource.mjs'; import { onKeyDown as cockpitKeyDown } from './ui/cockpitInput.js'; import { readFileSync as readRadioSource } from 'node:fs'; const radioBindings = readRadioSource(new URL('./ui/radioBindings.js', import.meta.url), 'utf8'); const radioPresentation = readRadioSource(new URL('./ui/radioPresentation.js', import.meta.url), 'utf8'); const radioControlsSource = readRadioSource(new URL('./ui/radioControls.js', import.meta.url), 'utf8'); import { bindPanelDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import test from 'node:test'; const source = readShellSource(); function method(name, nextName) { const start = source.indexOf(` ${name}(`); const end = source.indexOf(`\n ${nextName}(`, start); assert.ok(start >= 0 && end > start, `${name} source is available`); return source.slice(start, end); } function panelFixture({ id = 'data-panel', nested = false, nestedCollapsed = false, onDisclosure = false } = {}) { let collapsed = false; let focused = false; let blurred = false; const disclosure = { focus: () => { focused = true; }, blur: () => { blurred = true; focused = false; }, contains: (candidate) => candidate === disclosure, }; const panel = { id, classList: { contains: (name) => name === 'collapsed' && collapsed }, contains: (target) => target?.panel === panel, querySelector: () => disclosure, }; const inner = nested ? { id: 'param-slider-panel' } : panel; const target = onDisclosure ? disclosure : { panel, closest: (selector) => ( nested && nestedCollapsed && selector.includes(':not(.collapsed)') ? panel : inner ), }; if (onDisclosure) { disclosure.panel = panel; disclosure.closest = () => panel; } const document = { getElementById: (candidate) => candidate === id ? panel : null }; const manager = new Function('document', 'collapsePanelOnEscape', `return {${method('_collapsePanelOnEscape', '_initCommandDockPins')}};`)(document, collapsePanelOnEscape); manager.setPanelCollapsed = (candidate, value, options) => { assert.equal(candidate, id); assert.equal(value, true); assert.deepEqual(options, { explicit: true }); collapsed = true; }; const event = { key: 'Escape', target, defaultPrevented: false, propagationStopped: false, preventDefault() { this.defaultPrevented = true; }, stopPropagation() { this.propagationStopped = true; }, }; return { manager, panel, inner, target, event, collapsed: () => collapsed, focused: () => focused, blurred: () => blurred, }; } test('Escape collapses an expanded panel and returns focus to its disclosure', () => { const fixture = panelFixture(); assert.equal(fixture.manager._collapsePanelOnEscape(fixture.event, 'data-panel'), true); assert.equal(fixture.collapsed(), true); assert.equal(fixture.focused(), true); assert.equal(fixture.event.defaultPrevented, true); assert.equal(fixture.event.propagationStopped, true); }); test('Escape on an expanded panel disclosure closes it without retaining focus', () => { const fixture = panelFixture({ onDisclosure: true }); assert.equal(fixture.manager._collapsePanelOnEscape(fixture.event, 'data-panel'), true); assert.equal(fixture.collapsed(), true); assert.equal(fixture.focused(), false); assert.equal(fixture.blurred(), true); assert.equal(fixture.event.defaultPrevented, true); assert.equal(fixture.event.propagationStopped, true); }); test('non-Escape, handled, collapsed, and outside events do not collapse a panel', () => { for (const mutate of [ (fixture) => { fixture.event.key = 'Enter'; }, (fixture) => { fixture.event.defaultPrevented = true; }, (fixture) => { fixture.manager.setPanelCollapsed('data-panel', true, { explicit: true }); }, (fixture) => { fixture.event.target = { panel: null, closest: () => null }; }, ]) { const fixture = panelFixture(); mutate(fixture); const wasCollapsed = fixture.collapsed(); assert.equal(fixture.manager._collapsePanelOnEscape(fixture.event, 'data-panel'), false); assert.equal(fixture.collapsed(), wasCollapsed); assert.equal(fixture.focused(), false); } }); test('an expanded nested panel owns Escape before its expanded parent', () => { const inner = panelFixture({ id: 'param-slider-panel' }); assert.equal(inner.manager._collapsePanelOnEscape(inner.event, 'param-slider-panel'), true); const outer = panelFixture({ id: 'pp-toggles', nested: true }); assert.equal(outer.manager._collapsePanelOnEscape(outer.event, 'pp-toggles'), false); assert.equal(outer.collapsed(), false); assert.equal(outer.focused(), false); }); test('a collapsed nested panel does not block the next Escape from closing its parent', () => { const outer = panelFixture({ id: 'pp-toggles', nested: true, nestedCollapsed: true }); assert.equal(outer.manager._collapsePanelOnEscape(outer.event, 'pp-toggles'), true); assert.equal(outer.collapsed(), true); assert.equal(outer.focused(), true); }); test('Location Escape clears a hidden draft search before restoring disclosure focus', () => { const fixture = panelFixture({ id: 'location-bar' }); const removed = []; let blurred = false; fixture.manager._locationSearch = { classList: { remove: (...names) => removed.push(...names) }, value: 'focus cleanup', blur: () => { blurred = true; }, }; assert.equal(fixture.manager._collapsePanelOnEscape(fixture.event, 'location-bar'), true); assert.deepEqual(removed, ['expanded']); assert.equal(fixture.manager._locationSearch.value, ''); assert.equal(blurred, true); assert.equal(fixture.focused(), true); }); test('panel chrome wires Escape for every declared collapse target', () => { const init = method('_initPanelChrome', '_collapsePanelOnEscape'); assert.match(init, /for \(const \[targetId, buttons\] of targets\)/); assert.match(init, /bindPanelDisclosure\(\{[\s\S]*?onEscape: \(event\) => this\._collapsePanelOnEscape\(event, targetId\)/); assert.match(source, /createHoverDisclosure\(\{[\s\S]*?onEscape: \(event\) => this\._collapsePanelOnEscape\(event, panelId\)/); }); test('Cockpit Escape collapses Contact or Live Signals before exiting Cockpit', () => { const onKeyDown = cockpitKeyDown.toString(); assert.match( onKeyDown, /event\.target\?\.closest\?\.\('\.cesium-credit-lightbox'\)[\s\S]*?return;/, 'the focused attribution lightbox keeps ownership of Escape before Cockpit', ); assert.match( onKeyDown, /this\.context\?\.contains\(event\.target\)[\s\S]*?setContextCollapsed\(true\)[\s\S]*?event\.target === this\.contextToggle[\s\S]*?contextToggle\?\.blur[\s\S]*?contextToggle\?\.focus/, ); assert.match( onKeyDown, /this\.signalStream\?\.contains\(event\.target\)[\s\S]*?setSignalCollapsed\(true, \{ user: true \}\)[\s\S]*?event\.target === this\.signalToggle[\s\S]*?signalToggle\?\.blur[\s\S]*?signalToggle\?\.focus/, ); assert.ok(onKeyDown.indexOf('setContextCollapsed(true)') < onKeyDown.indexOf('this.exit()')); assert.ok(onKeyDown.indexOf('setSignalCollapsed(true') < onKeyDown.indexOf('this.exit()')); }); test('Cockpit utility Escape leaves an expanded nested Parameters panel to the shared handler', () => { assert.match( radioBindings, /const nestedPanel = event\.target\?\.closest\?\.\(\s*'\.panel-collapsible:not\(\.collapsed\), #param-slider-panel:not\(\.collapsed\)',?\s*\);[\s\S]*?if \(nestedPanel\) return;[\s\S]*?setCockpitDisclosure/, ); assert.match( radioBindings, /const kind = displayOpen \? 'display' : 'radio';[\s\S]*?escapedFromDisclosure[\s\S]*?returnFocus: !escapedFromDisclosure[\s\S]*?disclosure\?\.blur/, 'Cockpit utility disclosures clear their own focus when Escape closes them', ); assert.match( radioBindings, /_contextRadioDock\?\.classList\.contains\('disclosure-open'\)[\s\S]*?escapedFromDisclosure[\s\S]*?setRadioDisclosure\(false, \{ returnFocus: !escapedFromDisclosure \}\)[\s\S]*?_contextRadioToggleBtn\?\.blur/, 'compact Radio disclosure clears its own focus when Escape closes it', ); }); test('panel bindings have a single owner and are inert after destruction', () => { const panel = new EventTarget(); const button = new EventTarget(); let collapsed = true; let changes = 0; let keys = 0; panel.classList = { contains: () => collapsed }; const bind = () => bindPanelDisclosure({ panel, buttons: [button, button], onChange(value, options) { collapsed = value; changes += 1; assert.equal(options.explicit, true); }, onEscape() { keys += 1; }, }); const first = bind(); button.dispatchEvent(new Event('click')); panel.dispatchEvent(new Event('keydown')); assert.equal(collapsed, false); assert.equal(changes, 1); assert.equal(keys, 1); first.destroy(); first.destroy(); button.dispatchEvent(new Event('click')); panel.dispatchEvent(new Event('keydown')); assert.equal(changes, 1); assert.equal(keys, 1); const second = bind(); button.dispatchEvent(new Event('click')); assert.equal(changes, 2); assert.equal(collapsed, true); second.destroy(); });