mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
PanelChrome, its collapse-preference restore and the phone shell read a panel's disclosure state back from the gev-panel-chrome store (_shownPanel), falling back to the markup's classes before the panel is first presented, instead of reading the classes v-panel-state draws. The share link and the phone shell share one rule for a panel the person keeps closed (prefersCollapsed). The rails, the accordion and the hover disclosure still read the classes, which the directive draws in the same task as their layout.
457 lines
22 KiB
JavaScript
457 lines
22 KiB
JavaScript
import { shellMethod } from './testSupport/readShellSource.mjs';
|
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
|
import { createHoverDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createPinia } from 'pinia';
|
|
import { msg, t } from './i18n/index.js';
|
|
import { followPanelState, readPanelState } from './ui/rail/panelState.js';
|
|
import { usePanelChromeStore } from './ui/stores/panelChrome.js';
|
|
|
|
// Exercise the installed event routes and central close method, without WebGL.
|
|
const markup = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
|
const locationMarkup = markup.slice(markup.indexOf('<div id="location-bar"'), markup.indexOf('<div id="left-panel-stack"'));
|
|
const locationToggleMarkup = locationMarkup.match(/<button\b([^>]*\bid="location-bar-toggle"[^>]*)>([\s\S]*?)<\/button>/);
|
|
const locationToggleAttributes = Object.fromEntries(
|
|
[...(locationToggleMarkup?.[1] || '').matchAll(/([\w-]+)="([^"]*)"/g)].map((match) => [match[1], match[2]]),
|
|
);
|
|
|
|
function harness({ hidden = false, selected = true, noChips = false } = {}) {
|
|
let now = 0;
|
|
let nextTimer = 0;
|
|
let focusCalls = 0;
|
|
const timers = new Map();
|
|
const document = { activeElement: null };
|
|
const makeNode = (id, attributes = {}) => {
|
|
const listeners = new Map();
|
|
const classes = new Set();
|
|
return {
|
|
id, listeners, classes, hovered: false,
|
|
setAttribute(name, value) { attributes[name] = String(value); },
|
|
getAttribute(name) { return attributes[name] ?? null; },
|
|
classList: {
|
|
contains: (name) => classes.has(name),
|
|
remove: (name) => classes.delete(name),
|
|
toggle(name, value) { if (value) classes.add(name); else classes.delete(name); },
|
|
},
|
|
addEventListener(type, callback) {
|
|
if (!listeners.has(type)) listeners.set(type, []);
|
|
listeners.get(type).push(callback);
|
|
},
|
|
removeEventListener(type, callback) {
|
|
const values = listeners.get(type) || [];
|
|
listeners.set(type, values.filter((item) => item !== callback));
|
|
},
|
|
matches(selector) { return selector === ':hover' ? this.hovered : true; },
|
|
closest() { return this.isDisclosure ? this : null; },
|
|
};
|
|
};
|
|
const panel = makeNode('control-panel');
|
|
const location = makeNode('location-bar');
|
|
panel.classes.add('collapsed');
|
|
location.classes.add('collapsed');
|
|
const disclosure = makeNode('control-panel-toggle');
|
|
disclosure.isDisclosure = true;
|
|
const locationDisclosure = locationToggleMarkup ? makeNode('location-bar-toggle', { ...locationToggleAttributes }) : null;
|
|
if (locationDisclosure) locationDisclosure.isDisclosure = true;
|
|
const locationSearch = makeNode('location-search');
|
|
const first = makeNode('photoreal');
|
|
const active = makeNode('osm');
|
|
const elsewhere = makeNode('elsewhere');
|
|
const nodes = [panel, location, disclosure, locationDisclosure, locationSearch, first, active, elsewhere].filter(Boolean);
|
|
document.getElementById = (id) => nodes.find((node) => node.id === id);
|
|
panel.contains = (node) => [panel, disclosure, first, active].includes(node);
|
|
location.contains = (node) => Boolean(node) && [location, locationDisclosure, locationSearch].includes(node);
|
|
panel.querySelector = (selector) => {
|
|
if (selector.startsWith('[data-dock-toggle-target')) return disclosure;
|
|
if (selector.split(',').some((part) => part.trim() === '.panel-title')) return { textContent: 'VISUAL STYLES' };
|
|
if (noChips) return null;
|
|
if (selector === '.map-stack-chip.active') return selected ? active : null;
|
|
return selector === '.map-stack-chip' ? first : null;
|
|
};
|
|
location.querySelector = (selector) => {
|
|
if (selector === '[data-dock-toggle-target="location-bar"]') {
|
|
return locationDisclosure?.getAttribute('data-dock-toggle-target') === 'location-bar' ? locationDisclosure : null;
|
|
}
|
|
if (selector.split(',').some((part) => part.trim() === '.location-toolbar-label')) return { textContent: 'LOCATION' };
|
|
return null;
|
|
};
|
|
panel.querySelectorAll = location.querySelectorAll = () => [];
|
|
const emit = (node, type, values = {}) => {
|
|
assert.ok(node, 'the event target exists in the actual panel markup');
|
|
const event = {
|
|
target: node, defaultPrevented: false, propagationStopped: false,
|
|
preventDefault() { this.defaultPrevented = true; },
|
|
stopPropagation() { this.propagationStopped = true; },
|
|
...values,
|
|
};
|
|
for (const callback of node.listeners.get(type) || []) callback(event);
|
|
return event;
|
|
};
|
|
const focus = (node) => {
|
|
const prior = document.activeElement;
|
|
if (prior === node) return;
|
|
document.activeElement = node;
|
|
for (const owner of [panel, location]) {
|
|
if (owner.contains(prior)) emit(owner, 'focusout', { target: prior, relatedTarget: node });
|
|
if (owner.contains(node)) emit(owner, 'focusin', { target: node, relatedTarget: prior });
|
|
}
|
|
};
|
|
disclosure.focus = () => focus(disclosure);
|
|
if (locationDisclosure) locationDisclosure.focus = () => focus(locationDisclosure);
|
|
for (const chip of [first, active]) {
|
|
chip.focus = () => { focusCalls += 1; if (!hidden) focus(chip); };
|
|
}
|
|
const window = {
|
|
setTimeout(callback, delay) { timers.set(++nextTimer, { callback, at: now + delay }); return nextTimer; },
|
|
};
|
|
window.clearTimeout = (id) => timers.delete(id);
|
|
window.performance = { now: () => now };
|
|
document.defaultView = window;
|
|
const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', 't', 'msg', 'readPanelState',
|
|
`return ({${shellMethod('_initAutoHoverPanel').toString()},\n${shellMethod('_collapsePanelOnEscape').toString()},\n${shellMethod('setPanelCollapsed').toString()},\n${shellMethod('_presentPanel').toString()},\n${shellMethod('_shownPanel').toString()}});`)(
|
|
createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, t, msg, readPanelState,
|
|
);
|
|
const saves = [];
|
|
const claims = [];
|
|
let shareSyncs = 0;
|
|
const chromeStore = usePanelChromeStore(createPinia());
|
|
const chromeToken = chromeStore.connect({});
|
|
// The trays' roots draw the store's state, as v-panel-state does.
|
|
followPanelState(chromeStore, 'control-panel', panel);
|
|
followPanelState(chromeStore, 'location-bar', location);
|
|
const manager = {
|
|
_lifetime: { frame() {} },
|
|
_chromeStore: chromeStore,
|
|
_chromeToken: chromeToken,
|
|
_panelLayout: { _leftStackPreferredPanelId: null, _rightStackPreferredPanelId: null },
|
|
...methods,
|
|
_savePanelCollapsedState(...args) { saves.push(args); },
|
|
_scheduleLeftPanelLayout() {}, _scheduleRightPanelLayout() {},
|
|
shareLinkManager: {
|
|
claimRestoreLane(...args) { claims.push(args); },
|
|
onPanelStateChange() { shareSyncs += 1; },
|
|
},
|
|
};
|
|
manager._initAutoHoverPanel('control-panel');
|
|
manager._initAutoHoverPanel('location-bar');
|
|
disclosure.focus();
|
|
const tick = () => {
|
|
const next = [...timers.entries()].sort((a, b) => a[1].at - b[1].at)[0];
|
|
if (!next) return;
|
|
timers.delete(next[0]); now = next[1].at; next[1].callback();
|
|
};
|
|
return {
|
|
manager, panel, location, disclosure, locationDisclosure, locationSearch,
|
|
first, active, elsewhere, document, timers, emit, focus, tick, saves, claims,
|
|
calls: () => focusCalls,
|
|
/** A tray's state as PanelChrome presents it to the dock's buttons. */
|
|
tray: (id) => chromeStore.panels[id],
|
|
shareSyncs: () => shareSyncs,
|
|
show() { hidden = false; },
|
|
select(value) { selected = value; },
|
|
key(key = 'Enter', repeat = false) { return emit(disclosure, 'keydown', { key, repeat }); },
|
|
keyUp(key = ' ', repeat = false) {
|
|
const event = emit(disclosure, 'keyup', { key, repeat });
|
|
if (key === ' ' && !repeat && document.activeElement === disclosure && !event.defaultPrevented) emit(disclosure, 'click', { detail: 0 });
|
|
return event;
|
|
},
|
|
escape() { emit(panel, 'keydown', { key: 'Escape' }); },
|
|
locationKey(key = 'Enter', repeat = false) { return emit(locationDisclosure, 'keydown', { key, repeat }); },
|
|
locationKeyUp(key = ' ', repeat = false) {
|
|
const event = emit(locationDisclosure, 'keyup', { key, repeat });
|
|
if (key === ' ' && !repeat && document.activeElement === locationDisclosure && !event.defaultPrevented) {
|
|
emit(locationDisclosure, 'click', { detail: 0 });
|
|
}
|
|
return event;
|
|
},
|
|
locationEscape() { return emit(location, 'keydown', { key: 'Escape', target: document.activeElement }); },
|
|
click(detail = 1) { emit(disclosure, 'click', { detail }); },
|
|
drain() { for (let i = 0; i < 40 && timers.size; i += 1) tick(); },
|
|
};
|
|
}
|
|
|
|
test('Enter focuses selected OSM rather than the first chip', () => {
|
|
const h = harness(); h.key(); h.tick();
|
|
assert.equal(h.document.activeElement, h.active);
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.timers.size, 0);
|
|
});
|
|
test('Space waits for release before its synthesized click opens the tray', () => {
|
|
const h = harness();
|
|
const down = h.key(' ');
|
|
assert.equal(down.defaultPrevented, false);
|
|
assert.equal(down.propagationStopped, false);
|
|
assert.equal(h.panel.classList.contains('collapsed'), true);
|
|
assert.equal(h.timers.size, 0);
|
|
h.keyUp(' '); h.tick();
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.document.activeElement, h.active);
|
|
});
|
|
test('repeated keydown does not close an opening tray or replace its timer', () => {
|
|
const h = harness(); h.key(); const timer = [...h.timers.keys()][0]; h.key('Enter', true);
|
|
assert.equal([...h.timers.keys()][0], timer);
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
});
|
|
test('missing selection falls back to the first chip', () => {
|
|
const h = harness({ selected: false }); h.key(); h.tick();
|
|
assert.equal(h.document.activeElement, h.first);
|
|
});
|
|
test('delayed visibility retries until focus lands', () => {
|
|
const h = harness({ hidden: true }); h.key(); h.tick(); h.tick();
|
|
assert.equal(h.document.activeElement, h.disclosure);
|
|
assert.equal(h.timers.size, 1); h.show(); h.tick();
|
|
assert.equal(h.document.activeElement, h.active);
|
|
assert.equal(h.calls(), 3); assert.equal(h.timers.size, 0);
|
|
});
|
|
test('selection is read again when a delayed attempt can focus', () => {
|
|
const h = harness({ hidden: true, selected: false }); h.key(); h.tick();
|
|
h.select(true); h.show(); h.tick();
|
|
assert.equal(h.document.activeElement, h.active);
|
|
});
|
|
test('permanently hidden and missing targets have bounded work', () => {
|
|
for (const options of [{ hidden: true }, { noChips: true }]) {
|
|
const h = harness(options); h.key(); h.drain();
|
|
assert.equal(h.timers.size, 0);
|
|
assert.equal(h.calls(), options.noChips ? 0 : 25);
|
|
assert.equal(h.document.activeElement, h.disclosure);
|
|
}
|
|
});
|
|
test('focus departure cancels immediately even if focus returns before the timer', () => {
|
|
const h = harness(); h.key(); h.focus(h.elsewhere); h.focus(h.disclosure);
|
|
assert.equal(h.timers.size, 0); h.drain();
|
|
assert.equal(h.document.activeElement, h.disclosure); assert.equal(h.calls(), 0);
|
|
});
|
|
test('deliberate focus movement inside the tray cancels pending retries', () => {
|
|
const h = harness({ hidden: true }); h.key(); h.tick(); h.focus(h.first);
|
|
assert.equal(h.timers.size, 0); h.show(); h.drain();
|
|
assert.equal(h.document.activeElement, h.first); assert.equal(h.calls(), 1);
|
|
});
|
|
test('Escape cancels immediately, closes the tray and restores disclosure focus', () => {
|
|
const h = harness({ hidden: true }); h.key(); h.tick(); h.escape();
|
|
assert.equal(h.timers.size, 0); assert.equal(h.document.activeElement, h.disclosure);
|
|
assert.equal(h.panel.classList.contains('collapsed'), true); h.show(); h.drain();
|
|
assert.equal(h.calls(), 1);
|
|
});
|
|
test('the central close seam cancels before both transition and already-closed return', () => {
|
|
for (const alreadyClosed of [false, true]) {
|
|
const h = harness(); h.key();
|
|
if (alreadyClosed) h.panel.classes.add('collapsed');
|
|
h.manager.setPanelCollapsed('control-panel', true, { persist: false, syncShare: false });
|
|
assert.equal(h.timers.size, 0); h.drain(); assert.equal(h.calls(), 0);
|
|
}
|
|
});
|
|
test('opening the unpinned Location sibling cancels the Map Source request', () => {
|
|
const h = harness(); h.key(); h.manager.setPanelCollapsed('location-bar', false);
|
|
assert.equal(h.panel.classList.contains('collapsed'), true);
|
|
assert.equal(h.timers.size, 0); h.drain(); assert.equal(h.calls(), 0);
|
|
});
|
|
test('a pinned Map Source tray retains its own request when the sibling opens', () => {
|
|
const h = harness(); h.manager._presentPanel(h.panel, { pinned: true }); h.key();
|
|
h.manager.setPanelCollapsed('location-bar', false); h.tick();
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.document.activeElement, h.active);
|
|
});
|
|
test('pointer activation revokes a pending keyboard request without moving focus', () => {
|
|
const h = harness(); h.key(); h.emit(h.panel, 'pointerdown');
|
|
assert.equal(h.timers.size, 0); h.drain();
|
|
assert.equal(h.document.activeElement, h.disclosure); assert.equal(h.calls(), 0);
|
|
});
|
|
test('close then pointer or programmatic reopen cannot inherit a keyboard request', () => {
|
|
for (const reopen of [(h) => h.click(1), (h) => h.manager.setPanelCollapsed('control-panel', false)]) {
|
|
const h = harness(); h.key(); const stale = [...h.timers.values()][0].callback;
|
|
h.escape(); reopen(h); stale(); h.drain();
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.document.activeElement, h.disclosure); assert.equal(h.calls(), 0);
|
|
}
|
|
});
|
|
test('a stale callback cannot focus or clear the timer owned by a new keyboard opening', () => {
|
|
const h = harness(); h.key(); const stale = [...h.timers.values()][0].callback;
|
|
h.escape(); h.key(); const timer = [...h.timers.keys()][0]; stale();
|
|
assert.equal(h.calls(), 0); assert.equal([...h.timers.keys()][0], timer);
|
|
h.tick(); assert.equal(h.document.activeElement, h.active);
|
|
});
|
|
test('plain pointer opening does not request focus', () => {
|
|
const h = harness(); h.click(1); h.drain();
|
|
assert.equal(h.document.activeElement, h.disclosure); assert.equal(h.calls(), 0);
|
|
});
|
|
test('pointer leave preserves established keyboard focus and the open tray', () => {
|
|
const h = harness(); h.key(); h.tick(); h.emit(h.panel, 'pointerleave', { pointerType: 'mouse' }); h.drain();
|
|
assert.equal(h.document.activeElement, h.active);
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
});
|
|
|
|
|
|
test('Location markup provides one named native disclosure linked to its popover', () => {
|
|
assert.ok(locationToggleMarkup, 'Location must expose a native button in the real markup');
|
|
assert.equal(locationToggleAttributes.type, 'button');
|
|
assert.equal(locationToggleAttributes['data-dock-toggle-target'], 'location-bar');
|
|
assert.equal(locationToggleAttributes['aria-controls'], 'location-bar-popover');
|
|
assert.equal(locationToggleAttributes['aria-expanded'], 'false');
|
|
assert.match(locationToggleAttributes['aria-label'], /expand.*location/i);
|
|
assert.match(locationToggleAttributes.class, /(?:^|\s)dock-tray-toggle(?:\s|$)/);
|
|
assert.match(locationToggleMarkup[2], /class="location-toolbar-label"/);
|
|
assert.doesNotMatch(locationToggleMarkup[2], /<button\b/);
|
|
assert.match(locationMarkup.slice(locationToggleMarkup.index + locationToggleMarkup[0].length),
|
|
/^\s*<button class="panel-collapse-btn" data-collapse-target="location-bar"/);
|
|
assert.match(locationMarkup, /<div id="location-bar-popover" class="dock-popover-content">/);
|
|
});
|
|
|
|
test('Location Enter stays immediate while Space waits for native key release', () => {
|
|
for (const key of ['Enter', ' ']) {
|
|
const h = harness(); h.focus(h.locationDisclosure); h.drain();
|
|
const event = h.locationKey(key);
|
|
assert.equal(event.defaultPrevented, key === 'Enter');
|
|
assert.equal(event.propagationStopped, key === 'Enter');
|
|
if (key === ' ') {
|
|
assert.equal(h.location.classList.contains('collapsed'), true);
|
|
h.locationKeyUp(key);
|
|
}
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.tray('location-bar').collapsed, false);
|
|
assert.equal(h.document.activeElement, h.locationDisclosure);
|
|
assert.equal(h.timers.size, 0);
|
|
assert.equal(h.calls(), 0);
|
|
const repeated = h.locationKey(key, true);
|
|
assert.equal(repeated.defaultPrevented, key === 'Enter');
|
|
assert.equal(repeated.propagationStopped, key === 'Enter');
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.timers.size, 0);
|
|
const release = key === ' ' ? h.locationKeyUp(key, true) : h.emit(h.locationDisclosure, 'keyup', { key });
|
|
assert.equal(release.defaultPrevented, false, 'global voice release must remain reachable');
|
|
assert.equal(release.propagationStopped, false);
|
|
h.locationKey(key);
|
|
if (key === ' ') h.locationKeyUp(key);
|
|
assert.equal(h.location.classList.contains('collapsed'), true);
|
|
assert.equal(h.tray('location-bar').collapsed, true);
|
|
}
|
|
});
|
|
|
|
test('Location synthesized click and Escape retain focus and its accessible name', () => {
|
|
const h = harness(); h.focus(h.locationDisclosure); h.drain();
|
|
h.emit(h.locationDisclosure, 'click', { detail: 0 });
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.calls(), 0);
|
|
assert.equal(h.timers.size, 0);
|
|
h.focus(h.locationSearch);
|
|
const escape = h.locationEscape();
|
|
assert.equal(escape.defaultPrevented, true);
|
|
assert.equal(h.location.classList.contains('collapsed'), true);
|
|
assert.equal(h.tray('location-bar').collapsed, true);
|
|
assert.equal(h.document.activeElement, h.locationDisclosure);
|
|
h.drain();
|
|
assert.equal(h.calls(), 0);
|
|
});
|
|
|
|
test('Location keyboard opening cancels the unpinned sibling and cannot revive its old callback', () => {
|
|
const h = harness({ hidden: true }); h.key(); h.tick();
|
|
const stale = [...h.timers.values()][0].callback;
|
|
h.focus(h.locationDisclosure); h.locationKey();
|
|
assert.equal(h.panel.classList.contains('collapsed'), true);
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
const attemptsBefore = h.calls();
|
|
h.show(); stale(); h.drain();
|
|
assert.equal(h.calls(), attemptsBefore);
|
|
assert.equal(h.document.activeElement, h.locationDisclosure);
|
|
});
|
|
|
|
test('Location keyboard opening preserves a pinned sibling and a pinned Location survives sibling opening', () => {
|
|
// Pins go through the store, as the dock's pin button does.
|
|
const h = harness(); h.manager._presentPanel(h.panel, { pinned: true }); h.key(); h.tick();
|
|
h.focus(h.locationDisclosure); h.locationKey(); h.drain();
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.document.activeElement, h.locationDisclosure);
|
|
h.manager._presentPanel(h.location, { pinned: true });
|
|
h.manager.setPanelCollapsed('control-panel', true);
|
|
h.focus(h.disclosure); h.key(); h.tick();
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.tray('location-bar').collapsed, false);
|
|
assert.equal(h.document.activeElement, h.active);
|
|
});
|
|
|
|
test('Location restoration presents its state without claiming focus, collapsing its sibling or persisting', () => {
|
|
const h = harness(); h.key(); h.tick();
|
|
const saves = h.saves.length;
|
|
const claims = h.claims.length;
|
|
const shareSyncs = h.shareSyncs();
|
|
h.manager.setPanelCollapsed('location-bar', false, { restore: true, persist: false, syncShare: false });
|
|
assert.equal(h.panel.classList.contains('collapsed'), false);
|
|
assert.equal(h.location.classList.contains('collapsed'), false);
|
|
assert.equal(h.tray('location-bar').collapsed, false);
|
|
assert.equal(h.document.activeElement, h.active);
|
|
assert.equal(h.saves.length, saves);
|
|
assert.equal(h.claims.length, claims);
|
|
assert.equal(h.shareSyncs(), shareSyncs);
|
|
const presented = h.tray('location-bar');
|
|
h.manager.setPanelCollapsed('location-bar', false, { restore: true, persist: false, syncShare: false });
|
|
assert.ok(h.tray('location-bar') === presented, 'same-state synchronization does not republish');
|
|
assert.equal(h.timers.size, 0);
|
|
});
|
|
|
|
test('disposed UI cannot complete a pending handoff', () => {
|
|
const h = harness({ hidden: true }); h.key(); h.tick();
|
|
h.manager._disposed = true; h.show(); h.drain();
|
|
assert.equal(h.document.activeElement, h.disclosure);
|
|
assert.equal(h.calls(), 1); assert.equal(h.timers.size, 0);
|
|
});
|
|
|
|
|
|
test('destroying hover controls cancels pending opens and removes their event routes', () => {
|
|
const h = harness();
|
|
h.panel.hovered = true;
|
|
h.emit(h.panel, 'pointerenter', { pointerType: 'mouse' });
|
|
assert.ok(h.timers.size > 0);
|
|
const control = h.manager._hoverPanelControls.get('control-panel');
|
|
control.destroy();
|
|
control.destroy();
|
|
assert.equal(h.timers.size, 0);
|
|
for (const node of [h.panel, h.disclosure]) {
|
|
assert.equal([...node.listeners.values()].flat().length, 0);
|
|
}
|
|
h.key();
|
|
h.drain();
|
|
assert.equal(h.panel.classes.has('collapsed'), true);
|
|
assert.equal(h.calls(), 0);
|
|
});
|
|
|
|
test('destroying an open tray cancels close and focus work without changing saved state', () => {
|
|
const h = harness({ hidden: true });
|
|
h.key();
|
|
h.emit(h.panel, 'pointerleave', { pointerType: 'mouse' });
|
|
const callbacks = [...h.timers.values()].map((timer) => timer.callback);
|
|
const saves = h.saves.length;
|
|
h.manager._hoverPanelControls.get('control-panel').destroy();
|
|
assert.equal(h.timers.size, 0);
|
|
h.show();
|
|
for (const callback of callbacks) callback();
|
|
assert.equal(h.panel.classes.has('collapsed'), false);
|
|
assert.equal(h.saves.length, saves);
|
|
assert.equal(h.calls(), 0, 'even an already queued focus attempt is revoked');
|
|
});
|
|
|
|
test('replacing hover controls removes the previous listeners before rebinding', () => {
|
|
const h = harness();
|
|
h.manager._initAutoHoverPanel('control-panel');
|
|
h.key();
|
|
assert.equal(h.panel.classes.has('collapsed'), false, 'one activation toggles only once');
|
|
h.tick();
|
|
assert.equal(h.calls(), 1);
|
|
});
|
|
|
|
|
|
test('teardown during an opening callback cannot enqueue a later focus handoff', () => {
|
|
const h = harness({ hidden: true });
|
|
const change = h.manager.setPanelCollapsed.bind(h.manager);
|
|
h.manager.setPanelCollapsed = (...args) => {
|
|
change(...args);
|
|
h.manager._hoverPanelControls.get('control-panel').destroy();
|
|
};
|
|
h.key();
|
|
assert.equal(h.timers.size, 0);
|
|
h.show();
|
|
h.drain();
|
|
assert.equal(h.calls(), 0);
|
|
});
|