mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
Clips share a lane only when they carry the same z-index and don't overlap in time. Previously a non-overlapping clip packed into the first time-compatible row regardless of z, so restacking changed the z-index but the clip's row never followed.
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { TimelineElement } from "../store/playerStore";
|
|
import { buildStackingTimelineLayers, insertPreviewTrackOrder } from "./timelineTrackOrder";
|
|
|
|
// fallow-ignore-next-line complexity
|
|
function rowElement(input: {
|
|
id: string;
|
|
track?: number;
|
|
zIndex?: number;
|
|
hasExplicitZIndex?: boolean;
|
|
start?: number;
|
|
duration?: number;
|
|
tag?: string;
|
|
stackingContextId?: string | null;
|
|
parentCompositionId?: string | null;
|
|
compositionAncestors?: string[];
|
|
}): TimelineElement {
|
|
return {
|
|
id: input.id,
|
|
tag: input.tag ?? "div",
|
|
start: input.start ?? 0,
|
|
duration: input.duration ?? 1,
|
|
track: input.track ?? 0,
|
|
zIndex: input.zIndex ?? 0,
|
|
hasExplicitZIndex: input.hasExplicitZIndex ?? true,
|
|
stackingContextId: input.stackingContextId ?? "root",
|
|
parentCompositionId: input.parentCompositionId ?? null,
|
|
compositionAncestors: input.compositionAncestors ?? ["root"],
|
|
};
|
|
}
|
|
|
|
function rowIds(rows: readonly { elements: readonly TimelineElement[] }[]): string[][] {
|
|
return rows.map((row) => row.elements.map((element) => element.id));
|
|
}
|
|
|
|
describe("buildStackingTimelineLayers", () => {
|
|
it("splits non-overlapping clips into separate lanes when their z-index differs", () => {
|
|
// A lane is a z-band: differing z must land on different rows even when the
|
|
// clips don't overlap in time, so a vertical (z) restack actually moves the
|
|
// clip's row. Rows are ordered by descending z.
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "back", zIndex: 1, start: 0, duration: 1 }),
|
|
rowElement({ id: "front", zIndex: 10, start: 1, duration: 1 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["front"], ["back"]]);
|
|
expect(result.visualLayers[0]?.zIndex).toBe(10);
|
|
});
|
|
|
|
it("packs non-overlapping clips into one lane when they share a z-index", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "back", zIndex: 5, start: 0, duration: 1 }),
|
|
rowElement({ id: "front", zIndex: 5, start: 1, duration: 1 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["back", "front"]]);
|
|
expect(result.visualLayers[0]?.zIndex).toBe(5);
|
|
});
|
|
|
|
it("splits clips into separate lanes when they overlap in time", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "front", zIndex: 10, start: 0, duration: 2 }),
|
|
rowElement({ id: "back", zIndex: 1, start: 1, duration: 2 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["front"], ["back"]]);
|
|
});
|
|
|
|
it("uses DOM order to break stacking ties before lane packing", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "first", track: 2, zIndex: 5, start: 0, duration: 2 }),
|
|
rowElement({ id: "second", track: 0, zIndex: 5, start: 1, duration: 2 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["first"], ["second"]]);
|
|
});
|
|
|
|
it("packs auto-z clips by time instead of forcing one row per clip", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "a", zIndex: 0, hasExplicitZIndex: false, start: 0, duration: 1 }),
|
|
rowElement({ id: "b", zIndex: 0, hasExplicitZIndex: false, start: 1, duration: 1 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["a", "b"]]);
|
|
});
|
|
|
|
it("does not merge equal z-index clips across stacking contexts", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "root", zIndex: 4, start: 0, duration: 1 }),
|
|
rowElement({
|
|
id: "nested",
|
|
zIndex: 4,
|
|
start: 1,
|
|
duration: 1,
|
|
stackingContextId: "scene",
|
|
parentCompositionId: "scene",
|
|
compositionAncestors: ["root", "scene"],
|
|
}),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["root"], ["nested"]]);
|
|
});
|
|
|
|
it("returns audio clips as separate bottom rows without merging them into z layers", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "front", zIndex: 10 }),
|
|
rowElement({ id: "music-a", tag: "audio", track: 4 }),
|
|
rowElement({ id: "music-b", tag: "audio", track: 2 }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["front"]]);
|
|
expect(rowIds(result.audioLayers)).toEqual([["music-a"], ["music-b"]]);
|
|
expect(rowIds(result.rows)).toEqual([["front"], ["music-a"], ["music-b"]]);
|
|
});
|
|
|
|
it("orders rows by descending z-index with auto-z clips ranked at computed zero", () => {
|
|
const result = buildStackingTimelineLayers([
|
|
rowElement({ id: "auto-a", zIndex: 0, hasExplicitZIndex: false }),
|
|
rowElement({ id: "back", zIndex: -1 }),
|
|
rowElement({ id: "front", zIndex: 10 }),
|
|
rowElement({ id: "auto-b", zIndex: 0, hasExplicitZIndex: false }),
|
|
]);
|
|
|
|
expect(rowIds(result.visualLayers)).toEqual([["front"], ["auto-a"], ["auto-b"], ["back"]]);
|
|
});
|
|
|
|
it("keeps a row key stable when a clip's z-index changes but membership does not", () => {
|
|
const before = buildStackingTimelineLayers([rowElement({ id: "hero", zIndex: 1 })]);
|
|
const after = buildStackingTimelineLayers([rowElement({ id: "hero", zIndex: 20 })]);
|
|
|
|
expect(after.visualLayers[0]?.id).toBe(before.visualLayers[0]?.id);
|
|
});
|
|
});
|
|
|
|
describe("insertPreviewTrackOrder", () => {
|
|
it("inserts preview layer ids by target row index", () => {
|
|
expect(insertPreviewTrackOrder(["a", "b", "c"], "preview", 1)).toEqual([
|
|
"a",
|
|
"preview",
|
|
"b",
|
|
"c",
|
|
]);
|
|
});
|
|
});
|