- 새 src/commands/prompt.ts — PromptSpec · renderPrompt(키워드 물음은 「또는」 없음) · matchOption(initget 줄임) · 세션 기억 · OPT · SELECT_OBJECTS - 새 inputController/prompt-input.ts — routeTyped 사다리(점 · 옵션 묻는 중 명령 이름 거절 · 식 · 단위 거절 · 투명 ') · 알림 줄 · 덧물음 · cancelActiveCommand · undoCommand - 새 tools/factories/end-command.ts — final(END) 갈고리 → 같은 틱 선택 도구 · state.ts 갈고리 · LASTPOINT - sequence-tool v2 — ROUTE · id · prompt · when · next · options · keyword · loopFrom · escCommits · pickfirst · 확정 뒤 기본 끝 · Esc 늘 끝 - 흠 — N1 ②③ · N5 · N6 · N11 · N18(앞글자 실행 없앰) · H1(Ctrl · Alt 글자 안 쌓음 · Ctrl+C · X · Shift+C · Shift+V · 1) · 우클릭 = 쉴 때 반복 · SE 뺌 · ED 더함 - 시험 — cad-drive 도우미 · prompt-frame · prompt-contract · 옛 시험 손질(keyboard-chain · ime-sink · command-catalog · input-prefs-cad · eraser 녹화) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
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<KeyboardEvent> = {}) =>
|
|
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 · 우클릭 = 마지막 명령 반복(337 · 쉴 때 우클릭도)', () => {
|
|
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.LINE);
|
|
});
|
|
});
|