tests/composer.test.js
- import { describe, it, expect, vi, beforeEach } from "vitest";
- import { FIXTURES, MOCKS, STUBS } from "./api.js";
- const { composeParams, step1PartA, step1PartB } = FIXTURES;
- const {
- composeHooks,
- step1: step1Stub,
- step2: step2Stub,
- step3: step3Stub,
- } = STUBS;
- const { default: MidiWriter } = await MOCKS.midiWriter;
- const { compose } = await import("../src/controller/composer.js");
- describe("compose", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- STUBS.composerMidiWriter(MidiWriter);
- });
-
- 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 () => {
- const emptyLog = vi.fn();
- const empty = await compose(
- composeParams([]),
- [step1Stub, step2Stub(), step3Stub()],
- composeHooks({ log: emptyLog }),
- );
- expect(empty).toBeNull();
- expect(emptyLog).toHaveBeenCalledWith("No parts defined.");
- const onProgressionLine = vi.fn();
- const result = await compose(
- composeParams([step1PartA, step1PartB]),
- [step1Stub, step2Stub(), step3Stub()],
- composeHooks({
- onProgressionLine,
- chords: { A: ["I", "V"], B: ["IV", "I"] },
- }),
- );
- expect(result.parts).toEqual([step1PartA, step1PartB]);
- expect(result.solvedParts).toBe(2);
- expect(result.failedParts).toEqual([]);
- expect(result.midi).toEqual({ combinedB64: "B64", voicesB64: ["B64"] });
- expect(result.labelCache.A.chords).toEqual(["I", "V"]);
- expect(result.labelCache.A.solution).toEqual({ note_0_0: 60 });
- expect(result.labelCache.A.voicesB64).toEqual(["B64"]);
- expect(onProgressionLine).toHaveBeenCalledTimes(2);
- expect(onProgressionLine.mock.calls[0][0]).toEqual({
- label: "A",
- chords: ["I", "V"],
- });
- const partial = await compose(
- composeParams([step1PartA, step1PartB, { ...step1PartB }]),
- [step1Stub, step2Stub(), step3Stub()],
- composeHooks({
- chords: { A: ["I", "V"], B: ["IV", "I"] },
- fails: { B: "fail" },
- }),
- );
- expect(partial.solvedParts).toBe(1);
- expect(partial.failedParts).toEqual(["B", "B"]);
- const twiceA = [step1PartA, { ...step1PartA }];
- const reuseStep2 = step2Stub();
- const reuseStep3 = step3Stub();
- const step2Spy = vi.spyOn(reuseStep2, "solve");
- const step3Spy = vi.spyOn(reuseStep3, "solve");
- const reuseResult = await compose(
- composeParams(twiceA),
- [step1Stub, reuseStep2, reuseStep3],
- composeHooks({ chords: { A: ["I", "I"] } }),
- );
- expect(step2Spy).toHaveBeenCalledTimes(1);
- expect(step3Spy).toHaveBeenCalledTimes(1);
- expect(reuseResult.solvedParts).toBe(2);
- const failTwiceA = await compose(
- composeParams(twiceA),
- [step1Stub, step2Stub(), step3Stub()],
- composeHooks({
- chords: { A: ["I"] },
- fails: { A: "fail" },
- }),
- );
- expect(failTwiceA.failedParts).toEqual(["A", "A"]);
- expect(failTwiceA.solvedParts).toBe(0);
- expect(failTwiceA.midi).toBeNull();
- const nullStep2 = { id: "step2", name: "Step 2", solve: async () => null };
- const step3 = step3Stub();
- const step3Solve = vi.spyOn(step3, "solve");
- const nullProgression = vi.fn();
- await compose(
- composeParams([step1PartA]),
- [step1Stub, nullStep2, step3],
- composeHooks({ onProgressionLine: nullProgression }),
- );
- expect(nullProgression).toHaveBeenCalledWith({ label: "A", chords: [] });
- expect(step3Solve.mock.calls[0][0].chords).toEqual([]);
- expect(step3Solve.mock.calls[0][0].bars).toBe(1);
- });
- });