Home Manual Reference Source Test

tests/theory.test.js

  1. import { describe, it, expect } from "vitest";
  2. import { chordName, chordGraph, timeGrid } from "../src/model/theory.js";
  3. /** @test {chordName} */
  4. describe("chordName", () => {
  5. /** @test {chordName} */
  6. it("returns label form without keyIndex; transposes to concrete chord with sharp/flat spelling and quality suffix; passes NC and unknowns through", () => {
  7. expect(chordName("I")).toBe("I: Tonic");
  8. expect(chordName("V")).toBe("V: Dominant");
  9. expect(chordName("NC")).toBe("NC: No Chord");
  10. expect(chordName("UNKNOWN")).toBe("UNKNOWN");
  11. expect(chordName("NC", 0)).toBe("NC");
  12. expect(chordName("I", 0)).toBe("C");
  13. expect(chordName("I", 7)).toBe("G");
  14. expect(chordName("vi", 0)).toBe("Am");
  15. expect(chordName("viio", 0)).toBe("Bo");
  16. expect(chordName("UNKNOWN", 0)).toBe("UNKNOWN");
  17. });
  18. });
  19. /** @test {chordGraph} */
  20. describe("chordGraph", () => {
  21. /** @test {chordGraph} */
  22. it("provides major and minor defaults with nodes, tonic, cadence-normal-NC-loop cost ordering and edges aligned with costs.keys()", () => {
  23. const major = chordGraph();
  24. expect(major.tonic).toBe("I");
  25. expect(major.nodes).toEqual("I ii iii IV V vi viio NC".split(" "));
  26. expect(major.activeNodes).toEqual(major.nodes);
  27. const c = major.costs;
  28. expect(c.get("V->I")).toBeLessThan(c.get("I->ii"));
  29. expect(c.get("I->ii")).toBeLessThan(c.get("I->NC"));
  30. expect(c.get("I->NC")).toBeLessThan(c.get("I->I"));
  31. expect(major.edges.every(([u, v]) => c.has(`${u}->${v}`))).toBe(true);
  32. const minor = chordGraph("minor");
  33. expect(minor.tonic).toBe("i");
  34. expect(minor.nodes).toContain("bVI");
  35. expect(minor.nodes).toContain("bVII");
  36. expect(minor.costs.get("V->i")).toBeLessThan(minor.costs.get("i->iv"));
  37. });
  38. /** @test {chordGraph} */
  39. it("adds chromatic nodes from a preset, applies preset costs, clamps self-loops and skips unknown edges", () => {
  40. const baseLoopCost = chordGraph("major").costs.get("I->I");
  41. const g = chordGraph("major", {
  42. major: {
  43. chromatic: ["bII"],
  44. edges: { "I->bII": 3, "bII->V": 5, "I->I": 1, "Foo->Bar": 99 },
  45. },
  46. });
  47. expect(g.nodes).toContain("bII");
  48. expect(g.costs.get("I->bII")).toBe(3);
  49. expect(g.costs.get("bII->V")).toBe(5);
  50. expect(g.costs.get("I->I")).toBe(baseLoopCost);
  51. expect(g.costs.has("Foo->Bar")).toBe(false);
  52. });
  53. });
  54. /** @test {timeGrid} */
  55. describe("timeGrid", () => {
  56. /** @test {timeGrid} */
  57. it("derives tau as LCM of voice durations (1 for unknowns), T = iota * zeta * tau, and per-voice onset sets", () => {
  58. const v = (duration) => ({ duration });
  59. expect(
  60. timeGrid([v("whole"), v("eighth"), v("triplet")], 3, 4, 1).subdivision,
  61. ).toBe(6);
  62. expect(timeGrid([v("half"), v("sixteenth")], 2, 4, 1).subdivision).toBe(4);
  63. expect(timeGrid([v("weird")], 1, 4, 1).subdivision).toBe(1);
  64. const t = timeGrid([v("quarter"), v("eighth")], 2, 4, 2);
  65. expect(t.subdivision).toBe(2);
  66. expect(t.subBeatsPerBar).toBe(8);
  67. expect(t.numSubBeats).toBe(16);
  68. expect(t.activeBeats[0].has(0)).toBe(true);
  69. expect(t.activeBeats[1].has(1)).toBe(true);
  70. });
  71. });