import assert from 'node:assert/strict'; import test from 'node:test'; import { createRendererState } from '../state.js'; import { createControllerDebugModal } from './controller-debug.js'; function createClassList(initialTokens: string[] = []) { const tokens = new Set(initialTokens); return { add: (...entries: string[]) => { for (const entry of entries) tokens.add(entry); }, remove: (...entries: string[]) => { for (const entry of entries) tokens.delete(entry); }, contains: (entry: string) => tokens.has(entry), }; } test('controller debug modal renders active controller axes, buttons, and config-ready button indices', () => { const globals = globalThis as typeof globalThis & { window?: unknown }; const previousWindow = globals.window; Object.defineProperty(globalThis, 'window', { configurable: true, value: { electronAPI: { notifyOverlayModalClosed: () => {}, }, }, }); try { const state = createRendererState(); state.connectedGamepads = [{ id: 'pad-1', index: 0, mapping: 'standard', connected: true }]; state.activeGamepadId = 'pad-1'; state.controllerRawAxes = [0.5, -0.25]; state.controllerRawButtons = [{ value: 1, pressed: true, touched: true }]; state.controllerConfig = { enabled: true, preferredGamepadId: '', preferredGamepadLabel: '', smoothScroll: true, scrollPixelsPerSecond: 900, horizontalJumpPixels: 160, stickDeadzone: 0.2, triggerInputMode: 'auto', triggerDeadzone: 0.5, repeatDelayMs: 320, repeatIntervalMs: 120, buttonIndices: { select: 6, buttonSouth: 0, buttonEast: 1, buttonWest: 2, buttonNorth: 3, leftShoulder: 4, rightShoulder: 5, leftStickPress: 9, rightStickPress: 10, leftTrigger: 6, rightTrigger: 7, }, bindings: { toggleLookup: 'buttonSouth', closeLookup: 'buttonEast', toggleKeyboardOnlyMode: 'buttonNorth', mineCard: 'buttonWest', quitMpv: 'select', previousAudio: 'none', nextAudio: 'rightShoulder', playCurrentAudio: 'leftShoulder', toggleMpvPause: 'leftStickPress', leftStickHorizontal: 'leftStickX', leftStickVertical: 'leftStickY', rightStickHorizontal: 'rightStickX', rightStickVertical: 'rightStickY', }, }; const ctx = { dom: { overlay: { classList: createClassList() }, controllerDebugModal: { classList: createClassList(['hidden']), setAttribute: () => {}, }, controllerDebugClose: { addEventListener: () => {} }, controllerDebugToast: { textContent: '', classList: createClassList(['hidden']) }, controllerDebugStatus: { textContent: '', classList: createClassList() }, controllerDebugSummary: { textContent: '' }, controllerDebugAxes: { textContent: '' }, controllerDebugButtons: { textContent: '' }, controllerDebugButtonIndices: { textContent: '' }, }, state, }; const modal = createControllerDebugModal(ctx as never, { modalStateReader: { isAnyModalOpen: () => false }, syncSettingsModalSubtitleSuppression: () => {}, }); modal.openControllerDebugModal(); assert.match(ctx.dom.controllerDebugStatus.textContent, /pad-1/); assert.match(ctx.dom.controllerDebugSummary.textContent, /standard/); assert.match(ctx.dom.controllerDebugAxes.textContent, /axis\[0\] = 0\.500/); assert.match(ctx.dom.controllerDebugButtons.textContent, /button\[0\] value=1\.000 pressed=true/); assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"buttonIndices": \{/); assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"select": 6/); assert.match(ctx.dom.controllerDebugButtonIndices.textContent, /"leftStickPress": 9/); } finally { Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); } }); test('controller debug modal copies buttonIndices config to clipboard', async () => { const globals = globalThis as typeof globalThis & { window?: unknown; navigator?: unknown; }; const previousWindow = globals.window; const previousNavigator = globals.navigator; const copied: string[] = []; const handlers: { copy: null | (() => void) } = { copy: null }; Object.defineProperty(globalThis, 'window', { configurable: true, value: { electronAPI: { notifyOverlayModalClosed: () => {}, }, }, }); Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { clipboard: { writeText: async (text: string) => { copied.push(text); }, }, }, }); try { const state = createRendererState(); state.controllerConfig = { enabled: true, preferredGamepadId: '', preferredGamepadLabel: '', smoothScroll: true, scrollPixelsPerSecond: 900, horizontalJumpPixels: 160, stickDeadzone: 0.2, triggerInputMode: 'auto', triggerDeadzone: 0.5, repeatDelayMs: 320, repeatIntervalMs: 120, buttonIndices: { select: 6, buttonSouth: 0, buttonEast: 1, buttonWest: 2, buttonNorth: 3, leftShoulder: 4, rightShoulder: 5, leftStickPress: 9, rightStickPress: 10, leftTrigger: 6, rightTrigger: 7, }, bindings: { toggleLookup: 'buttonSouth', closeLookup: 'buttonEast', toggleKeyboardOnlyMode: 'buttonNorth', mineCard: 'buttonWest', quitMpv: 'select', previousAudio: 'none', nextAudio: 'rightShoulder', playCurrentAudio: 'leftShoulder', toggleMpvPause: 'leftStickPress', leftStickHorizontal: 'leftStickX', leftStickVertical: 'leftStickY', rightStickHorizontal: 'rightStickX', rightStickVertical: 'rightStickY', }, }; const ctx = { dom: { overlay: { classList: createClassList() }, controllerDebugModal: { classList: createClassList(['hidden']), setAttribute: () => {}, }, controllerDebugClose: { addEventListener: () => {} }, controllerDebugCopy: { addEventListener: (_event: string, handler: () => void) => { handlers.copy = handler; }, }, controllerDebugToast: { textContent: '', classList: createClassList(['hidden']) }, controllerDebugStatus: { textContent: '', classList: createClassList() }, controllerDebugSummary: { textContent: '' }, controllerDebugAxes: { textContent: '' }, controllerDebugButtons: { textContent: '' }, controllerDebugButtonIndices: { textContent: '' }, }, state, }; const modal = createControllerDebugModal(ctx as never, { modalStateReader: { isAnyModalOpen: () => false }, syncSettingsModalSubtitleSuppression: () => {}, }); modal.wireDomEvents(); modal.openControllerDebugModal(); if (handlers.copy) { handlers.copy(); } await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(copied, [ctx.dom.controllerDebugButtonIndices.textContent]); assert.match(ctx.dom.controllerDebugStatus.textContent, /Copied controller buttonIndices config/); assert.match(ctx.dom.controllerDebugToast.textContent, /Copied controller buttonIndices config/); assert.equal(ctx.dom.controllerDebugToast.classList.contains('hidden'), false); } finally { Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow }); Object.defineProperty(globalThis, 'navigator', { configurable: true, value: previousNavigator, }); } });