/** * 337 가-1 · 가-2 옵션 틀 — 물음 꼴 · 옵션 맞추기 · 친 글 사다리 · 거절 · Esc · 빈 Enter · 우클릭 · 명령 끝 · 엔진 v2 · 기억 · 덧물음 · Ctrl. * 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 2 장 · 6-3 가 줄. * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad prompt-frame */ import { readFileSync } from "node:fs"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("react-toastify", () => ({ toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), })); import { Actor } from "xstate"; import { OPT, type OptionSpec, type PromptSpec, SELECT_OBJECTS, matchOption, previousSelection, recall, remember, renderPrompt, } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; import { TOOL_STATE_MACHINES } from "../../../B07_DesignDetail/openwebcad/src/commands/registry"; import { getLastCommandId, runCommandInput, } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; import { pushSubPrompt, routeTyped, } from "../../../B07_DesignDetail/openwebcad/src/inputController/prompt-input"; import { getActiveToolActor, getSelectedEntityIds, setActiveToolActor, setSelectedEntityIds, } from "../../../B07_DesignDetail/openwebcad/src/state"; import { commandCandidate } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-history"; import { getSnapOverride } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-snap-override"; import { getScreenCanvasDrawController } from "../../../B07_DesignDetail/openwebcad/src/state"; import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; import { createSequenceTool } from "../../../B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool"; import { activeTool, click, driver, entities, initIdle, inputText, key, noticeLine, promptLine, rightClick, toolState, type, typeEnter, typed, } from "./helpers/cad-drive"; type Xy = { x: number; y: number }; const opt = (key: string, kw: string, label: string): OptionSpec => ({ key, kw, en: key, label }); const start = (machine: unknown) => setActiveToolActor(new Actor(machine as never), false); const enter = () => key("Enter"); const lineIds = () => entities("Line").map((entity) => entity.id); beforeEach(() => { initIdle(); }); describe("옵션 맞추기(2-4)", () => { const prompt: PromptSpec = { text: "다음 점 지정", options: [ opt("CENTER", "C", "중심"), opt("CLOSE", "CL", "닫기"), { key: "LTYPE", kw: "LT", en: "LTYPE", label: "선종류" }, OPT.UNDO, OPT.ALL, ], absent: [{ key: "ELEVATION", kw: "E", en: "ELEVATION", label: "고도" }], }; const keyOf = (text: string) => { const hit = matchOption(text, prompt); return hit && "option" in hit ? hit.option.key : hit && "absent" in hit ? `없음:${hit.absent.key}` : null; }; it("대소문자 · 앞 `_` · 빈칸 · kw · en · 사이 길이", () => { expect(keyOf("c")).toBe("CENTER"); expect(keyOf(" _center ")).toBe("CENTER"); expect(keyOf("CE")).toBe("CENTER"); expect(keyOf("cl")).toBe("CLOSE"); // kw 딱 맞음 우선 expect(keyOf("clo")).toBe("CLOSE"); expect(keyOf("u")).toBe("UNDO"); expect(keyOf("und")).toBe("UNDO"); expect(keyOf("lty")).toBe("LTYPE"); }); it("kw 보다 짧으면 안 맞음 · 숨김도 맞음 · 없는 옵션 안내 · 엉뚱한 글", () => { expect(keyOf("l")).toBeNull(); // LT 의 L 은 모자람(initget) expect(keyOf("all")).toBe("ALL"); expect(keyOf("e")).toBe("없음:ELEVATION"); expect(keyOf("zz")).toBeNull(); }); }); describe("물음 그림(2-3)", () => { it("옵션 · 기본 · 숨김", () => { expect(renderPrompt({ text: "다음 점 지정", options: [OPT.CLOSE, OPT.UNDO] })).toBe( "다음 점 지정 또는 [닫기(C)/명령 취소(U)]:", ); expect( renderPrompt({ text: "기준점 지정", options: [OPT.DISPLACEMENT], default: { kind: "option", key: "DISPLACEMENT" }, }), ).toBe("기준점 지정 또는 [변위(D)] <변위>:"); expect(renderPrompt(SELECT_OBJECTS)).toBe("객체 선택:"); expect( renderPrompt({ text: "두 번째 점 지정", default: { kind: "enter", label: "첫 번째 점을 변위로 사용" }, }), ).toBe("두 번째 점 지정 <첫 번째 점을 변위로 사용>:"); }); it("값 기본 — 소수 넷 · 정수 · 각 · 좌표 · 기억이 처음 값을 이김 · null 은 안 그림", () => { const length: PromptSpec = { text: "직사각형의 길이 지정", default: { kind: "value", remember: "T.RECT", initial: 10 }, }; expect(renderPrompt(length)).toBe("직사각형의 길이 지정 <10.0000>:"); remember("T.RECT", 12.5); expect(renderPrompt(length)).toBe("직사각형의 길이 지정 <12.5000>:"); expect(recall("T.RECT")).toBe(12.5); expect( renderPrompt({ text: "변의 수 입력", default: { kind: "value", remember: "T.INT", initial: 4, format: "int" }, }), ).toBe("변의 수 입력 <4>:"); expect( renderPrompt({ text: "각 지정", default: { kind: "value", remember: "T.ANG", initial: 22.5, format: "ang" }, }), ).toBe("각 지정 <22.5>:"); expect( renderPrompt({ text: "변위 지정", default: { kind: "value", remember: "T.XY", initial: { x: 0, y: 0 } }, }), ).toBe("변위 지정 <0.0000, 0.0000>:"); expect( renderPrompt({ text: "반지름 지정", default: { kind: "value", remember: "T.NULL", initial: null }, }), ).toBe("반지름 지정:"); }); }); /** 빈 Enter 넷 갈래 · 숫자 검사 · 키워드 — 변의 수 → 중심 → 내접/외접 → 점 되풀이 */ const commits: unknown[][] = []; const polygonLike = createSequenceTool({ tool: Tool.POLYGON, repeatLastStep: true, steps: [ { kind: "number", id: "sides", prompt: { text: "변의 수 입력", default: { kind: "value", remember: "T.SIDES", initial: 4, format: "int" }, number: { int: true, min: 3, max: 1024 }, }, }, { kind: "point", id: "center", prompt: { text: "중심 지정", default: { kind: "value", remember: "T.CENTER", initial: { x: 5, y: 6 } }, }, }, { kind: "keyword", id: "ic", prompt: { text: "옵션 입력", options: [opt("INSCRIBED", "I", "원에 내접"), opt("CIRCUMSCRIBED", "C", "원에 외접")], default: { kind: "option", key: "INSCRIBED", label: "I" }, }, }, { kind: "point", id: "pts", prompt: { text: "다음 점 지정", default: { kind: "enter", label: "끝" } }, }, ], commit: (input) => { const center = input.get("center"); commits.push([input.get("sides"), center.x, center.y, input.get("ic"), input.points().length]); }, }); describe("빈 Enter 넷 갈래 · 거절 · 기억 · 끝(2-5 · 2-6 · 2-8 · 2-9)", () => { beforeEach(() => { commits.length = 0; }); it("값(숫자) · 값(좌표) · 옵션 · enter → 확정 → 같은 틱에 쉬는 상태", () => { start(polygonLike); expect(promptLine()).toBe("변의 수 입력 <4>:"); enter(); // 4 expect(promptLine()).toBe("중심 지정 <5.0000, 6.0000>:"); enter(); // (5, 6) expect(promptLine()).toBe("옵션 입력 [원에 내접(I)/원에 외접(C)] :"); enter(); // INSCRIBED type("1,1"); expect(promptLine()).toBe("다음 점 지정 <끝>:"); enter(); // 되풀이 점 = 확정 expect(commits).toEqual([[4, 5, 6, "INSCRIBED", 2]]); expect(activeTool()).toBe(Tool.SELECT); // await 없이 expect(toolState()).toBe("WAITING_FOR_FIRST_SELECT_POINT"); }); it("숫자 물음 — 식 · 단위 · 정수 · 범위 · 명령 이름 거절 · 받은 값은 다음 <기본>", () => { start(polygonLike); const rejects: Array<[string, string]> = [ ["3.2*1.5", "숫자 값을 요구함."], ["2m", "숫자 값을 요구함."], ["l", "숫자 값을 요구함."], ["2.5", "정수 값을 요구함."], ["2", "3에서 1024 사이의 정수를 요구함."], ]; for (const [text, message] of rejects) { type(text); expect(noticeLine()).toBe(message); expect(toolState()).toBe("STEP_0"); expect(inputText()).toBe(""); } type("6"); type("0,0"); type("c"); // 소문자 키워드 type("1,0"); enter(); expect(commits).toEqual([[6, 0, 0, "CIRCUMSCRIBED", 2]]); start(polygonLike); expect(promptLine()).toBe("변의 수 입력 <6>:"); // 세션 기억 enter(); expect(promptLine()).toBe("중심 지정 <0.0000, 0.0000>:"); // 누른 점도 기억 }); it("점 물음 — 명령 이름 · 엉뚱한 글 거절(도구 그대로) · 키워드 물음 거절 글", () => { start(polygonLike); enter(); type("circle"); expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); expect(activeTool()).toBe(Tool.POLYGON); type("0,0"); type("x"); expect(noticeLine()).toBe("옵션 키워드가 잘못됨."); expect(toolState()).toBe("STEP_2"); }); it("Esc 한 번 = 명령 끝(확정 없음) · 기록 줄", () => { start(polygonLike); enter(); type("0,0"); key("Escape"); expect(activeTool()).toBe(Tool.SELECT); expect(commits).toEqual([]); }); }); describe("엔진 v2 — loopFrom · when · mode · next · escCommits · undo · close · pickfirst(2-13)", () => { it("loopFrom = 앞 값 둠 · mode · when · next · 되풀이 첫 물음 Enter = 끝", () => { const seen: unknown[] = []; const tool = createSequenceTool({ tool: Tool.DIST, loopFrom: "at", steps: [ { kind: "number", id: "d", prompt: { text: "거리 지정", default: { kind: "value", remember: "T.D", initial: 1 } }, }, { kind: "point", id: "at", prompt: { text: "점 지정", options: [opt("MODE", "M", "모드")] }, options: { MODE: { mode: "m", next: "extra" } }, }, { kind: "number", id: "extra", when: (input) => input.mode === "m" && !input.has("extra"), next: "at", prompt: { text: "덧 값" }, }, ], commit: (input) => seen.push([ input.get("d"), input.get("at").x, input.has("extra") ? input.get("extra") : null, input.mode, ]), }); start(tool); enter(); // d = 1 type("1,1"); expect(seen).toEqual([[1, 1, null, null]]); expect(promptLine()).toBe("점 지정 또는 [모드(M)]:"); // 거리는 둠 · 점부터 다시 type("m"); expect(promptLine()).toBe("덧 값:"); type("7"); type("2,2"); expect(seen[1]).toEqual([1, 2, 7, "m"]); enter(); // 한 번 돈 뒤 첫 되풀이 물음 Enter = 끝 expect(activeTool()).toBe(Tool.SELECT); }); const polys: Array<{ points: number; closed: boolean }> = []; const plineLike = createSequenceTool({ tool: Tool.DIST, repeatLastStep: true, escCommits: true, steps: [ { kind: "point", prompt: { text: "시작점 지정" } }, { kind: "point", prompt: (input) => ({ text: "다음 점 지정", options: input.points().length >= 3 ? [OPT.CLOSE, OPT.UNDO] : [OPT.UNDO], }), options: { CLOSE: "close", UNDO: "undo" }, }, ], commit: (input) => polys.push({ points: input.points().length, closed: input.closed }), }); it("undo · close(셋째 점부터) · escCommits · 점 하나 Esc = 버림", () => { polys.length = 0; start(plineLike); type("0,0"); type("10,0"); type("c"); // 아직 닫기 없음 expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); type("10,10"); expect(promptLine()).toBe("다음 점 지정 또는 [닫기(C)/명령 취소(U)]:"); type("u"); type("20,0"); key("Escape"); // 모은 값으로 확정 뒤 끝 expect(polys).toEqual([{ points: 3, closed: false }]); expect(activeTool()).toBe(Tool.SELECT); start(plineLike); type("0,0"); type("10,0"); type("10,10"); type("c"); expect(polys[1]).toEqual({ points: 3, closed: true }); start(plineLike); type("0,0"); key("Escape"); expect(polys).toHaveLength(2); }); it("고르기 첫 단계 — 고른 것 없이 Enter = 끝 · 고른 채 시작 = 바로 다음 물음(PICKFIRST) · 이전(P) 기억", () => { const picked: number[] = []; const tool = createSequenceTool({ tool: Tool.DIST, steps: [{ kind: "selection" }, { kind: "point", prompt: { text: "기준점 지정" } }], commit: (input) => picked.push(input.entities(0).length), }); start(tool); expect(promptLine()).toBe("객체 선택:"); enter(); expect(activeTool()).toBe(Tool.SELECT); expect(picked).toEqual([]); type("l"); type("0,0"); type("10,0"); key("Escape"); const ids = lineIds(); setSelectedEntityIds(ids); start(tool); expect(promptLine()).toBe("기준점 지정:"); expect(previousSelection()).toEqual(ids); type("5,5"); expect(picked).toEqual([1]); }); it("고르기 명령 Esc = 끝 + 고름 풂", () => { const tool = createSequenceTool({ tool: Tool.DIST, steps: [{ kind: "selection" }, { kind: "point", prompt: { text: "기준점 지정" } }], commit: () => {}, }); type("l"); type("0,0"); type("10,0"); key("Escape"); setSelectedEntityIds(lineIds()); start(tool); key("Escape"); expect(activeTool()).toBe(Tool.SELECT); expect(getSelectedEntityIds()).toEqual([]); }); }); describe("쉼 · 명령 이름 · 별칭(2-5 ⑨ · 2-17 · N18)", () => { it("앱 시작 = 쉼(선택 도구) — main.tsx 가 켜는 도구 · 「l ⏎」 = 첫 번째 점 지정", () => { const main = readFileSync( new URL("../../../B07_DesignDetail/openwebcad/src/main.tsx", import.meta.url), "utf8", ); const started = [...main.matchAll(/TOOL_STATE_MACHINES\[Tool\.(\w+)\]/g)].map((m) => m[1]); expect(started).toEqual(["SELECT"]); start(TOOL_STATE_MACHINES[Tool.SELECT]); expect(activeTool()).toBe(Tool.SELECT); const idle = promptLine(); expect(idle).not.toBe("첫 번째 점 지정:"); typeEnter("l"); expect(activeTool()).toBe(Tool.LINE); expect(promptLine()).toBe("첫 번째 점 지정:"); key("Escape"); expect(promptLine()).toBe(idle); }); it("딱 맞음만 — R · LE · CIR · SE = 알 수 없는 명령 · l = LINE · ed = TEXTEDIT", () => { for (const text of ["r", "le", "cir", "se"]) { type(text); expect(noticeLine()).toBe(`알 수 없는 명령 「${text.toUpperCase()}」.`); expect(activeTool()).toBe(Tool.SELECT); } type("ed"); expect(activeTool()).toBe(Tool.TEXTEDIT); key("Escape"); type("l"); expect(activeTool()).toBe(Tool.LINE); }); it("빈 Enter · Space · 우클릭(쉴 때) = 앞 명령 반복", () => { type("l"); key("Escape"); expect(activeTool()).toBe(Tool.SELECT); rightClick(); expect(activeTool()).toBe(Tool.LINE); key("Escape"); key(" "); expect(activeTool()).toBe(Tool.LINE); }); it("Esc 한 번 = 선 끝(그린 토막 남음) · 쉴 때 Esc = 고름 풂", () => { type("l"); type("0,0"); type("10,0"); typed("12"); key("Escape"); expect(activeTool()).toBe(Tool.SELECT); expect(inputText()).toBe(""); expect(entities("Line")).toHaveLength(1); setSelectedEntityIds(lineIds()); key("Escape"); expect(getSelectedEntityIds()).toEqual([]); }); it("명령행 소문자 옵션(N6) — 선 c = 닫기", () => { type("l"); type("0,0"); type("10,0"); type("10,10"); driver().submitText("c"); expect(entities("Line")).toHaveLength(3); }); }); describe("명령 바꿈 · 되돌림 · 덧물음 · 투명(2-8 · 2-11 · N1 · N11)", () => { const drawSelected = () => { type("l"); type("0,0"); type("20,0"); key("Escape"); setSelectedEntityIds(lineIds()); }; it("N1 — 이동 기준점 뒤 딴 명령 = 끊고(원본 되살림) 켬 · 선 1 개", () => { drawSelected(); type("move"); type("0,0"); runCommandInput("CIRCLE"); expect(activeTool()).toBe(Tool.CIRCLE); expect(entities("Line")).toHaveLength(1); }); it("N1 — 이동 도중 Ctrl+Z = 끊고 뒤 되돌림 · 같은 id 두 벌 없음", () => { drawSelected(); type("move"); type("0,0"); key("z", { ctrlKey: true, code: "KeyZ" }); const ids = entities().map((entity) => entity.id); expect(new Set(ids).size).toBe(ids.length); expect(activeTool()).toBe(Tool.SELECT); }); it("쉴 때 U = UNDO · 명령 중 U 는 옵션일 때만", () => { type("l"); type("0,0"); type("10,0"); key("Escape"); expect(entities("Line")).toHaveLength(1); type("u"); expect(entities("Line")).toHaveLength(0); start(polygonLike); type("u"); expect(noticeLine()).toBe("숫자 값을 요구함."); }); it("덧물음 — 친 점 · 누른 점이 덧물음 먼저 · {point} = 원래 물음에", () => { const seen: Xy[] = []; type("l"); pushSubPrompt({ prompt: { text: "덧 점 지정" }, kind: "point", onPoint: (point) => { seen.push({ x: point.x, y: point.y }); return seen.length === 1 ? "stay" : { point }; }, }); expect(promptLine()).toBe("덧 점 지정:"); type("3,4"); expect(promptLine()).toBe("덧 점 지정:"); // stay click(30, 40); expect(seen).toEqual([ { x: 3, y: 4 }, { x: 30, y: 40 }, ]); expect(toolState()).toBe("WAITING_FOR_END_POINT"); // (30, 40) 이 선 첫 점으로 type("@10,0"); const [line] = entities("Line") as unknown as Array<{ segment: { start: Xy; end: Xy } }>; expect(line.segment.start.x).toBeCloseTo(30, 3); expect(line.segment.end.x).toBeCloseTo(40, 3); }); it("투명 `'` — 도구형은 거절 · 즉시형은 도구 둔 채", () => { type("l"); type("'line"); expect(noticeLine()).toBe("「LINE」 명령은 투명하게 쓸 수 없음."); expect(activeTool()).toBe(Tool.LINE); routeTyped("'zoomin"); routeTyped("'zoomout"); expect(getLastCommandId()).toBe("ZOOMOUT"); expect(activeTool()).toBe(Tool.LINE); }); }); describe("Ctrl · Alt 글자(H1)", () => { it("Ctrl+C = COPYCLIP · Ctrl+글자 · Alt+글자는 쌓지 않음", () => { key("c", { ctrlKey: true, code: "KeyC" }); expect(getLastCommandId()).toBe("COPYCLIP"); expect(activeTool()).toBe(Tool.SELECT); // 원(C) 아님 key("ㅌ", { ctrlKey: true, code: "KeyX" }); expect(getLastCommandId()).toBe("CUTCLIP"); key("l", { altKey: true }); key("q", { ctrlKey: true, code: "KeyQ" }); expect(inputText()).toBe(""); typeEnter("l"); expect(activeTool()).toBe(Tool.LINE); }); }); // ---- 가-2 — ↑↓ · Tab · FROM · M2P · @ · # · 스냅 이름 · H11 ---- const segs = () => (entities("Line") as unknown as Array<{ segment: { start: Xy; end: Xy } }>).map( (line) => line.segment, ); const expectSeg = (seg: { start: Xy; end: Xy }, x1: number, y1: number, x2: number, y2: number) => { expect( [seg.start.x, seg.start.y, seg.end.x, seg.end.y].map((v) => Math.round(v * 1000) / 1000), ).toEqual([x1, y1, x2, y2]); }; describe("가-2 ↑↓ 기록 · 최근 값 · 후보(2-12 · H11)", () => { it("쉴 때 ↑↓ = 친 명령만(알 수 없는 글 · 알림 줄 안 섞임) · 끝을 지나면 빈 글", () => { type("l"); key("Escape"); type("zz"); // 알 수 없는 명령 — 기록 안 됨 type("circle"); key("Escape"); key("ArrowUp"); expect(inputText()).toBe("CIRCLE"); key("ArrowUp"); expect(inputText()).toBe("L"); key("ArrowDown"); expect(inputText()).toBe("CIRCLE"); key("ArrowDown"); expect(inputText()).toBe(""); }); it("명령 중 ↑ = 그 물음 최근 값", () => { start(polygonLike); type("7"); key("Escape"); start(polygonLike); key("ArrowUp"); expect(inputText()).toBe("7"); key("Escape"); }); it("Tab · ↑↓ = 후보 돌림(처음 친 글 기준) · Shift+↑ = 화면 옮김", () => { typed("ci"); key("Tab"); expect(inputText()).toBe(commandCandidate("CI", 0)); key("Tab"); expect(inputText()).toBe(commandCandidate("CI", 1)); key("ArrowUp"); expect(inputText()).toBe(commandCandidate("CI", 0)); key("Escape"); const before = getScreenCanvasDrawController().getScreenOffset().y; key("ArrowUp", { shiftKey: true }); expect(getScreenCanvasDrawController().getScreenOffset().y).not.toBe(before); key("ArrowDown", { shiftKey: true }); }); }); describe("가-2 점 물음 보조(1-4)", () => { it("FROM — 기준점 · <간격띄우기> @dx,dy → 그 점이 원래 물음에", () => { type("l"); type("from"); expect(promptLine()).toBe("기준점:"); type("10,10"); expect(promptLine()).toBe("<간격띄우기>:"); type("@5,0"); type("@10,0"); expectSeg(segs()[0], 15, 10, 25, 10); }); it("M2P — 두 점의 가운데", () => { type("l"); type("m2p"); expect(promptLine()).toBe("중간점의 첫 번째 점:"); type("0,0"); expect(promptLine()).toBe("중간점의 두 번째 점:"); type("10,0"); type("@0,5"); expectSeg(segs()[0], 5, 0, 5, 5); }); it("@ = 마지막 점 · 기준점 없는 첫 점 @dx,dy = 마지막 점 기준 · # = 절대", () => { type("l"); type("0,0"); type("10,0"); key("Escape"); type("l"); type("@"); type("@0,10"); expectSeg(segs()[1], 10, 0, 10, 10); key("Escape"); type("l"); type("@1,1"); // 마지막 점 (10, 10) 기준 type("#0,0"); expectSeg(segs()[2], 11, 11, 0, 0); }); it("스냅 이름 — 세 글자~영문 전체 · QUA · NOD · 없는 스냅 안내 · 두 글자는 아님", () => { type("l"); for (const [text, snap] of [ ["endp", "END"], ["endpoint", "END"], ["qua", "QUA"], ["node", "NOD"], ["_non", "NON"], ]) { type(text); expect(getSnapOverride()).toBe(snap); } type("tangent"); expect(noticeLine()).toBe("「TAN」 스냅은 이 CAD 에 없음."); type("ext"); expect(noticeLine()).toBe("「EXT」 스냅은 이 CAD 에 없음."); expect(activeTool()).toBe(Tool.LINE); // EXT · INS 가 명령으로 안 돎(N14) key("Escape"); expect(getSnapOverride()).toBeNull(); }); it("옵션 영문 전체 = 옵션 · 줄임 CEN = 스냅", () => { const seen: string[] = []; const tool = createSequenceTool({ tool: Tool.DIST, steps: [ { kind: "point", prompt: { text: "호의 두 번째 점 지정", options: [opt("CENTER", "C", "중심")] }, options: { CENTER: { run: () => seen.push("CENTER") } }, }, ], commit: () => {}, }); start(tool); type("cen"); expect(getSnapOverride()).toBe("CEN"); expect(seen).toEqual([]); type("center"); expect(seen).toEqual(["CENTER"]); key("Escape"); }); });