import { describe, it, expect } from "vitest"; import type { TimelineElement } from "../player/store/playerStore"; import { SPLIT_BOUNDARY_EPSILON_S, canSplitElementAt, isSplitTimeWithinBounds, selectSplittableElements, } from "./timelineElementSplit"; function element(overrides: Partial = {}): TimelineElement { return { id: "el-1", tag: "div", start: 1, duration: 4, track: 0, ...overrides, }; } describe("isSplitTimeWithinBounds", () => { const start = 1; const duration = 4; const end = start + duration; it("accepts the exact lower clamp boundary", () => { // The timeline canvas clamps an edge click to exactly // start + SPLIT_BOUNDARY_EPSILON_S, so that value must be splittable. expect(isSplitTimeWithinBounds(start + SPLIT_BOUNDARY_EPSILON_S, start, duration)).toBe(true); }); it("accepts the exact upper clamp boundary", () => { expect( isSplitTimeWithinBounds(start + duration - SPLIT_BOUNDARY_EPSILON_S, start, duration), ).toBe(true); }); it("accepts an interior split time", () => { expect(isSplitTimeWithinBounds(3, start, duration)).toBe(true); }); it("rejects times at or outside the clip edges", () => { expect(isSplitTimeWithinBounds(start, start, duration)).toBe(false); expect(isSplitTimeWithinBounds(end, start, duration)).toBe(false); expect(isSplitTimeWithinBounds(start - 1, start, duration)).toBe(false); expect(isSplitTimeWithinBounds(end + 1, start, duration)).toBe(false); }); it("rejects times inside the epsilon margins", () => { expect(isSplitTimeWithinBounds(start + SPLIT_BOUNDARY_EPSILON_S / 2, start, duration)).toBe( false, ); expect(isSplitTimeWithinBounds(end - SPLIT_BOUNDARY_EPSILON_S / 2, start, duration)).toBe( false, ); }); it("rejects every time on a clip shorter than two epsilons", () => { // Math.max(min, Math.min(max, t)) collapses to min when the clip is too // short for the clamp range; that collapsed value must still be rejected. const shortDuration = SPLIT_BOUNDARY_EPSILON_S; expect(isSplitTimeWithinBounds(start + SPLIT_BOUNDARY_EPSILON_S, start, shortDuration)).toBe( false, ); expect(isSplitTimeWithinBounds(start + shortDuration / 2, start, shortDuration)).toBe(false); }); }); describe("canSplitElementAt", () => { it("accepts a splittable element at an interior time", () => { expect(canSplitElementAt(element({ start: 1, duration: 4 }), 3)).toBe(true); }); it("rejects a time inside the boundary epsilon", () => { expect( canSplitElementAt(element({ start: 1, duration: 4 }), 1 + SPLIT_BOUNDARY_EPSILON_S / 2), ).toBe(false); }); it("rejects locked, implicit and sub-composition elements", () => { expect(canSplitElementAt(element({ timelineLocked: true }), 3)).toBe(false); expect(canSplitElementAt(element({ timingSource: "implicit" }), 3)).toBe(false); expect(canSplitElementAt(element({ compositionSrc: "child.html" }), 3)).toBe(false); }); }); describe("selectSplittableElements", () => { it("excludes a clip shorter than two epsilons even when the time is inside it", () => { // Regression: split-all used raw start < t < end, so a clip too short for // the epsilon margin was still selected and produced a degenerate slice. const tiny = element({ id: "tiny", start: 1, duration: SPLIT_BOUNDARY_EPSILON_S + 0.01 }); const interiorTime = tiny.start + tiny.duration / 2; expect(interiorTime).toBeGreaterThan(tiny.start); expect(interiorTime).toBeLessThan(tiny.start + tiny.duration); expect(selectSplittableElements([tiny], interiorTime)).toEqual([]); }); it("keeps only the elements whose epsilon-bounded range contains the time", () => { const inside = element({ id: "inside", start: 0, duration: 4 }); const outside = element({ id: "outside", start: 5, duration: 4 }); const locked = element({ id: "locked", start: 0, duration: 4, timelineLocked: true }); const result = selectSplittableElements([inside, outside, locked], 2); expect(result.map((el) => el.id)).toEqual(["inside"]); }); it("accepts an element at the exact lower clamp boundary", () => { const el = element({ start: 2, duration: 4 }); expect(selectSplittableElements([el], 2 + SPLIT_BOUNDARY_EPSILON_S)).toEqual([el]); }); });