Home Manual Reference Source Test

tests/app.test.js

  1. /**
  2. * app.js wires Alpine state to controller/composer + view layers.
  3. * All deps mocked; tests exercise every branch through the data-object methods.
  4. */
  5. import { describe, it, expect, vi, beforeEach } from "vitest";
  6. import { MOCKS, STUBS } from "./api.js";
  7. const { fakeEl } = STUBS;
  8. await Promise.all([
  9. MOCKS.composer,
  10. MOCKS.parameters,
  11. MOCKS.solving,
  12. MOCKS.theory,
  13. MOCKS.step1Parts,
  14. MOCKS.step2Chords,
  15. MOCKS.step3Notes,
  16. MOCKS.controls,
  17. MOCKS.download,
  18. MOCKS.feedback,
  19. MOCKS.plotting,
  20. ]);
  21. /** @test {app} */
  22. describe("app", () => {
  23. let state;
  24. let captured;
  25. beforeEach(async () => {
  26. vi.resetModules();
  27. vi.clearAllMocks();
  28. captured = {};
  29. const docHandlers = {};
  30. STUBS.documentApp(captured, docHandlers);
  31. STUBS.windowApp(captured);
  32. STUBS.alpine();
  33. STUBS.alert();
  34. STUBS.silenceConsole("error", "warn");
  35. await import("../src/app.js");
  36. docHandlers["alpine:init"]();
  37. state = Alpine.data.mock.calls[0][1]();
  38. state.$watch = vi.fn((_, cb) => cb());
  39. state.$nextTick = vi.fn((fn) => {
  40. fn?.();
  41. return Promise.resolve();
  42. });
  43. state.$refs = { feedbackDialog: STUBS.dialog() };
  44. });
  45. /** @test {app#init} */
  46. it("init wires reactivity, autoscroll, import-params; surfaces solver init failures via log", async () => {
  47. await state.init();
  48. expect(state.isMiniZincReady).toBe(true);
  49. expect(state.statusText).toContain("Ready");
  50. captured.mq({ matches: false });
  51. expect(state.isSystemDark).toBe(false);
  52. captured.start();
  53. document.getElementById.mockImplementationOnce(() => null);
  54. captured.start();
  55. document.getElementById.mockImplementationOnce(() =>
  56. fakeEl({
  57. querySelector: () => null,
  58. shadowRoot: { querySelector: () => null },
  59. }),
  60. );
  61. captured.start();
  62. state.parameters = {};
  63. await captured.change({ target: { files: [{ name: "x.json" }] } });
  64. expect(state.importedFileName).toBe("x.json");
  65. await captured.change({ target: { files: [] } });
  66. const { importParameters } =
  67. await import("../src/controller/parameters.js");
  68. importParameters.mockRejectedValueOnce(new Error("bad"));
  69. await captured.change({ target: { files: [{ name: "y.json" }] } });
  70. expect(state.statusText).toContain("bad");
  71. state.isMiniZincReady = false;
  72. const { initSolvers } = await import("../src/controller/solving.js");
  73. initSolvers.mockRejectedValueOnce(new Error("WASM"));
  74. await state.init();
  75. expect(state.statusText).toContain("WASM");
  76. expect(state.isMiniZincReady).toBe(false);
  77. });
  78. /** @test {app#log} */
  79. it("log mirrors msg into status + console; with err prefixes 'Error:' and stringifies via .message, JSON, or template fallback", () => {
  80. state.log("hello");
  81. expect(state.statusText).toBe("hello");
  82. expect(state.consoleLog[0]).toMatch(/\[\d{2}:\d{2}:\d{2}\] hello/);
  83. state.log("oops", new Error("boom"));
  84. expect(state.statusText).toContain("Error: oops: boom");
  85. state.log("plain", "rawerror");
  86. expect(state.statusText).toContain("rawerror");
  87. state.log("obj", { code: 42 });
  88. expect(state.statusText).toContain("42");
  89. });
  90. /** @test {app#runSolver} */
  91. it("runSolver bails when not ready, drives compose hooks, status (Done / Partial / No-solution), catches errors, missing window.Tone tolerated", async () => {
  92. state.isMiniZincReady = false;
  93. await state.runSolver();
  94. expect(state.statusText).toBe("Initializing...");
  95. await state.init();
  96. const { compose } = await import("../src/controller/composer.js");
  97. compose.mockResolvedValueOnce({
  98. parts: [{ label: "A" }],
  99. labelCache: {},
  100. midi: { combinedB64: "M", voicesB64: ["v"] },
  101. failedParts: [],
  102. solvedParts: 1,
  103. });
  104. await state.runSolver();
  105. expect(state.musicB64Midi).toBe("M");
  106. expect(state.statusText).toContain("Done: 1/1");
  107. const hooks = compose.mock.calls.at(-1)[2];
  108. hooks.log("hooked");
  109. hooks.onProgressionLine({ label: "A", chords: ["I"] });
  110. expect(state.chordProgressionText).toEqual([{ label: "A", chords: ["I"] }]);
  111. compose.mockResolvedValueOnce(null);
  112. await state.runSolver();
  113. compose.mockResolvedValueOnce({
  114. parts: [{}, {}],
  115. labelCache: {},
  116. midi: null,
  117. failedParts: ["B"],
  118. solvedParts: 1,
  119. });
  120. await state.runSolver();
  121. expect(state.statusText).toContain("Partial: 1/2");
  122. expect(state.statusText).toContain("Overconstrained: B");
  123. compose.mockResolvedValueOnce({
  124. parts: [{}],
  125. labelCache: {},
  126. midi: null,
  127. failedParts: ["A"],
  128. solvedParts: 0,
  129. });
  130. await state.runSolver();
  131. expect(state.statusText).toContain("No solution");
  132. compose.mockRejectedValueOnce(new Error("crash"));
  133. await state.runSolver();
  134. expect(state.statusText).toContain("crash");
  135. window.Tone = undefined;
  136. compose.mockResolvedValueOnce({
  137. parts: [{}],
  138. labelCache: {},
  139. midi: null,
  140. failedParts: [],
  141. solvedParts: 1,
  142. });
  143. await state.runSolver();
  144. });
  145. /** @test {app#solveStep} */
  146. 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 () => {
  147. await state.init();
  148. const sol = await state.solveStep(
  149. { name: "Step 2", index: 2, id: "step2" },
  150. "A",
  151. "var x;",
  152. );
  153. expect(sol).toEqual({ x: 1 });
  154. expect(state.mznModelText[1][0].label).toBe("Step 2: A");
  155. const { solveModel } = await import("../src/controller/solving.js");
  156. solveModel.mockResolvedValueOnce({
  157. solution: { output: { json: {} } },
  158. statistics: null,
  159. });
  160. await state.solveStep({ name: "Step 1", index: 1, id: "step1" }, "", "y");
  161. expect(state.mznModelText[0][0].label).toBe("Step 1");
  162. expect(state.mznStatisticsText[0][0].content).toBe("{}");
  163. state.musicB64Midi = "x";
  164. state.consoleLog = ["a", "b"];
  165. state.resetResults();
  166. expect(state.musicB64Midi).toBeNull();
  167. expect(state.consoleLog).toEqual(["b"]);
  168. await expect(state.loadPreset("manual")).resolves.toBeNull();
  169. await expect(state.loadPreset()).resolves.toBeNull();
  170. STUBS.fetchJson({ ok: true });
  171. await expect(state.loadPreset("bach")).resolves.toEqual({ ok: true });
  172. STUBS.fetchReject("nope");
  173. await expect(state.loadPreset("missing")).resolves.toBeNull();
  174. state.parameters = {
  175. step2: {
  176. part: [
  177. { key: 0, mode: "major", start_chord: "X", end_chord: "V" },
  178. { key: 7, mode: "minor", start_chord: "X", end_chord: "Y" },
  179. ],
  180. },
  181. };
  182. state.updateGraph({
  183. graph: { activeNodes: ["I"] },
  184. path: null,
  185. key: 0,
  186. mode: "major",
  187. });
  188. expect(state.parameters.step2.part[0].start_chord).toBe("I");
  189. expect(state.parameters.step2.part[0].end_chord).toBe("V");
  190. expect(state.parameters.step2.part[1].start_chord).toBe("i");
  191. expect(state.parameters.step2.part[1].end_chord).toBe("i");
  192. state.parameters = { step2: { part: [{ key: "5", mode: "major" }] } };
  193. expect(() => state.updateGraph()).not.toThrow();
  194. state.parameters = {};
  195. expect(() => state.updateGraph()).not.toThrow();
  196. document.getElementById.mockImplementation(() => null);
  197. state.updateGraph({
  198. graph: { activeNodes: [] },
  199. path: null,
  200. key: 0,
  201. mode: "major",
  202. });
  203. });
  204. });