// tests/main/services/keybinding-altgr-redteam-r3-8.test.ts // 레드팀 r3-8 회귀 테스트 (데스크톱 어댑터) — AltGr 배열. // // Windows 는 AltGr 를 "가짜 LCtrl 눌림 → RAlt 눌림"(같은 time)으로 보내고, libuiohook 은 그 // RAlt 눌림에 ctrlKey 를 싣는다. 어댑터가 그대로 옮기면 '@'(AltGr+Q) 마다 'command' 가 발동하고 // 기본 dictation(RightAlt 단독)은 발동하지 않는다. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { uIOhook, UiohookKey } from 'uiohook-napi' import type { UiohookKeyboardEvent } from 'uiohook-napi' import { ALTGR_CHORD_GRACE_MS } from '@d3ro/core/keybinding-runtime' import { initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService' import { getKeyBindingService } from '../../../src/main/services/KeyBindingService' import type { KeyBindingTriggerPayload } from '../../../src/main/services/KeyBindingService' const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') function setPlatform(platform: NodeJS.Platform): void { Object.defineProperty(process, 'platform', { value: platform, configurable: true }) } function restorePlatform(): void { if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform) } type KeyHandler = (e: UiohookKeyboardEvent) => void function handlerOf(event: 'keydown' | 'keyup'): KeyHandler { const calls = vi.mocked(uIOhook.on).mock.calls.filter((c) => c[0] === event) const last = calls[calls.length - 1] if (!last) throw new Error(`no ${event} handler`) return last[1] as KeyHandler } interface Mods { ctrl?: boolean alt?: boolean } function keyEvent(keycode: number, time: number, mods: Mods = {}): UiohookKeyboardEvent { return { keycode, time, ctrlKey: mods.ctrl ?? false, altKey: mods.alt ?? false, shiftKey: false, metaKey: false } as unknown as UiohookKeyboardEvent } describe('KeyBindingService — AltGr 배열 (Windows)', () => { const events: string[] = [] const listener = (p: KeyBindingTriggerPayload): void => { events.push(`${p.actionId}:${p.type}`) } beforeEach(() => { vi.useFakeTimers() setPlatform('win32') initInMemoryConfig() vi.mocked(uIOhook.on).mockClear() events.length = 0 const service = getKeyBindingService() service.loadFromConfig() service.start() service.on('triggered', listener) }) afterEach(() => { const service = getKeyBindingService() service.off('triggered', listener) service.stop() resetInMemoryConfig() restorePlatform() vi.useRealTimers() }) function altGrDown(time: number): void { handlerOf('keydown')(keyEvent(UiohookKey.Ctrl, time, { ctrl: true })) handlerOf('keydown')(keyEvent(UiohookKey.AltRight, time, { ctrl: true, alt: true })) } function altGrUp(): void { handlerOf('keyup')(keyEvent(UiohookKey.Ctrl, 0, { alt: true })) handlerOf('keyup')(keyEvent(UiohookKey.AltRight, 0)) } it("AltGr+Q('@') 는 어떤 트리거도 내보내지 않는다", () => { altGrDown(5_000) handlerOf('keydown')(keyEvent(UiohookKey.Q, 5_040, { ctrl: true, alt: true })) handlerOf('keyup')(keyEvent(UiohookKey.Q, 5_080, { ctrl: true, alt: true })) altGrUp() vi.advanceTimersByTime(1_000) expect(events).toEqual([]) }) it('AltGr 를 누르고 있으면 기본 dictation 이 발동한다 (command 가 아니다)', () => { altGrDown(6_000) vi.advanceTimersByTime(ALTGR_CHORD_GRACE_MS) expect(events).toEqual(['dictation:pressed']) altGrUp() expect(events).toEqual(['dictation:pressed', 'dictation:released']) }) })