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); }); });