Home Manual Reference Source Test

tests/composer.test.js

  1. import { describe, it, expect, vi, beforeEach } from "vitest";
  2. import { FIXTURES, MOCKS, STUBS } from "./api.js";
  3. const { composeParams, step1PartA, step1PartB } = FIXTURES;
  4. const {
  5. composeHooks,
  6. step1: step1Stub,
  7. step2: step2Stub,
  8. step3: step3Stub,
  9. } = STUBS;
  10. const { default: MidiWriter } = await MOCKS.midiWriter;
  11. const { compose } = await import("../src/controller/composer.js");
  12. /** @test {compose} */
  13. describe("compose", () => {
  14. beforeEach(() => {
  15. vi.clearAllMocks();
  16. STUBS.composerMidiWriter(MidiWriter);
  17. });
  18. /** @test {compose} */
  19. it("returns null on empty input; emits MIDI + parts + solved cache + progression lines on the multi-label happy path; reports failed labels on partial fail; reuses Step2 + Step3 once for repeated labels via the label cache; falls back when step.solve returns null", async () => {
  20. const emptyLog = vi.fn();
  21. const empty = await compose(
  22. composeParams([]),
  23. [step1Stub, step2Stub(), step3Stub()],
  24. composeHooks({ log: emptyLog }),
  25. );
  26. expect(empty).toBeNull();
  27. expect(emptyLog).toHaveBeenCalledWith("No parts defined.");
  28. const onProgressionLine = vi.fn();
  29. const result = await compose(
  30. composeParams([step1PartA, step1PartB]),
  31. [step1Stub, step2Stub(), step3Stub()],
  32. composeHooks({
  33. onProgressionLine,
  34. chords: { A: ["I", "V"], B: ["IV", "I"] },
  35. }),
  36. );
  37. expect(result.parts).toEqual([step1PartA, step1PartB]);
  38. expect(result.solvedParts).toBe(2);
  39. expect(result.failedParts).toEqual([]);
  40. expect(result.midi).toEqual({ combinedB64: "B64", voicesB64: ["B64"] });
  41. expect(result.labelCache.A.chords).toEqual(["I", "V"]);
  42. expect(result.labelCache.A.solution).toEqual({ note_0_0: 60 });
  43. expect(result.labelCache.A.voicesB64).toEqual(["B64"]);
  44. expect(onProgressionLine).toHaveBeenCalledTimes(2);
  45. expect(onProgressionLine.mock.calls[0][0]).toEqual({
  46. label: "A",
  47. chords: ["I", "V"],
  48. });
  49. const partial = await compose(
  50. composeParams([step1PartA, step1PartB, { ...step1PartB }]),
  51. [step1Stub, step2Stub(), step3Stub()],
  52. composeHooks({
  53. chords: { A: ["I", "V"], B: ["IV", "I"] },
  54. fails: { B: "fail" },
  55. }),
  56. );
  57. expect(partial.solvedParts).toBe(1);
  58. expect(partial.failedParts).toEqual(["B", "B"]);
  59. const twiceA = [step1PartA, { ...step1PartA }];
  60. const reuseStep2 = step2Stub();
  61. const reuseStep3 = step3Stub();
  62. const step2Spy = vi.spyOn(reuseStep2, "solve");
  63. const step3Spy = vi.spyOn(reuseStep3, "solve");
  64. const reuseResult = await compose(
  65. composeParams(twiceA),
  66. [step1Stub, reuseStep2, reuseStep3],
  67. composeHooks({ chords: { A: ["I", "I"] } }),
  68. );
  69. expect(step2Spy).toHaveBeenCalledTimes(1);
  70. expect(step3Spy).toHaveBeenCalledTimes(1);
  71. expect(reuseResult.solvedParts).toBe(2);
  72. const failTwiceA = await compose(
  73. composeParams(twiceA),
  74. [step1Stub, step2Stub(), step3Stub()],
  75. composeHooks({
  76. chords: { A: ["I"] },
  77. fails: { A: "fail" },
  78. }),
  79. );
  80. expect(failTwiceA.failedParts).toEqual(["A", "A"]);
  81. expect(failTwiceA.solvedParts).toBe(0);
  82. expect(failTwiceA.midi).toBeNull();
  83. const nullStep2 = { id: "step2", name: "Step 2", solve: async () => null };
  84. const step3 = step3Stub();
  85. const step3Solve = vi.spyOn(step3, "solve");
  86. const nullProgression = vi.fn();
  87. await compose(
  88. composeParams([step1PartA]),
  89. [step1Stub, nullStep2, step3],
  90. composeHooks({ onProgressionLine: nullProgression }),
  91. );
  92. expect(nullProgression).toHaveBeenCalledWith({ label: "A", chords: [] });
  93. expect(step3Solve.mock.calls[0][0].chords).toEqual([]);
  94. expect(step3Solve.mock.calls[0][0].bars).toBe(1);
  95. });
  96. });