import { test, expect, describe } from "bun:test"; import { dragAudio, dragMidi, hitAnchor, anchorAtCursor, MIN_GAP_SEC } from "../../../src/align/page/edit.ts"; import { makeWarp, type Anchor } from "../../../src/align/warp.ts"; const m = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "manual" }); const a = (midiSec: number, audioSec: number): Anchor => ({ midiSec, audioSec, kind: "auto" }); const base = [m(10, 11), m(20, 22), m(30, 32)]; describe("dragAudio (recording end)", () => { test("moves audioSec, keeps midiSec, returns the anchor's new index", () => { const r = dragAudio(base, 1, 23); expect(r.anchors[r.index]).toEqual(m(20, 23)); expect(r.anchors).toHaveLength(3); }); test("cannot cross manual neighbours", () => { expect(dragAudio(base, 1, 5).anchors[1]!.audioSec).toBeCloseTo(11 + MIN_GAP_SEC, 9); expect(dragAudio(base, 1, 99).anchors[1]!.audioSec).toBeCloseTo(32 - MIN_GAP_SEC, 9); }); test("the first anchor cannot be dragged before the start of the recording", () => { expect(dragAudio(base, 0, -3).anchors[0]!.audioSec).toBe(0); }); test("dragging an auto anchor makes it manual and drops the autos in its stretches", () => { const list = [m(0, 0), a(5, 5.2), a(10, 10.3), a(15, 15.1), m(20, 20)]; const r = dragAudio(list, 2, 10.5); expect(r.anchors).toEqual([m(0, 0), m(10, 10.5), m(20, 20)]); expect(r.index).toBe(1); }); }); describe("dragMidi (MIDI end): the grabbed MIDI moment follows the mouse, audioSec stays", () => { for (const [grab, to] of [[22, 23.5], [22, 20.5], [21, 25], [24, 21]] as const) { test(`grab at ${grab}s, drag to ${to}s`, () => { const m0 = makeWarp(base).toMidi(grab); const r = dragMidi(base, 1, m0, to); expect(r.anchors[r.index]!.audioSec).toBe(22); expect(makeWarp(r.anchors).toAudio(m0)).toBeCloseTo(to, 6); }); } test("no drag = no change", () => { const r = dragMidi(base, 1, 20, 22); expect(r.anchors[1]!.midiSec).toBeCloseTo(20, 6); }); test("clamped between manual neighbours' MIDI times", () => { const r = dragMidi(base, 1, 20, 31.9); expect(r.anchors[1]!.midiSec).toBeGreaterThanOrEqual(10 + MIN_GAP_SEC - 1e-9); const s = dragMidi(base, 1, 20, 11.1); expect(s.anchors[1]!.midiSec).toBeLessThanOrEqual(30 - MIN_GAP_SEC + 1e-9); }); test("works for the first and only anchor (edge extrapolation)", () => { const r = dragMidi([m(10, 11)], 0, 10, 12); expect(makeWarp(r.anchors).toAudio(10)).toBeCloseTo(12, 6); expect(r.anchors[0]!.audioSec).toBe(11); const f = dragMidi(base, 0, 10, 12); expect(makeWarp(f.anchors).toAudio(10)).toBeCloseTo(12, 6); }); }); test("hitAnchor picks the nearest anchor line within tolerance", () => { const v = { start: 0, pxPerSec: 10, width: 500 }; expect(hitAnchor(base, v, 221, 6)).toBe(1); expect(hitAnchor(base, v, 113, 6)).toBe(0); expect(hitAnchor(base, v, 160, 6)).toBe(-1); }); test("anchorAtCursor adds a manual anchor where the current warp maps the cursor", () => { const r = anchorAtCursor(base, 27); const k = r.anchors.findIndex((x) => x.audioSec === 27); expect(r.index).toBe(k); expect(r.anchors[k]).toEqual(m(makeWarp(base).toMidi(27), 27)); expect(() => anchorAtCursor(base, 22)).toThrow(); // already an anchor there });