Home Manual Reference Source Test

tests/plotting.test.js

  1. import { describe, it, expect, vi } from "vitest";
  2. import { PLOTTING } from "../src/view/plotting.js";
  3. import { chordGraph } from "../src/model/theory.js";
  4. import { FIXTURES, STUBS } from "./api.js";
  5. const { majorChordGraphWithChromatic } = FIXTURES;
  6. /** @test {PLOTTING} */
  7. describe("PLOTTING", () => {
  8. const {
  9. wireMidiToPlayer,
  10. buildResultTable,
  11. applyVoiceStyles,
  12. plotChordGraph,
  13. } = PLOTTING;
  14. /** @test {PLOTTING#wireMidiToPlayer} */
  15. it("wireMidiToPlayer sets player.src + visualizer.src to combined data URL, binds visualizer once, and wires per-voice <midi-visualizer> elements via querySelectorAll", () => {
  16. const player = STUBS.midiPlayer();
  17. const visualizer = STUBS.midiVisualizer();
  18. const v1 = STUBS.midiVisualizer();
  19. const v2 = STUBS.midiVisualizer();
  20. STUBS.documentPlot(
  21. { "midi-player": player, "midi-visualizer": visualizer },
  22. [v1, v2],
  23. );
  24. wireMidiToPlayer({ combinedB64: "M", voicesB64: ["a", "b"] });
  25. expect(player.src).toBe("data:audio/midi;base64,M");
  26. expect(visualizer.src).toBe("data:audio/midi;base64,M");
  27. expect(player.addVisualizer).toHaveBeenCalledTimes(1);
  28. expect(v1.src).toBe("data:audio/midi;base64,a");
  29. expect(v2.src).toBe("data:audio/midi;base64,b");
  30. wireMidiToPlayer({ combinedB64: "M2", voicesB64: undefined });
  31. expect(player.addVisualizer).toHaveBeenCalledTimes(1);
  32. document.getElementById.mockImplementation(() => null);
  33. document.querySelectorAll.mockReturnValue([]);
  34. expect(() =>
  35. wireMidiToPlayer({ combinedB64: "M3", voicesB64: ["x"] }),
  36. ).not.toThrow();
  37. vi.unstubAllGlobals();
  38. });
  39. /** @test {PLOTTING#buildResultTable} */
  40. it("buildResultTable shapes parts + labelCache + voiceConfig into the rendered result-table data; missing labels fall back to 'Part i', missing voice fields default", () => {
  41. const voiceConfig = {
  42. voices: 2,
  43. voice: [
  44. { instrument: 0, color: "#abc" },
  45. { instrument: 1, color: "#def" },
  46. ],
  47. };
  48. const result = {
  49. parts: [{ label: "A" }, {}],
  50. labelCache: {
  51. A: { voicesB64: ["v1A", "v2A"], concreteProgression: "I-V" },
  52. "Part 2": { voicesB64: ["v1B"] },
  53. },
  54. };
  55. const t = buildResultTable(result, voiceConfig);
  56. expect(t.headers).toEqual(["A", "Part 2"]);
  57. expect(t.cells).toEqual([
  58. ["v1A", "v1B"],
  59. ["v2A", null],
  60. ]);
  61. expect(t.voices).toBe(2);
  62. expect(t.voiceLabels).toHaveLength(2);
  63. expect(t.voiceLabels[0]).toMatchObject({ num: 1, color: "#abc" });
  64. expect(t.partChordNames).toEqual(["I-V", ""]);
  65. const empty = buildResultTable(
  66. { parts: [{}], labelCache: {} },
  67. { voices: 1, voice: [{}] },
  68. );
  69. expect(empty.voiceLabels[0].color).toBe("#888888");
  70. });
  71. /** @test {PLOTTING#applyVoiceStyles} */
  72. it("applyVoiceStyles creates a #voice-colors style element on first call and sets per-voice rect.note rules; reuses the element on repeat", () => {
  73. const styleEl = STUBS.styleEl();
  74. STUBS.documentVoiceStyles(styleEl);
  75. applyVoiceStyles([{ color: "#abc" }, { color: "#def" }]);
  76. expect(styleEl.id).toBe("voice-colors");
  77. expect(styleEl.textContent).toContain("#abc");
  78. expect(styleEl.textContent).toContain("#def");
  79. expect(styleEl.textContent).toContain('data-instrument="0"');
  80. applyVoiceStyles([{ color: "#xyz" }]);
  81. expect(styleEl.textContent).toContain("#xyz");
  82. expect(styleEl.textContent).not.toContain("#abc");
  83. vi.unstubAllGlobals();
  84. });
  85. /** @test {PLOTTING#plotChordGraph} */
  86. it("plotChordGraph renders SVG with diatonic nodes in gray when no path is given; adds gradient and a lit self-loop when a path is supplied", () => {
  87. const g = chordGraph("major");
  88. const noPath = plotChordGraph(g, null, 0, "major");
  89. expect(noPath.startsWith("<svg")).toBe(true);
  90. expect(noPath.endsWith("</svg>")).toBe(true);
  91. expect(noPath).toContain("<circle");
  92. expect(noPath).toContain('stroke="#999"');
  93. expect(noPath).not.toContain("<linearGradient");
  94. expect(noPath).toMatch(/>I<\/text>/);
  95. expect(noPath).toMatch(/>C<\/text>/);
  96. const withPath = plotChordGraph(
  97. g,
  98. { nodes: ["V", "I"], edges: new Set(["V->I", "I->I"]) },
  99. 0,
  100. "major",
  101. );
  102. expect(withPath).toContain("<linearGradient");
  103. expect(withPath).toContain("url(#eg0)");
  104. expect(withPath).toContain('opacity="1"');
  105. });
  106. /** @test {PLOTTING#plotChordGraph} */
  107. it("plotChordGraph works in minor mode and accepts costs as a plain Object at the post-orchestrator boundary", () => {
  108. const g = chordGraph("minor");
  109. const adj = {
  110. nodes: g.nodes,
  111. edges: g.edges,
  112. costs: Object.fromEntries(g.costs),
  113. };
  114. const svg = plotChordGraph(adj, null, 9, "minor");
  115. expect(svg).toContain("<svg");
  116. expect(svg).toMatch(/>i<\/text>/);
  117. expect(svg).toMatch(/>Am<\/text>/);
  118. });
  119. /** @test {PLOTTING#plotChordGraph} */
  120. it("plotChordGraph handles chromatic edge cases by graying paths, drawing self-loop arcs only when present and placing single versus multi chromatics correctly", () => {
  121. const grayG = majorChordGraphWithChromatic(["bII"], {
  122. "I->bII": 3,
  123. "bII->V": 3,
  124. });
  125. const graySvg = plotChordGraph(
  126. grayG,
  127. { nodes: ["I", "bII", "V"], edges: new Set(["I->bII", "bII->V"]) },
  128. 0,
  129. "major",
  130. );
  131. expect(graySvg).toContain("rgb(120,120,120)");
  132. const noLoopAdj = {
  133. nodes: ["I", "V"],
  134. edges: [["I", "V"]],
  135. costs: { "I->V": 2 },
  136. };
  137. const noLoopSvg = plotChordGraph(noLoopAdj, null, 0, "major");
  138. expect(noLoopSvg.match(/marker-end/g) || []).toHaveLength(1);
  139. const oneSvg = plotChordGraph(
  140. majorChordGraphWithChromatic(["bII"], { "I->bII": 3 }),
  141. null,
  142. 0,
  143. "major",
  144. );
  145. expect(oneSvg).toMatch(/>bII<\/text>/);
  146. expect(oneSvg).toContain('cx="430"');
  147. const multiSvg = plotChordGraph(
  148. majorChordGraphWithChromatic(["bII", "bIII"], {
  149. "I->bII": 3,
  150. "I->bIII": 3,
  151. }),
  152. null,
  153. 0,
  154. "major",
  155. );
  156. expect(multiSvg).toMatch(/>bII<\/text>/);
  157. expect(multiSvg).toMatch(/>bIII<\/text>/);
  158. });
  159. });