tests/theory.test.js
- import { describe, it, expect } from "vitest";
- import { chordName, chordGraph, timeGrid } from "../src/model/theory.js";
- describe("chordName", () => {
-
- it("returns label form without keyIndex; transposes to concrete chord with sharp/flat spelling and quality suffix; passes NC and unknowns through", () => {
- expect(chordName("I")).toBe("I: Tonic");
- expect(chordName("V")).toBe("V: Dominant");
- expect(chordName("NC")).toBe("NC: No Chord");
- expect(chordName("UNKNOWN")).toBe("UNKNOWN");
- expect(chordName("NC", 0)).toBe("NC");
- expect(chordName("I", 0)).toBe("C");
- expect(chordName("I", 7)).toBe("G");
- expect(chordName("vi", 0)).toBe("Am");
- expect(chordName("viio", 0)).toBe("Bo");
- expect(chordName("UNKNOWN", 0)).toBe("UNKNOWN");
- });
- });
- describe("chordGraph", () => {
-
- it("provides major and minor defaults with nodes, tonic, cadence-normal-NC-loop cost ordering and edges aligned with costs.keys()", () => {
- const major = chordGraph();
- expect(major.tonic).toBe("I");
- expect(major.nodes).toEqual("I ii iii IV V vi viio NC".split(" "));
- expect(major.activeNodes).toEqual(major.nodes);
- const c = major.costs;
- expect(c.get("V->I")).toBeLessThan(c.get("I->ii"));
- expect(c.get("I->ii")).toBeLessThan(c.get("I->NC"));
- expect(c.get("I->NC")).toBeLessThan(c.get("I->I"));
- expect(major.edges.every(([u, v]) => c.has(`${u}->${v}`))).toBe(true);
- const minor = chordGraph("minor");
- expect(minor.tonic).toBe("i");
- expect(minor.nodes).toContain("bVI");
- expect(minor.nodes).toContain("bVII");
- expect(minor.costs.get("V->i")).toBeLessThan(minor.costs.get("i->iv"));
- });
-
- it("adds chromatic nodes from a preset, applies preset costs, clamps self-loops and skips unknown edges", () => {
- const baseLoopCost = chordGraph("major").costs.get("I->I");
- const g = chordGraph("major", {
- major: {
- chromatic: ["bII"],
- edges: { "I->bII": 3, "bII->V": 5, "I->I": 1, "Foo->Bar": 99 },
- },
- });
- expect(g.nodes).toContain("bII");
- expect(g.costs.get("I->bII")).toBe(3);
- expect(g.costs.get("bII->V")).toBe(5);
- expect(g.costs.get("I->I")).toBe(baseLoopCost);
- expect(g.costs.has("Foo->Bar")).toBe(false);
- });
- });
- describe("timeGrid", () => {
-
- it("derives tau as LCM of voice durations (1 for unknowns), T = iota * zeta * tau, and per-voice onset sets", () => {
- const v = (duration) => ({ duration });
- expect(
- timeGrid([v("whole"), v("eighth"), v("triplet")], 3, 4, 1).subdivision,
- ).toBe(6);
- expect(timeGrid([v("half"), v("sixteenth")], 2, 4, 1).subdivision).toBe(4);
- expect(timeGrid([v("weird")], 1, 4, 1).subdivision).toBe(1);
- const t = timeGrid([v("quarter"), v("eighth")], 2, 4, 2);
- expect(t.subdivision).toBe(2);
- expect(t.subBeatsPerBar).toBe(8);
- expect(t.numSubBeats).toBe(16);
- expect(t.activeBeats[0].has(0)).toBe(true);
- expect(t.activeBeats[1].has(1)).toBe(true);
- });
- });