Files
Aislo/resources/tester/cad/text-inplace-edit.test.ts
T

217 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 글자 더블클릭 = 그 자리 입력 칸(PLAN 293-1) — prompt 대신 글자 위 입력 칸 · Enter 적용 · Esc 취소 ·
* 칸 밖 누름 적용 · 되돌리기 한 번 · 여러 줄(MTEXT)은 Ctrl+Enter 적용 · 키가 CAD 로 새지 않음.
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad text-inplace-edit
*/
import { Point } from "@flatten-js/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../../B07_DesignDetail/openwebcad/src/theme.ts", () => ({
paintColor: (color: string) => color,
}));
vi.mock("../../../B07_DesignDetail/openwebcad/src/state.ts", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
getScreenCanvasDrawController: () => ({
getScreenScale: () => 2,
worldToTarget: (point: Point) => point,
// 시험 쪽 Point 로 — 앱과 시험이 flatten 을 따로 실어 instanceof 가 갈림
targetToWorld: (point: Point) => new Point(point.x, point.y),
measureTextScreen: () => 40,
zoomToFitScreen: () => {},
}),
}));
import { TextEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/TextEntity.ts";
import {
closeTextEditor,
editorKeyAction,
handleTextDoubleClick,
} from "../../../B07_DesignDetail/openwebcad/src/integration/text-inplace-edit";
import {
getEntities,
resetUndoBaseline,
setEntities,
undo,
} from "../../../B07_DesignDetail/openwebcad/src/state";
type Listener = (evt: Record<string, unknown>) => void;
class FakeElement {
public value = "";
public className = "";
public style: Record<string, string> = {};
public selected = false;
public removed = false;
public listeners: Record<string, Listener[]> = {};
constructor(public tagName: string) {}
setAttribute() {}
addEventListener(type: string, listener: Listener) {
(this.listeners[type] ??= []).push(listener);
}
fire(type: string, evt: Record<string, unknown> = {}) {
for (const listener of this.listeners[type] ?? []) listener(evt);
}
focus() {}
select() {
this.selected = true;
}
remove() {
this.removed = true;
this.fire("blur");
}
}
let created: FakeElement[] = [];
let documentListeners: Record<string, Listener[]> = {};
const prompt = vi.fn();
const canvas = {
getBoundingClientRect: () => ({ left: 10, top: 20, bottom: 520, right: 810 }),
} as unknown as HTMLCanvasElement;
beforeEach(() => {
created = [];
documentListeners = {};
prompt.mockReset();
vi.stubGlobal("window", { prompt, dispatchEvent: () => true });
vi.stubGlobal("requestAnimationFrame", () => 1);
vi.stubGlobal("cancelAnimationFrame", () => {});
vi.stubGlobal("document", {
createElement: (tag: string) => {
const element = new FakeElement(tag);
created.push(element);
return element;
},
body: { appendChild: () => {} },
addEventListener: (type: string, listener: Listener) =>
(documentListeners[type] ??= []).push(listener),
removeEventListener: (type: string, listener: Listener) =>
(documentListeners[type] = (documentListeners[type] ?? []).filter((l) => l !== listener)),
});
});
afterEach(() => {
closeTextEditor(false);
vi.unstubAllGlobals();
});
/** 화면 좌표 = 월드 좌표(가짜 제어기) · 캔버스 아래 520 기준으로 y 를 뒤집음 */
const dblclickAt = (x: number, y: number) =>
handleTextDoubleClick({ clientX: 10 + x, clientY: 520 - y } as MouseEvent, canvas);
const key = (editor: FakeElement, k: string, extra: Record<string, unknown> = {}) => {
const evt = {
key: k,
ctrlKey: false,
metaKey: false,
isComposing: false,
keyCode: 0,
stopped: false,
prevented: false,
stopPropagation() {
evt.stopped = true;
},
preventDefault() {
evt.prevented = true;
},
...extra,
};
editor.fire("keydown", evt);
return evt;
};
const labels = () => getEntities().map((entity) => (entity as TextEntity).getLabel());
const load = (...entities: TextEntity[]) => {
setEntities(entities, false);
resetUndoBaseline();
};
describe("글자 더블클릭 = 그 자리 입력 칸", () => {
it("더블클릭 → 글자 자리에 입력 칸 · 글 전체 골라짐 · prompt 안 부름 · 캔버스 글자 감춤", () => {
const text = new TextEntity("0", "배수관", new Point(100, 200), { fontSize: 5 });
load(text);
dblclickAt(101, 200);
expect(prompt).not.toHaveBeenCalled();
expect(created).toHaveLength(1);
const [editor] = created;
expect(editor.tagName).toBe("input");
expect(editor.value).toBe("배수관");
expect(editor.selected).toBe(true);
expect(editor.style.left).toBe("110px"); // 캔버스 왼쪽 10 + 글자 x 100
expect(editor.style.top).toBe("320px"); // 캔버스 아래 520 - 글자 y 200
expect(editor.style.font).toContain("10px"); // 글 크기 5 × 배율 2
expect(text.previewLabel).toBe("");
});
it("Enter = 적용 · 되돌리기 한 번에 옛 글 · 키가 CAD 로 새지 않음", () => {
load(new TextEntity("0", "배수관", new Point(100, 200), { fontSize: 5 }));
dblclickAt(100, 200);
const [editor] = created;
const typed = key(editor, "a");
expect(typed.stopped).toBe(true);
expect(typed.prevented).toBe(false);
editor.value = "횡단배수관";
const enter = key(editor, "Enter");
expect(enter.stopped).toBe(true);
expect(editor.removed).toBe(true);
expect(labels()).toEqual(["횡단배수관"]);
expect((getEntities()[0] as TextEntity).previewLabel).toBeNull();
undo();
expect(labels()).toEqual(["배수관"]);
});
it("Esc = 취소 · 글 그대로 · 감춘 글자 되살림", () => {
const text = new TextEntity("0", "배수관", new Point(100, 200), { fontSize: 5 });
load(text);
dblclickAt(100, 200);
const [editor] = created;
editor.value = "바꿈";
key(editor, "Escape");
expect(editor.removed).toBe(true);
expect(labels()).toEqual(["배수관"]);
expect(getEntities()[0]).toBe(text);
expect(text.previewLabel).toBeNull();
});
it("칸 밖 누름 = 적용 · 가운데 단추(팬)는 닫지 않음", () => {
load(new TextEntity("0", "배수관", new Point(100, 200), { fontSize: 5 }));
dblclickAt(100, 200);
const [editor] = created;
editor.value = "옹벽";
for (const listener of documentListeners.pointerdown) listener({ target: {}, button: 1 });
expect(editor.removed).toBe(false);
for (const listener of documentListeners.pointerdown) listener({ target: {}, button: 0 });
expect(editor.removed).toBe(true);
expect(labels()).toEqual(["옹벽"]);
});
it("한글 조합 중 Enter 는 적용하지 않음", () => {
expect(editorKeyAction({ key: "Enter", isComposing: true } as KeyboardEvent, false)).toBeNull();
expect(editorKeyAction({ key: "Enter", keyCode: 229 } as KeyboardEvent, false)).toBeNull();
});
it("여러 줄(MTEXT) = 여러 줄 칸 · Enter 줄바꿈 · Ctrl+Enter 적용 · 줄 늘면 아래로 · 되돌리기 한 번", () => {
const first = new TextEntity("0", "첫 줄", new Point(100, 200), { fontSize: 5 });
const second = new TextEntity("0", "둘째 줄", new Point(100, 193), { fontSize: 5 });
first.groupId = second.groupId = "m1";
load(second, first);
dblclickAt(100, 193);
const [editor] = created;
expect(editor.tagName).toBe("textarea");
expect(editor.value).toBe("첫 줄\n둘째 줄");
expect(key(editor, "Enter").prevented).toBe(false);
expect(editor.removed).toBe(false);
editor.value = "첫 줄\n둘째\n셋째";
key(editor, "Enter", { ctrlKey: true });
expect(editor.removed).toBe(true);
const lines = getEntities() as TextEntity[];
expect(lines.map((line) => line.getLabel())).toEqual(["둘째", "셋째", "첫 줄"]);
expect(lines.every((line) => line.groupId === "m1")).toBe(true);
expect(lines[1].getFirstPoint()?.y).toBeCloseTo(186);
undo();
expect(labels()).toEqual(["둘째 줄", "첫 줄"]);
});
it("수량표 글자는 칸을 띄우지 않음", () => {
load(new TextEntity("b08-cross-table", "12.5", new Point(100, 200), { fontSize: 5 }));
dblclickAt(100, 200);
expect(created).toHaveLength(0);
expect(prompt).not.toHaveBeenCalled();
});
});