tests/controls.test.js
- import { describe, it, expect, vi } from "vitest";
- import { CONTROLS } from "../src/view/controls.js";
- import { STUBS } from "./api.js";
- describe("CONTROLS", () => {
- const {
- stepBy,
- isDuplicatePart,
- duplicateSourceIndex,
- clampVoicePitchRange,
- setupChordTonesTristate,
- setupFeedbackTristates,
- } = CONTROLS;
-
- it("stepBy bumps params[name] by delta and clamps to [min, max]", () => {
- const params = { parts: 3 };
- const desc = { name: "parts", min: 1, max: 5 };
- stepBy(params, desc, 1);
- expect(params.parts).toBe(4);
- stepBy(params, desc, 10);
- expect(params.parts).toBe(5);
- stepBy(params, desc, -10);
- expect(params.parts).toBe(1);
- });
-
- it("clampVoicePitchRange snaps the just-edited field to the other when the range inverts; no-op when range is valid", () => {
- const v1 = { min_pitch: 70, max_pitch: 60 };
- clampVoicePitchRange(v1, "min_pitch");
- expect(v1.min_pitch).toBe(60);
- const v2 = { min_pitch: 70, max_pitch: 60 };
- clampVoicePitchRange(v2, "max_pitch");
- expect(v2.max_pitch).toBe(70);
- const v3 = { min_pitch: 50, max_pitch: 70 };
- clampVoicePitchRange(v3, "min_pitch");
- expect(v3).toEqual({ min_pitch: 50, max_pitch: 70 });
- });
-
- it("isDuplicatePart + duplicateSourceIndex flag reused labels in 'parts'/'step1.parts' sections; 1-based source index, 0 when none, undefined-list-safe", () => {
- const parts = [{ label: "A" }, { label: "B" }, { label: "A" }];
- expect(isDuplicatePart(parts, 2, "parts")).toBe(true);
- expect(isDuplicatePart(parts, 1, "parts")).toBe(false);
- expect(isDuplicatePart(parts, 2, "voices")).toBe(false);
- expect(duplicateSourceIndex(parts, 2)).toBe(1);
- expect(duplicateSourceIndex(parts, 0)).toBe(0);
- expect(duplicateSourceIndex(undefined, 0)).toBe(0);
- expect(duplicateSourceIndex([{}], 0)).toBe(0);
- });
-
- it("setupChordTonesTristate cycles free to scale to chord on change; visual: chord = checked, scale = indeterminate, free = neither", () => {
- let value = "free";
- const el = STUBS.tristateInput();
- setupChordTonesTristate(
- el,
- () => value,
- (v) => (value = v),
- );
- expect(el).toMatchObject({ checked: false, indeterminate: false });
- const onChange = el.addEventListener.mock.calls[0][1];
- onChange();
- expect(value).toBe("scale");
- expect(el).toMatchObject({ checked: false, indeterminate: true });
- onChange();
- expect(value).toBe("chord");
- expect(el).toMatchObject({ checked: true, indeterminate: false });
- onChange();
- expect(value).toBe("free");
- });
-
- it("setupFeedbackTristates scans [data-tri-group] fieldsets, seeds triData[group][name] with 'neutral', toggles aria-invalid + tristate-* classes through the cycle", () => {
- const handlers = [];
- const cb = STUBS.tristateCheckbox(handlers);
- const fieldset = STUBS.tristateFieldset("questions", [cb]);
- const formEl = { querySelectorAll: vi.fn(() => [fieldset]) };
- const tri = {};
- setupFeedbackTristates(formEl, tri);
- expect(tri.questions.human).toBe("neutral");
- expect(cb.removeAttribute).toHaveBeenLastCalledWith("aria-invalid");
- handlers[0]();
- expect(tri.questions.human).toBe("negative");
- expect(cb.setAttribute).toHaveBeenLastCalledWith("aria-invalid", "true");
- expect(cb.classList.add).toHaveBeenLastCalledWith("tristate-negative");
- handlers[0]();
- expect(tri.questions.human).toBe("positive");
- expect(cb.setAttribute).toHaveBeenLastCalledWith("aria-invalid", "false");
- expect(cb.classList.add).toHaveBeenLastCalledWith("tristate-positive");
- handlers[0]();
- expect(tri.questions.human).toBe("neutral");
- });
- });