import fs from 'fs' import path from 'path' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MouseButton, VK } from '@d3ro/core/keybinding' type EffectFn = () => void | (() => void) const effects: EffectFn[] = [] vi.mock('react', () => ({ useEffect: (fn: EffectFn) => { effects.push(fn) }, })) const { KeyBindingCaptureSession, resolveCapturePort, useKeyBindingCaptureSuspension, } = await import('../../src/renderer/components/keybinding/keybinding-capture') const { BROWSER_BUTTON_TO_MOUSE, NO_MODIFIERS, buildBinding, resolveKeyCode } = await import( '../../src/renderer/components/keybinding/keybinding-recording' ) const PICKER = path.resolve( __dirname, '../../src/renderer/components/keybinding/KeyBindingPicker.tsx' ) function fakePort() { const calls: string[] = [] return { calls, port: { beginCapture: vi.fn(async () => { calls.push('begin') }), endCapture: vi.fn(async () => { calls.push('end') }), }, } } describe('KeyBindingPicker — 녹화 중 전역 트리거 중단 (r3-23)', () => { beforeEach(() => { effects.length = 0 }) it('피커가 열려 있는 동안 캡처 억제 훅을 open 으로 건다', () => { const source = fs.readFileSync(PICKER, 'utf8') expect(source).toContain('useKeyBindingCaptureSuspension(open)') // 전역 on/off 는 hotkeyEnabled 를 저장하므로 캡처에 쓰면 안 된다. expect(source).not.toMatch(/keybinding\s*\.\s*setEnabled/) }) it('열리면 beginCapture, 닫히거나 언마운트되면 endCapture 를 한 번씩 보낸다', () => { const { port, calls } = fakePort() useKeyBindingCaptureSuspension(true, () => port) expect(effects).toHaveLength(1) const cleanup = effects[0]() expect(calls).toEqual(['begin']) expect(typeof cleanup).toBe('function') if (typeof cleanup === 'function') cleanup() expect(calls).toEqual(['begin', 'end']) }) it('닫혀 있으면 아무것도 보내지 않는다', () => { const { port, calls } = fakePort() useKeyBindingCaptureSuspension(false, () => port) const cleanup = effects[0]() expect(cleanup).toBeUndefined() expect(calls).toEqual([]) }) it('세션은 begin/end 를 중복 전송하지 않고 begin 없이 end 를 보내지 않는다', () => { const { port, calls } = fakePort() const session = new KeyBindingCaptureSession(port) session.end() session.begin() session.begin() expect(session.isActive).toBe(true) session.end() session.end() expect(session.isActive).toBe(false) expect(calls).toEqual(['begin', 'end']) }) it('IPC 실패는 녹화를 막지 않는다', async () => { const session = new KeyBindingCaptureSession({ beginCapture: () => Promise.reject(new Error('no handler')), endCapture: () => Promise.reject(new Error('no handler')), }) expect(() => { session.begin() session.end() }).not.toThrow() await Promise.resolve() }) it('포트가 없으면(null) 조용히 아무것도 하지 않는다', () => { const session = new KeyBindingCaptureSession(null) session.begin() expect(session.isActive).toBe(false) }) it('preload API 구조로 포트를 판별한다', () => { expect(resolveCapturePort(undefined)).toBeNull() expect(resolveCapturePort({ setEnabled: () => undefined })).toBeNull() expect(resolveCapturePort({ beginCapture: () => undefined })).toBeNull() const begin = vi.fn(() => Promise.resolve({ success: true })) const end = vi.fn(() => Promise.resolve({ success: true })) const port = resolveCapturePort({ beginCapture: begin, endCapture: end }) expect(port).not.toBeNull() void port?.beginCapture() void port?.endCapture() expect(begin).toHaveBeenCalledTimes(1) expect(end).toHaveBeenCalledTimes(1) }) }) describe('keybinding-recording — 추출된 녹화 규칙 (동작 보존)', () => { it('좌/우 수정자를 location 으로 구분한다', () => { expect(resolveKeyCode({ keyCode: 17, which: 17, location: 1 })).toBe(VK.CtrlLeft) expect(resolveKeyCode({ keyCode: 17, which: 17, location: 2 })).toBe(VK.CtrlRight) expect(resolveKeyCode({ keyCode: 16, which: 16, location: 2 })).toBe(VK.ShiftRight) expect(resolveKeyCode({ keyCode: 18, which: 18, location: 1 })).toBe(VK.AltLeft) expect(resolveKeyCode({ keyCode: 93, which: 93, location: 2 })).toBe(VK.MetaRight) expect(resolveKeyCode({ keyCode: 93, which: 93, location: 0 })).toBe(93) expect(resolveKeyCode({ keyCode: 0, which: 65, location: 0 })).toBe(65) }) it('주 키는 비수정자 키, 없으면 마지막 수정자', () => { expect(buildBinding([], NO_MODIFIERS)).toBeNull() const combo = buildBinding([VK.CtrlLeft, 65], { ...NO_MODIFIERS, ctrl: true }) expect(combo).toMatchObject({ device: 'keyboard', code: 65, ctrl: true }) const mods = buildBinding([VK.CtrlLeft, VK.AltRight], { ...NO_MODIFIERS, ctrl: true }) expect(mods?.code).toBe(VK.AltRight) }) it('브라우저 마우스 버튼을 uiohook 코드로 옮긴다', () => { expect(BROWSER_BUTTON_TO_MOUSE[1]).toBe(MouseButton.Middle) expect(BROWSER_BUTTON_TO_MOUSE[2]).toBe(MouseButton.Right) expect(BROWSER_BUTTON_TO_MOUSE[3]).toBe(MouseButton.Back) expect(BROWSER_BUTTON_TO_MOUSE[4]).toBe(MouseButton.Forward) }) })