schemeta/tests/state-store.test.js
2026-02-19 22:32:13 -05:00

166 lines
5.6 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import fixture from "../frontend/sample.schemeta.json" with { type: "json" };
import { createSchemetaStore } from "../frontend-react/src/state/store.js";
function baseModel() {
return {
...fixture,
instances: fixture.instances.map((instance) => ({
...instance,
placement: {
...instance.placement
}
}))
};
}
test("setSelection normalizes and sorts refs deterministically", () => {
const store = createSchemetaStore();
store.actions.setSelection({
selectedRefs: ["U7", "U1", "U2", "U1", "U3"],
selectedNet: "I2C_SCL",
selectedPin: { ref: "U1", pin: "GPIO9" }
});
const { selection } = store.getState();
assert.deepEqual(selection.selectedRefs, ["U1", "U2", "U3", "U7"]);
assert.equal(selection.selectedNet, "I2C_SCL");
assert.deepEqual(selection.selectedPin, { ref: "U1", pin: "GPIO9" });
});
test("moveComponent only updates targeted instance and is deterministic", () => {
const model = baseModel();
const storeA = createSchemetaStore();
const storeB = createSchemetaStore();
storeA.actions.setModel(model);
storeA.actions.moveComponent("U1", { x: 121.7, y: 333.2 });
storeB.actions.setModel(model);
storeB.actions.moveComponent("U1", { x: 121.7, y: 333.2 });
const stateA = storeA.getState();
const stateB = storeB.getState();
assert.deepEqual(stateA.model, stateB.model);
const moved = stateA.model.instances.find((x) => x.ref === "U1");
assert.equal(moved.placement.x, 122);
assert.equal(moved.placement.y, 333);
const untouched = stateA.model.instances.find((x) => x.ref === "U2");
assert.equal(untouched.placement.x, null);
assert.equal(untouched.placement.y, null);
});
test("applyJsonText updates model on valid JSON and clears jsonError", () => {
const store = createSchemetaStore({
uiFlags: { jsonError: "Old error" }
});
const payload = JSON.stringify(baseModel());
const result = store.actions.applyJsonText(payload);
assert.equal(result.ok, true);
assert.equal(store.getState().uiFlags.jsonError, null);
assert.equal(store.getState().model.instances.length > 0, true);
});
test("applyJsonText reports parse errors without mutating model", () => {
const store = createSchemetaStore();
store.actions.setModel(baseModel());
const before = store.getState().model;
const result = store.actions.applyJsonText("{ bad json }");
assert.equal(result.ok, false);
assert.equal(typeof result.error, "string");
assert.equal(store.getState().uiFlags.jsonError.length > 0, true);
assert.deepEqual(store.getState().model, before);
});
test("undo/redo restores deterministic snapshots", () => {
const store = createSchemetaStore();
store.actions.setModel(baseModel());
store.actions.setSelection({ selectedRefs: ["U3", "U1"] });
const beforeMove = store.getState().model;
store.actions.moveComponent("U1", { x: 80, y: 60 });
assert.equal(store.getState().history.past.length >= 3, true);
store.actions.undo();
assert.deepEqual(store.getState().model, beforeMove);
store.actions.redo();
const moved = store.getState().model.instances.find((x) => x.ref === "U1");
assert.equal(moved.placement.x, 80);
assert.equal(moved.placement.y, 60);
});
test("transaction scaffold groups multiple actions into one undo step", () => {
const store = createSchemetaStore();
store.actions.setModel(baseModel());
const pastBefore = store.getState().history.past.length;
store.actions.beginTransaction("drag-and-select");
store.actions.moveComponent("U1", { x: 240, y: 140 });
store.actions.setSelection({ selectedRefs: ["U2", "U1"] });
store.actions.commitTransaction();
assert.equal(store.getState().history.past.length, pastBefore + 1);
assert.deepEqual(store.getState().selection.selectedRefs, ["U1", "U2"]);
store.actions.undo();
assert.deepEqual(store.getState().selection.selectedRefs, []);
const u1 = store.getState().model.instances.find((x) => x.ref === "U1");
assert.equal(u1.placement.x, null);
assert.equal(u1.placement.y, null);
});
test("compile lifecycle state transitions are deterministic", () => {
const storeA = createSchemetaStore();
const storeB = createSchemetaStore();
storeA.actions.beginCompile("Compile");
storeA.actions.failCompile("Compile", "Bad payload");
storeA.actions.beginCompile("Analyze");
storeA.actions.completeCompile("Analyze");
storeB.actions.beginCompile("Compile");
storeB.actions.failCompile("Compile", "Bad payload");
storeB.actions.beginCompile("Analyze");
storeB.actions.completeCompile("Analyze");
assert.deepEqual(storeA.getState().lifecycle, storeB.getState().lifecycle);
assert.equal(storeA.getState().lifecycle.isCompiling, false);
assert.equal(storeA.getState().lifecycle.lastError, null);
assert.equal(storeA.getState().lifecycle.lastAction, "Analyze");
});
test("compile lifecycle updates do not alter undo/redo history", () => {
const store = createSchemetaStore();
store.actions.setModel(baseModel());
store.actions.moveComponent("U1", { x: 90, y: 70 });
const beforeUndo = store.getState();
const pastBeforeLifecycle = beforeUndo.history.past.length;
store.actions.beginCompile("Compile");
store.actions.failCompile("Compile", "Synthetic error");
store.actions.beginCompile("Compile");
store.actions.completeCompile("Compile");
assert.equal(store.getState().history.past.length, pastBeforeLifecycle);
assert.equal(store.getState().lifecycle.lastAction, "Compile");
assert.equal(store.getState().lifecycle.lastError, null);
store.actions.undo();
const u1 = store.getState().model.instances.find((x) => x.ref === "U1");
assert.equal(u1.placement.x, null);
assert.equal(u1.placement.y, null);
});