// use-shortcuts depends on shortcut.js, which calls document.addEventListener at module load time. // Here we inject minimal document / window stubs first, then dynamically import the tested module. // // pulse-beacon.js pulls `@/utils/...` (Vite alias) and the `p` shortcut gates // on its build-time env flag — both are awkward in the Node runner. A load // hook stubs that module with a mutable `hasPulseBackend` so we can cover // both branches (the shortcut's other gate, configs.cloudFlare, is plain // test input). import assert from 'node:assert/strict'; import { describe, it, after } from 'node:test'; import { registerHooks } from 'node:module'; import { ref, computed, reactive, nextTick } from 'vue'; const documentHandlers = {}; // Per-test override (see "o" shortcut cases below). Default: no card is // currently highlighted via J/K, so any lookup returns null. let querySelectorImpl = () => null; globalThis.document = { addEventListener(type, handler) { documentHandlers[type] = handler; }, getElementById() { return null; }, querySelector(sel) { return querySelectorImpl(sel); }, }; globalThis.window = { scrollTo() {}, scrollY: 0, pageYOffset: 0, }; registerHooks({ load(url, context, nextLoad) { if (url.includes('/utils/features/pulse-beacon.js')) { return { format: 'module', shortCircuit: true, source: ` export const PULSE_BEACON_URL = 'https://pulse.test'; export const sendVisitBeacon = () => {}; export let hasPulseBackend = false; globalThis.__setPulseBackendForTest = (v) => { hasPulseBackend = Boolean(v); }; `, }; } return nextLoad(url, context); }, }); const { useShortcuts } = await import('../frontend/composables/use-shortcuts.js'); const { onAppEvent } = await import('../frontend/utils/app-events.js'); const { registerAppCommand } = await import('../frontend/utils/app-commands.js'); const t = (k) => `<${k}>`; // The test-running shortcuts dispatch on the command bus rather than calling // through refs. Register the five owner commands once, into a shared record // that loadAndGetKeyMap resets per test. const commandCalls = { ipRefresh: [], connectivity: [], webrtc: [], dnsleak: [], speedTest: 0 }; registerAppCommand('ipinfo:refresh', (payload) => { commandCalls.ipRefresh.push(payload.index); }); registerAppCommand('connectivity:run', (payload) => { commandCalls.connectivity.push(payload.trigger); }); registerAppCommand('webrtc:run', (payload) => { commandCalls.webrtc.push(payload.isRefresh); }); registerAppCommand('dnsleak:run', (payload) => { commandCalls.dnsleak.push(payload.isRefresh); }); registerAppCommand('speedtest:toggle', () => { commandCalls.speedTest += 1; }); const resetCommandCalls = () => { commandCalls.ipRefresh = []; commandCalls.connectivity = []; commandCalls.webrtc = []; commandCalls.dnsleak = []; commandCalls.speedTest = 0; }; function makeStoreStub() { const state = reactive({ refreshRequested: false, toggledSheets: [], }); return { state, setRefreshEveryThing(v) { state.refreshRequested = v; }, toggleSheet(name) { state.toggledSheets.push(name); }, }; } function makeRefs() { // UI-chrome calls live here; the test-section calls land in commandCalls // (merged into the returned record so assertions read one object). const calls = { queryOpen: 0, helpOpen: 0, shareOpen: 0, advancedNavigate: [], advancedFullScreen: 0, mask: 0, get speedTest() { return commandCalls.speedTest; }, get ipRefresh() { return commandCalls.ipRefresh; }, get connectivity() { return commandCalls.connectivity; }, get webrtc() { return commandCalls.webrtc; }, get dnsleak() { return commandCalls.dnsleak; }, }; const advancedToolsRef = ref({ openTool(slug) { calls.advancedNavigate.push(slug); }, fullScreen() { calls.advancedFullScreen += 1; }, }); return { refs: { queryIPRef: ref({ openModal: () => { calls.queryOpen += 1; } }), helpModalRef: ref({ openModal: () => { calls.helpOpen += 1; }, keyMap: null }), shareReportRef: ref({ openDialog: () => { calls.shareOpen += 1; } }), advancedToolsRef, isInfosLoaded: ref(true), toggleInfoMask: () => { calls.mask += 1; }, }, calls, }; } after(() => { globalThis.__setPulseBackendForTest?.(false); }); function loadAndGetKeyMap({ originalSite = false, ipHistoryEnabled = true, pulseBackend = false, cloudFlare = false, } = {}) { globalThis.__setPulseBackendForTest(pulseBackend); resetCommandCalls(); const store = makeStoreStub(); const { refs, calls } = makeRefs(); const configs = computed(() => ({ originalSite, map: true, cloudFlare })); const userPreferences = computed(() => ({ ipCardsToShow: 2, ipHistoryEnabled })); const { loadShortcuts } = useShortcuts({ refs, store, t, configs, userPreferences, }); loadShortcuts(); // helpModalRef.keyMap receives the global keyMap after loadShortcuts const keyMap = refs.helpModalRef.value.keyMap || []; return { keyMap, store, refs, calls }; } describe('useShortcuts()', () => { it('loadShortcuts() registers a keymap of 26+ entries on a non-original site', () => { const { keyMap } = loadAndGetKeyMap({ originalSite: false, pulseBackend: false, cloudFlare: false }); // 26 base entries (no invisibility / enhanced-DNS / pulse). const distinctKeys = new Set(keyMap.map((e) => e.keys)); assert.ok(distinctKeys.size >= 26, `expected ≥26 distinct shortcut keys, got ${distinctKeys.size}`); assert.ok(distinctKeys.has('R')); assert.ok(distinctKeys.has('?')); assert.ok(distinctKeys.has('g')); assert.ok(distinctKeys.has('o')); assert.ok(distinctKeys.has('H')); assert.ok(distinctKeys.has('x'), 'Ad & Tracker Blocking Test is a public tool'); assert.equal(distinctKeys.has('p'), false, 'pulse shortcut stays off without a pulse backend or cloudFlare key'); assert.equal(distinctKeys.has('P'), false, 'persona shortcut stays off a self-hosted instance'); assert.equal(distinctKeys.has('B'), false, 'blocklist shortcut stays off a self-hosted instance'); assert.equal(distinctKeys.has('A'), false, 'ASN Profile shortcut stays off without a cloudFlare key'); // Overlays take no keys at all (utils/shortcut.js), so there is no // drawer-only entry left to register. assert.equal(distinctKeys.has('f'), false, 'no shortcut acts on an open overlay'); }); it('originalSite=true adds invisibility ("i"), enhanced DNS-leak ("D"), blocklist ("B") and persona ("P") shortcuts', () => { const { keyMap, calls } = loadAndGetKeyMap({ originalSite: true }); const hasInvisibility = keyMap.some((e) => e.keys === 'i'); assert.ok(hasInvisibility, 'key "i" should be present on originalSite'); const D = keyMap.findLast((e) => e.keys === 'D'); assert.ok(D, 'key "D" should be present on originalSite'); D.action(); assert.deepEqual(calls.advancedNavigate, ['enhanceddnsleaktest']); // Uppercase B is its own key — lowercase b is Browser Info's. const B = keyMap.findLast((e) => e.keys === 'B'); assert.ok(B, 'key "B" should be present on originalSite'); B.action(); // Uppercase P is its own key — lowercase p is Earth Online's. const P = keyMap.findLast((e) => e.keys === 'P'); assert.ok(P, 'key "P" should be present on originalSite'); P.action(); assert.deepEqual(calls.advancedNavigate, ['enhanceddnsleaktest', 'blocklist', 'personacheck']); }); it('configs.cloudFlare adds the ASN Profile shortcut ("A")', () => { const { keyMap, calls } = loadAndGetKeyMap({ cloudFlare: true }); // Uppercase A is its own key — lowercase a is the IP calculator's. const A = keyMap.findLast((e) => e.keys === 'A'); assert.ok(A, 'key "A" should be present when configs.cloudFlare is set'); A.action(); assert.deepEqual(calls.advancedNavigate, ['asn']); }); it('"R" action triggers store.setRefreshEveryThing(true)', () => { const { keyMap, store } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.keys === 'R'); assert.ok(entry, '"R" key should be registered'); entry.action(); assert.equal(store.state.refreshRequested, true); }); it('advanced-tool shortcuts open via advancedToolsRef.openTool(slug)', () => { const { keyMap, calls } = loadAndGetKeyMap(); const l = keyMap.findLast((e) => e.keys === 'l'); l.action(); const M = keyMap.findLast((e) => e.keys === 'M'); M.action(); const t_ = keyMap.findLast((e) => e.keys === 't'); t_.action(); assert.deepEqual(calls.advancedNavigate, ['pingtest', 'macchecker', 'mtrtest']); }); it('numeric regex "[1-6]" passes through number argument to refreshCard', () => { const { keyMap, calls } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.type === 'regex'); assert.ok(entry); entry.action(2); // user typed "2" assert.deepEqual(calls.ipRefresh, [1], 'action receives num; refreshCard(card, num-1)'); }); it('numeric shortcut respects userPreferences.ipCardsToShow upper bound', () => { const { keyMap, calls } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.type === 'regex'); entry.action(6); // beyond ipCardsToShow (2) assert.deepEqual(calls.ipRefresh, [], 'num > ipCardsToShow → no-op'); }); it('"c"/"w"/"d"/"s" dispatch the section commands with their payloads', () => { const { keyMap, calls } = loadAndGetKeyMap(); keyMap.findLast((e) => e.keys === 'c').action(); keyMap.findLast((e) => e.keys === 'w').action(); keyMap.findLast((e) => e.keys === 'd').action(); keyMap.findLast((e) => e.keys === 's').action(); assert.deepEqual(calls.connectivity, ['manual']); assert.deepEqual(calls.webrtc, [false]); assert.deepEqual(calls.dnsleak, [true]); assert.equal(calls.speedTest, 1, 'the s key stays a run/pause toggle'); }); it('"?" opens help modal and emits the shortcut:help-opened app event', () => { // The achievement itself (CleverTrickery) is the engine's concern — // use-shortcuts only emits the domain event. See achievement-rules tests. let emitted = 0; const off = onAppEvent('shortcut:help-opened', () => { emitted += 1; }); const { keyMap, calls } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.keys === '?'); entry.action(); off(); assert.equal(calls.helpOpen, 1); assert.equal(emitted, 1); }); it('"e" opens the share report dialog', () => { const { keyMap, calls } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.keys === 'e'); assert.ok(entry, '"e" key should be registered'); entry.action(); assert.equal(calls.shareOpen, 1); }); it('"h" toggles info mask only when infos loaded', () => { const { keyMap, calls } = loadAndGetKeyMap(); const entry = keyMap.findLast((e) => e.keys === 'h'); entry.action(); assert.equal(calls.mask, 1); }); it('"H" opens the IP history sheet when ipHistoryEnabled', () => { const { keyMap, store } = loadAndGetKeyMap({ ipHistoryEnabled: true }); const entry = keyMap.findLast((e) => e.keys === 'H'); assert.ok(entry, '"H" key should be registered'); entry.action(); assert.deepEqual(store.state.toggledSheets, ['ipHistory']); }); it('"H" is a no-op when ipHistoryEnabled is false', () => { const { keyMap, store } = loadAndGetKeyMap({ ipHistoryEnabled: false }); const entry = keyMap.findLast((e) => e.keys === 'H'); entry.action(); assert.deepEqual(store.state.toggledSheets, []); }); it('the pulse backend flag adds the pulse shortcut (key "p")', () => { const { keyMap, store } = loadAndGetKeyMap({ pulseBackend: true }); const entry = keyMap.findLast((e) => e.keys === 'p'); assert.ok(entry, '"p" key should be present when hasPulseBackend'); entry.action(); assert.deepEqual(store.state.toggledSheets, ['pulse']); }); it('configs.cloudFlare alone adds the pulse shortcut (outage-only fork)', () => { const { keyMap, store } = loadAndGetKeyMap({ pulseBackend: false, cloudFlare: true }); const entry = keyMap.findLast((e) => e.keys === 'p'); assert.ok(entry, '"p" key should be present when configs.cloudFlare is set'); entry.action(); assert.deepEqual(store.state.toggledSheets, ['pulse']); }); it('configs landing after registration re-registers the map (late "p")', async () => { globalThis.__setPulseBackendForTest(false); const store = makeStoreStub(); const { refs } = makeRefs(); // Reactive configs source so the watcher inside useShortcuts can fire. const configsData = ref({ originalSite: false, map: true }); const configs = computed(() => configsData.value); const userPreferences = computed(() => ({ ipCardsToShow: 2, ipHistoryEnabled: true })); const { loadShortcuts } = useShortcuts({ refs, store, t, configs, userPreferences }); loadShortcuts(); const keyMap = refs.helpModalRef.value.keyMap; assert.equal(keyMap.some((e) => e.keys === 'p'), false, 'no "p" before configs resolve'); configsData.value = { originalSite: false, map: true, cloudFlare: true }; await nextTick(); // flush the configs watcher assert.equal(keyMap.some((e) => e.keys === 'p'), true, '"p" appears once cloudFlare lands'); }); it('"o" opens the highlighted advanced tool by its data-adv-slug', () => { const { keyMap, calls } = loadAndGetKeyMap(); // Stub the highlighted card to be an advanced tool card pointing at mtrtest. querySelectorImpl = (sel) => { if (sel === '.keyboard-shortcut-card[data-keyboard-hover="true"]') { return { getAttribute: (name) => name === 'data-adv-slug' ? 'mtrtest' : null, querySelector: () => null, }; } return null; }; const entry = keyMap.findLast((e) => e.keys === 'o'); entry.action(); querySelectorImpl = () => null; assert.deepEqual(calls.advancedNavigate, ['mtrtest']); }); it('"o" clicks the highlighted Connectivity card\'s site link (a[data-card-open])', () => { const { keyMap, calls } = loadAndGetKeyMap(); let clicked = 0; querySelectorImpl = (sel) => { if (sel === '.keyboard-shortcut-card[data-keyboard-hover="true"]') { return { getAttribute: () => null, querySelector: (inner) => (inner === 'a[data-card-open][href]' ? { click: () => { clicked += 1; } } : null), }; } return null; }; const entry = keyMap.findLast((e) => e.keys === 'o'); entry.action(); querySelectorImpl = () => null; assert.equal(clicked, 1); assert.deepEqual(calls.advancedNavigate, []); }); it('"o" is a no-op when the highlighted card has no slug and no site link (e.g. IP card)', () => { const { keyMap, calls } = loadAndGetKeyMap(); querySelectorImpl = () => ({ getAttribute: () => null, querySelector: () => null }); const entry = keyMap.findLast((e) => e.keys === 'o'); entry.action(); querySelectorImpl = () => null; assert.deepEqual(calls.advancedNavigate, []); }); it('"o" is a no-op when nothing is highlighted', () => { const { keyMap, calls } = loadAndGetKeyMap(); // querySelectorImpl default returns null (nothing highlighted). const entry = keyMap.findLast((e) => e.keys === 'o'); entry.action(); assert.deepEqual(calls.advancedNavigate, []); }); });