Files
Beyond-KLE/mock/tests/domain.test.ts
T
ewdernaandClaude Opus 5.5 0838487f4a mock: 変化のないスナップショットをUndo履歴に積まない
数値欄の change がフォーカスを外したときにもう一度届くと、同じ値の変更が履歴に2回積まれ、
Undo 1回で戻らなかった。グループ編集の commit と同じく、直前と同じなら積まない。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yoUu9Xd8dhwyQdCfKfaXG
2026-10-23 22:18:50 +09:00

276 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { createProjectFromKle } from "../src/domain/project";
import { saveProject, loadProject } from "../src/io/projectFile";
import { validateSchema } from "../src/io/validateSchema";
import { appendKeys, unregisteredKeys } from "../src/domain/idx";
import { assignKey, isEditableKeycode, parseKeycode, renameLayer, deleteLayer } from "../src/domain/map";
import { getKeycodeLabel } from "../src/domain/keycodeLabels";
import { qmkBasicKeycodes } from "../src/domain/qmkBasicKeycodes";
import { generateMockTarget } from "../src/domain/generator";
import { deleteNodeCascade } from "../src/domain/deleteCascade";
import { ProjectHistory } from "../src/domain/history";
import { validateProject } from "../src/domain/validate";
import { mcuPinCandidates } from "../src/domain/elc";
const kle = JSON.parse(
readFileSync(new URL("../../samples/kle/macropad-4x4.json", import.meta.url), "utf8"),
) as unknown;
const gen = (() => {
let i = 0;
return () => `${String(++i).padStart(8, "0")}-0000-4000-8000-000000000000`;
})();
describe("domain project flow", () => {
it("supports QMK basic keycode names and aliases while preserving entered spelling", async () => {
const editable = [
"KC_ENT",
"KC_ENTER",
"KC_ESCAPE",
"KC_ESC",
"KC_P0",
"KC_KP_0",
"KC_LCTL",
"KC_LEFT_CTRL",
"KC_INT1",
"KC_LNG1",
];
for (const code of editable) expect(isEditableKeycode(code), code).toBe(true);
const formalNames = qmkBasicKeycodes.filter((entry) => entry.group !== "internal");
expect(formalNames.length).toBeGreaterThanOrEqual(100);
const p = await createProjectFromKle(kle, { idGenerator: gen });
const keyId = p.idx.order[0]!;
for (const code of editable) {
p.map = assignKey(p.map, "Base", keyId, code);
expect(p.map.layers[0]!.keymap[0]!.keycode).toBe(code);
}
p.map = assignKey(p.map, "Base", keyId, "LCTL(KC_ENT)");
expect(isEditableKeycode("LCTL(KC_ENT)")).toBe(true);
p.map.layers.push({ name: "Nav", keymap: [] });
p.map = assignKey(p.map, "Base", keyId, "LT(Nav, KC_LNG1)");
expect(isEditableKeycode("LT(Nav, KC_LNG1)")).toBe(true);
p.map = assignKey(p.map, "Base", keyId, "KC_P0");
const roundTrip = await loadProject((await saveProject(p)).json!);
expect(roundTrip.project!.map.layers[0]!.keymap[0]!.keycode).toBe("KC_P0");
for (const code of ["KC_NO", "XXXXXXX", "KC_TRANSPARENT", "KC_TRNS", "_______"]) {
expect(isEditableKeycode(code), code).toBe(true);
p.map = assignKey(p.map, "Base", keyId, code);
expect(p.map.layers[0]!.keymap[0]!.keycode).toBe(code);
}
for (const code of ["KC_FOO", "QK_BOOT"]) {
expect(parseKeycode(code).kind).toBe("unknown");
expect(isEditableKeycode(code)).toBe(false);
expect(() => assignKey(p.map, "Base", keyId, code)).toThrow();
}
p.map.layers[0]!.keymap[0]!.keycode = "KC_FOO";
expect(p.map.layers[0]!.keymap[0]!.keycode).toBe("KC_FOO");
expect(getKeycodeLabel("KC_A")).toBe("A");
expect(getKeycodeLabel("KC_ENTER")).toBe("Enter");
expect(getKeycodeLabel("KC_ENT")).toBe("Enter");
expect(getKeycodeLabel("KC_ESCAPE")).toBe("Escape");
expect(getKeycodeLabel("KC_ESC")).toBe("Escape");
});
it("imports KLE, saves, reloads and validates schemas", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
const saved = await saveProject(p);
expect(saved.errors.some((issue) => issue.severity === "blocksSave")).toBe(false);
const loaded = await loadProject(saved.json!);
expect(loaded.errors.filter((x) => x.severity === "blocksSave")).toEqual([]);
expect(JSON.stringify(loaded.project)).toBe(JSON.stringify(p));
for (const l of ["phy", "idx", "elc", "map"] as const) expect(validateSchema(l, p[l])).toEqual([]);
});
it("preserves unknown data and IDX order", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
p.extra = { x: 1 };
(p.phy as typeof p.phy & { extraPhy: boolean }).extraPhy = true;
p.idx.extraIdx = true;
p.elc.extraElc = true;
p.map.extraMap = true;
const q = (await loadProject((await saveProject(p)).json!)).project!;
expect(q.extra).toEqual({ x: 1 });
expect((q.phy as typeof q.phy & { extraPhy: boolean }).extraPhy).toBe(true);
expect(q.idx.extraIdx).toBe(true);
expect(q.elc.extraElc).toBe(true);
expect(q.map.extraMap).toBe(true);
expect(unregisteredKeys(q.phy, appendKeys(q.idx, []))).toEqual([]);
});
it("supports MAP layer reference changes, duplicate checks and safe parsing", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
p.map.layers.push({ name: "Nav", keymap: [] });
p.map = assignKey(p.map, "Base", p.idx.order[0]!, "MO(Nav)");
p.map = assignKey(p.map, "Base", p.idx.order[1]!, "LT(Nav, KC_SPC)");
p.map = renameLayer(p.map, "Nav", "Fn");
expect(p.map.layers[0]!.keymap.map((x) => x.keycode)).toEqual(["MO(Fn)", "LT(Fn, KC_SPC)"]);
expect(parseKeycode("MO(Fn)").layer).toBe("Fn");
expect(() => deleteLayer(p.map, "Fn")).toThrow();
p.map.layers[0]!.keymap.push({ ...p.map.layers[0]!.keymap[0]! });
expect(validateProject(p).some((x) => x.code === "duplicate-map-key")).toBe(true);
});
it("generates ordered matrix/keymap previews and cascades deletion", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
const ids = p.idx.order;
p.output.matrix = {
rowPins: ["m.GP0", "m.GP1", "m.GP2", "m.GP3"],
colPins: ["m.GP4", "m.GP5", "m.GP6", "m.GP7"],
};
p.elc.providers.push({ id: "m", kind: "mcu", part: "RP2040" });
p.elc.ledChains = [{ id: "chain", data: "m.GP8", order: [ids[0]!, ids[1]!] }];
p.map.layers[0]!.keymap = ids.map((keyId, i) => ({ keyId, keycode: `KC_${i % 10}` }));
p.elc.keys = ids.map((key, i) => ({
key,
detect: "contact",
terminals: { a: `m.GP${Math.floor(i / 4)}`, b: `m.GP${4 + (i % 4)}` },
diode: "b-to-a",
}));
const out = generateMockTarget(p);
expect(out.errors).toEqual([]);
expect(out.target?.idx).toEqual(ids);
expect(out.target?.matrix.rows.slice(0, 5)).toEqual([0, 0, 0, 0, 1]);
expect(out.target?.matrix.cols.slice(0, 5)).toEqual([0, 1, 2, 3, 0]);
expect(out.target?.keymaps[0]!.keycodes).toHaveLength(16);
const q = deleteNodeCascade(p, ids[0]!);
expect(q.idx.order).not.toContain(ids[0]);
expect(q.elc.keys.some((x) => x.key === ids[0])).toBe(false);
expect(q.elc.ledChains?.[0]?.order).not.toContain(ids[0]);
expect(q.map.layers[0]!.keymap.some((x) => x.keyId === ids[0])).toBe(false);
});
it("groups history edits", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen }),
h = new ProjectHistory(p);
const q = structuredClone(p);
q.phy.metadata.name = "drag";
h.begin();
h.push(q);
h.replaceTop(q);
h.commit(q);
expect(h.canUndo).toBe(true);
expect(h.undo().phy.metadata.name).toBe(p.phy.metadata.name);
expect(h.redo().phy.metadata.name).toBe("drag");
});
it("does not add a history step for an unchanged snapshot", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen }),
h = new ProjectHistory(p);
const q = structuredClone(p);
q.phy.metadata.name = "edit";
h.push(q);
h.push(structuredClone(q));
expect(h.undo().phy.metadata.name).toBe(p.phy.metadata.name);
expect(h.canUndo).toBe(false);
});
it("imports the VIA geometry sample", async () => {
const via = JSON.parse(
readFileSync(new URL("../../samples/via/cr0wn60.json", import.meta.url), "utf8"),
) as unknown;
const p = await createProjectFromKle(via, { idGenerator: gen });
expect(validateSchema("phy", p.phy)).toEqual([]);
});
it("detects T-25 collisions and permits reversed diode pairs and exclusive variants", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
const ids = p.idx.order.slice(0, 2);
p.phy.root = p.phy.root.filter((n) => n.type === "key" && ids.includes(n.id));
p.idx.order = ids;
p.elc.providers = [{ id: "m", kind: "mcu", part: "RP2040" }];
p.elc.keys = [
{ key: ids[0]!, detect: "contact", terminals: { a: "m.GP0", b: "m.GP1" } },
{ key: ids[1]!, detect: "contact", terminals: { a: "m.GP1", b: "m.GP0" } },
];
expect(validateProject(p).some((x) => x.code === "wiring-collision")).toBe(true);
p.elc.keys = [
{ key: ids[0]!, detect: "contact", terminals: { a: "m.GP0", b: "m.GP1" }, diode: "b-to-a" },
{ key: ids[1]!, detect: "contact", terminals: { a: "m.GP1", b: "m.GP0" }, diode: "b-to-a" },
];
expect(validateProject(p).some((x) => x.code === "wiring-collision")).toBe(false);
p.elc.keys = [
{ key: ids[0]!, detect: "contact", terminals: { a: "m.GP0", b: "m.GP1" } },
{ key: ids[1]!, detect: "contact", terminals: { a: "m.GP0", b: "m.GP1" } },
];
expect(validateProject(p).some((x) => x.code === "wiring-collision")).toBe(true);
p.elc.keys = [
{ key: ids[0]!, detect: "contact", terminals: { a: "m.GP0", b: "m.GP1" }, diode: "a-to-b" },
{ key: ids[1]!, detect: "contact", terminals: { a: "m.GP1", b: "m.GP0" } },
];
expect(validateProject(p).some((x) => x.code === "wiring-collision")).toBe(true);
const a = p.phy.root[0]!,
b = p.phy.root[1]!;
p.phy.root = [
{
type: "variantGroup",
id: "vg",
name: "Choice",
transform: { x: 0, y: 0, rotation: 0 },
default: "va",
children: [
{ type: "variant", id: "va", name: "A", children: [a] },
{ type: "variant", id: "vb", name: "B", children: [b] },
],
},
];
expect(validateProject(p).some((x) => x.code === "wiring-collision")).toBe(false);
expect(mcuPinCandidates(p.elc.providers[0] as { id: string; kind: "mcu"; part: string })).toHaveLength(
30,
);
});
it("filters variant output without rewriting stored IDX order", async () => {
const p = await createProjectFromKle(kle, { idGenerator: gen });
const ids = p.idx.order.slice(0, 2);
p.phy.root = [
{
type: "variantGroup",
id: "vgx",
name: "Choice",
transform: { x: 0, y: 0, rotation: 0 },
default: "vax",
children: [
{
type: "variant",
id: "vax",
name: "A",
children: [
{
type: "key",
id: ids[0]!,
profile: "default",
transform: { x: 0, y: 0, rotation: 0 },
size: { w: 1, h: 1 },
},
],
},
{
type: "variant",
id: "vbx",
name: "B",
children: [
{
type: "key",
id: ids[1]!,
profile: "default",
transform: { x: 1, y: 0, rotation: 0 },
size: { w: 1, h: 1 },
},
],
},
],
},
];
p.output.variantSelection = { vgx: "vax" };
p.idx.order = ids;
p.elc.providers = [{ id: "m", kind: "mcu", part: "RP2040" }];
p.output.matrix = { rowPins: ["m.GP0"], colPins: ["m.GP1"] };
p.elc.keys = ids.map((key) => ({
key,
detect: "contact",
terminals: { a: "m.GP0", b: "m.GP1" },
diode: "b-to-a",
}));
p.map.layers[0]!.keymap = ids.map((keyId) => ({ keyId, keycode: "KC_A" }));
let g = generateMockTarget(p);
expect(g.target?.idx).toEqual([ids[0]]);
p.output.variantSelection = { vgx: "vbx" };
g = generateMockTarget(p);
expect(g.target?.idx).toEqual([ids[1]]);
expect(p.idx.order).toEqual(ids);
});
});