import { describe, expect, it, vi } from 'vitest'; import { Actor } from 'xstate'; import { MouseButton } from '../App.types.ts'; import { getActiveToolActor, setActiveToolActor } from '../state.ts'; import { Tool } from '../tools.ts'; import { createSequenceTool } from '../tools/factories/sequence-tool.ts'; import { selectToolStateMachine } from '../tools/select-tool.ts'; import { activeToolWantsFreeText, ImeSink, isImeKey } from './ime-sink.ts'; import { InputController } from './input-controller.ts'; describe('한글 IME 입력', () => { it('조합 사건으로 「가나다」 가 들어간다', () => { let text = ''; const sink = new ImeSink((data) => { text += data; }); for (const [steps, done] of [ [['ㄱ', '가'], '가'], [['ㄴ', '나'], '나'], [['ㄷ', '다'], '다'], ] as const) { for (const step of steps) sink.handleCompositionUpdate(step); expect(sink.composing).toBe(steps[steps.length - 1]); sink.handleCompositionEnd(done); } expect(text).toBe('가나다'); expect(sink.composing).toBe(''); // 조합 중 input 은 compositionend 몫 — 겹쳐 넣지 않음 · 조합 뒤 띄어쓰기는 받음 sink.handleInput({ isComposing: true, data: '다' }); sink.handleInput({ isComposing: false, data: ' ' }); expect(text).toBe('가나다 '); }); it('조합 중 키는 단축키로 먹지 않는다', () => { expect(isImeKey({ isComposing: true, keyCode: 0, key: 'r' } as KeyboardEvent)).toBe(true); expect(isImeKey({ isComposing: false, keyCode: 229, key: 'r' } as KeyboardEvent)).toBe(true); expect(isImeKey({ isComposing: false, keyCode: 0, key: 'Process' } as KeyboardEvent)).toBe( true ); expect(isImeKey({ isComposing: false, keyCode: 76, key: 'l' } as KeyboardEvent)).toBe(false); const controller = new InputController(); const preventDefault = vi.fn(); controller.handleKeyStroke({ key: 'Process', keyCode: 229, isComposing: true, target: null, preventDefault, stopPropagation: vi.fn(), } as unknown as KeyboardEvent); expect(preventDefault).not.toHaveBeenCalled(); controller.ime.handleCompositionEnd('가나다'); expect((controller as unknown as { text: string }).text).toBe('가나다'); }); }); describe('Space = Enter(오토캐드)', () => { const press = (controller: InputController, key: string, extra: Partial = {}) => controller.handleKeyStroke({ key, keyCode: 0, isComposing: false, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, repeat: false, target: null, preventDefault: vi.fn(), stopPropagation: vi.fn(), ...extra, } as unknown as KeyboardEvent); const textOf = (controller: InputController) => (controller as unknown as { text: string }).text; const activeTool = () => getActiveToolActor()?.getSnapshot()?.context?.type; const selectIdle = () => setActiveToolActor(new Actor(selectToolStateMachine), false); it('「L␣」 = LINE 실행', () => { selectIdle(); const controller = new InputController(); press(controller, 'l'); press(controller, ' '); expect(activeTool()).toBe(Tool.LINE); expect(textOf(controller)).toBe(''); }); it('조합 중 Space · Shift/Ctrl+Space · 길게 눌러 반복은 무시', () => { selectIdle(); const controller = new InputController(); press(controller, 'l'); press(controller, ' ', { isComposing: true, keyCode: 229 }); press(controller, ' ', { shiftKey: true }); press(controller, ' ', { ctrlKey: true }); press(controller, ' ', { repeat: true }); expect(textOf(controller)).toBe('L'); expect(activeTool()).toBe(Tool.SELECT); }); it('글 단계(TEXT 등)에서는 띄어쓰기', () => { const machine = createSequenceTool({ tool: Tool.TEXT, steps: [{ kind: 'text', instructions: '문자를 입력하십시오.' }], commit: () => {}, }); setActiveToolActor(new Actor(machine), false); expect(activeToolWantsFreeText()).toBe(true); const controller = new InputController(); press(controller, 'a'); press(controller, ' '); press(controller, 'b'); expect(textOf(controller)).toBe('a b'); expect(activeTool()).toBe(Tool.TEXT); }); it('빈 Space · Enter = 마지막 명령 반복 · 빈 우클릭은 반복 안 함', () => { selectIdle(); const controller = new InputController(); press(controller, 'l'); press(controller, 'Enter'); expect(activeTool()).toBe(Tool.LINE); selectIdle(); expect(activeToolWantsFreeText()).toBe(false); press(controller, ' '); expect(activeTool()).toBe(Tool.LINE); selectIdle(); press(controller, 'Enter'); expect(activeTool()).toBe(Tool.LINE); selectIdle(); controller.handleMouseUp({ button: MouseButton.Right, target: null, preventDefault: vi.fn(), stopPropagation: vi.fn(), } as unknown as MouseEvent); expect(activeTool()).toBe(Tool.SELECT); }); });