tests/app.test.js
- import { describe, it, expect, vi, beforeEach } from "vitest";
- import { MOCKS, STUBS } from "./api.js";
- const { fakeEl } = STUBS;
- await Promise.all([
- MOCKS.composer,
- MOCKS.parameters,
- MOCKS.solving,
- MOCKS.theory,
- MOCKS.step1Parts,
- MOCKS.step2Chords,
- MOCKS.step3Notes,
- MOCKS.controls,
- MOCKS.download,
- MOCKS.feedback,
- MOCKS.plotting,
- ]);
- describe("app", () => {
- let state;
- let captured;
- beforeEach(async () => {
- vi.resetModules();
- vi.clearAllMocks();
- captured = {};
- const docHandlers = {};
- STUBS.documentApp(captured, docHandlers);
- STUBS.windowApp(captured);
- STUBS.alpine();
- STUBS.alert();
- STUBS.silenceConsole("error", "warn");
- await import("../src/app.js");
- docHandlers["alpine:init"]();
- state = Alpine.data.mock.calls[0][1]();
- state.$watch = vi.fn((_, cb) => cb());
- state.$nextTick = vi.fn((fn) => {
- fn?.();
- return Promise.resolve();
- });
- state.$refs = { feedbackDialog: STUBS.dialog() };
- });
-
- it("init wires reactivity, autoscroll, import-params; surfaces solver init failures via log", async () => {
- await state.init();
- expect(state.isMiniZincReady).toBe(true);
- expect(state.statusText).toContain("Ready");
- captured.mq({ matches: false });
- expect(state.isSystemDark).toBe(false);
- captured.start();
- document.getElementById.mockImplementationOnce(() => null);
- captured.start();
- document.getElementById.mockImplementationOnce(() =>
- fakeEl({
- querySelector: () => null,
- shadowRoot: { querySelector: () => null },
- }),
- );
- captured.start();
- state.parameters = {};
- await captured.change({ target: { files: [{ name: "x.json" }] } });
- expect(state.importedFileName).toBe("x.json");
- await captured.change({ target: { files: [] } });
- const { importParameters } =
- await import("../src/controller/parameters.js");
- importParameters.mockRejectedValueOnce(new Error("bad"));
- await captured.change({ target: { files: [{ name: "y.json" }] } });
- expect(state.statusText).toContain("bad");
- state.isMiniZincReady = false;
- const { initSolvers } = await import("../src/controller/solving.js");
- initSolvers.mockRejectedValueOnce(new Error("WASM"));
- await state.init();
- expect(state.statusText).toContain("WASM");
- expect(state.isMiniZincReady).toBe(false);
- });
-
- it("log mirrors msg into status + console; with err prefixes 'Error:' and stringifies via .message, JSON, or template fallback", () => {
- state.log("hello");
- expect(state.statusText).toBe("hello");
- expect(state.consoleLog[0]).toMatch(/\[\d{2}:\d{2}:\d{2}\] hello/);
- state.log("oops", new Error("boom"));
- expect(state.statusText).toContain("Error: oops: boom");
- state.log("plain", "rawerror");
- expect(state.statusText).toContain("rawerror");
- state.log("obj", { code: 42 });
- expect(state.statusText).toContain("42");
- });
-
- it("runSolver bails when not ready, drives compose hooks, status (Done / Partial / No-solution), catches errors, missing window.Tone tolerated", async () => {
- state.isMiniZincReady = false;
- await state.runSolver();
- expect(state.statusText).toBe("Initializing...");
- await state.init();
- const { compose } = await import("../src/controller/composer.js");
- compose.mockResolvedValueOnce({
- parts: [{ label: "A" }],
- labelCache: {},
- midi: { combinedB64: "M", voicesB64: ["v"] },
- failedParts: [],
- solvedParts: 1,
- });
- await state.runSolver();
- expect(state.musicB64Midi).toBe("M");
- expect(state.statusText).toContain("Done: 1/1");
- const hooks = compose.mock.calls.at(-1)[2];
- hooks.log("hooked");
- hooks.onProgressionLine({ label: "A", chords: ["I"] });
- expect(state.chordProgressionText).toEqual([{ label: "A", chords: ["I"] }]);
- compose.mockResolvedValueOnce(null);
- await state.runSolver();
- compose.mockResolvedValueOnce({
- parts: [{}, {}],
- labelCache: {},
- midi: null,
- failedParts: ["B"],
- solvedParts: 1,
- });
- await state.runSolver();
- expect(state.statusText).toContain("Partial: 1/2");
- expect(state.statusText).toContain("Overconstrained: B");
- compose.mockResolvedValueOnce({
- parts: [{}],
- labelCache: {},
- midi: null,
- failedParts: ["A"],
- solvedParts: 0,
- });
- await state.runSolver();
- expect(state.statusText).toContain("No solution");
- compose.mockRejectedValueOnce(new Error("crash"));
- await state.runSolver();
- expect(state.statusText).toContain("crash");
- window.Tone = undefined;
- compose.mockResolvedValueOnce({
- parts: [{}],
- labelCache: {},
- midi: null,
- failedParts: [],
- solvedParts: 1,
- });
- await state.runSolver();
- });
-
- it("solveStep records raw-output tabs (with/without stats, with/without partLabel); resetResults clears state; loadPreset variants; updateGraph renders from form or hook payload, bails on missing data, resets invalid per-part chords to tonic", async () => {
- await state.init();
- const sol = await state.solveStep(
- { name: "Step 2", index: 2, id: "step2" },
- "A",
- "var x;",
- );
- expect(sol).toEqual({ x: 1 });
- expect(state.mznModelText[1][0].label).toBe("Step 2: A");
- const { solveModel } = await import("../src/controller/solving.js");
- solveModel.mockResolvedValueOnce({
- solution: { output: { json: {} } },
- statistics: null,
- });
- await state.solveStep({ name: "Step 1", index: 1, id: "step1" }, "", "y");
- expect(state.mznModelText[0][0].label).toBe("Step 1");
- expect(state.mznStatisticsText[0][0].content).toBe("{}");
- state.musicB64Midi = "x";
- state.consoleLog = ["a", "b"];
- state.resetResults();
- expect(state.musicB64Midi).toBeNull();
- expect(state.consoleLog).toEqual(["b"]);
- await expect(state.loadPreset("manual")).resolves.toBeNull();
- await expect(state.loadPreset()).resolves.toBeNull();
- STUBS.fetchJson({ ok: true });
- await expect(state.loadPreset("bach")).resolves.toEqual({ ok: true });
- STUBS.fetchReject("nope");
- await expect(state.loadPreset("missing")).resolves.toBeNull();
- state.parameters = {
- step2: {
- part: [
- { key: 0, mode: "major", start_chord: "X", end_chord: "V" },
- { key: 7, mode: "minor", start_chord: "X", end_chord: "Y" },
- ],
- },
- };
- state.updateGraph({
- graph: { activeNodes: ["I"] },
- path: null,
- key: 0,
- mode: "major",
- });
- expect(state.parameters.step2.part[0].start_chord).toBe("I");
- expect(state.parameters.step2.part[0].end_chord).toBe("V");
- expect(state.parameters.step2.part[1].start_chord).toBe("i");
- expect(state.parameters.step2.part[1].end_chord).toBe("i");
- state.parameters = { step2: { part: [{ key: "5", mode: "major" }] } };
- expect(() => state.updateGraph()).not.toThrow();
- state.parameters = {};
- expect(() => state.updateGraph()).not.toThrow();
- document.getElementById.mockImplementation(() => null);
- state.updateGraph({
- graph: { activeNodes: [] },
- path: null,
- key: 0,
- mode: "major",
- });
- });
- });