mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): restore golden-branch timeline behaviors dropped by the stack rebuild
The Studio stack rebuild (#2291) landed the remaining NLE layers but dropped or regressed several final-wave behaviors from the reviewed studio-dnd stack, and never repaired the stale timelineZones.ts that #2279 introduced. Restores: - TimelineRuler: sticky under vertical scroll, full-height gridlines removed (beat lines only), frame-number tick labels via a persisted timeDisplayMode store preference (PlayerControls toggle now store-backed) - timelineZones: stable track lanes — lane = authored data-track-index ascending; z is paint order only (replaces the stale z-driven lane pack, which broke track insert-band commits that contractually depend on it) - persistTimelineBatchEdit: a batch member whose patch is a no-op (attributes already at target values, e.g. in a track-insert renumber) is skipped instead of aborting and rolling back the whole batch — this alone made new-track creation (incl. the top insert band) fail silently - useTimelineStackingSync: unresolvable clips read as NaN again so timelineStackingSync's Number.isFinite exclusion contract holds (z=0 fabrications skewed stacking boundaries) - timelineAssetDrop: drops land on the drop track (no overlap bump to max-track+1), data-hf-id stamped, audio gets data-volume - timing edits: soft-reload the server's rewritten GSAP script instead of a full iframe remount (no all-clips flash on move/resize); full reload only when no scriptText or the soft path can't apply, and one full reload when a group edit touches non-active files (new hooks/timelineTimingSync.ts) - duration: content-driven grow-AND-shrink on move/resize/delete, synced optimistically to the store and the live root data-duration at release (was a grow-only ratchet; shrink never updated the readout) New UX: sidebar asset click opens a compact non-modal preview over the canvas (dismiss on outside click, Escape, playback, or seek), and clicking an already-added asset reveals its clip in the timeline (smooth minimal scroll to its time and lane; vertical-only in fit zoom). Verified by pointer-driving a real project: sticky ruler + gridline removal, no iframe remount on move/resize (marker survives, GSAP tween positions rewritten in place), duration readout 40->37->40 on shrink/stretch, and top-insert-band track creation renumbering lanes correctly on disk.
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
applyTimelineStackingReorder,
|
||||
buildTimelineMoveTimingPatch,
|
||||
deleteSelectedKeyframes,
|
||||
extendRootDurationIfNeeded,
|
||||
persistTimelineBatchEdit,
|
||||
type PersistTimelineBatchChange,
|
||||
} from "./timelineEditingHelpers";
|
||||
import type { TimelineElement } from "../player/store/playerStore";
|
||||
import { usePlayerStore } from "../player/store/playerStore";
|
||||
@@ -108,6 +111,91 @@ describe("extendRootDurationIfNeeded", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("persistTimelineBatchEdit", () => {
|
||||
const SOURCE = `<div id="root"><video id="a" class="clip" data-start="1" data-track-index="0"></video><video id="b" class="clip" data-start="2" data-track-index="1"></video></div>`;
|
||||
|
||||
function batchInput(changes: PersistTimelineBatchChange[], writes: Array<[string, string]>) {
|
||||
return {
|
||||
projectId: "p1",
|
||||
activeCompPath: "index.html",
|
||||
label: "Move timeline clips",
|
||||
changes,
|
||||
writeProjectFile: async (path: string, content: string) => {
|
||||
writes.push([path, content]);
|
||||
},
|
||||
recordEdit: async () => {},
|
||||
domEditSaveTimestampRef: { current: 0 },
|
||||
pendingTimelineEditPathRef: { current: new Set<string>() },
|
||||
};
|
||||
}
|
||||
|
||||
function stubReadFileContent(content: string) {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ content }),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("skips no-op members instead of aborting the batch (track-insert renumber)", async () => {
|
||||
// A track-insert renumber can include a member whose attributes already
|
||||
// hold the target values — its patch is string-identical. The batch must
|
||||
// skip it and still persist the members that DID change.
|
||||
stubReadFileContent(SOURCE);
|
||||
const writes: Array<[string, string]> = [];
|
||||
|
||||
await persistTimelineBatchEdit(
|
||||
batchInput(
|
||||
[
|
||||
{
|
||||
// no-op: data-start already "1", track already 0
|
||||
element: el({ id: "a", tag: "video", domId: "a", start: 1, track: 0 }),
|
||||
buildPatches: (original, target) =>
|
||||
buildTimelineMoveTimingPatch(original, target, 1, 5, 0),
|
||||
},
|
||||
{
|
||||
// real change: track 1 -> 2
|
||||
element: el({ id: "b", tag: "video", domId: "b", start: 2, track: 1 }),
|
||||
buildPatches: (original, target) =>
|
||||
buildTimelineMoveTimingPatch(original, target, 2, 5, 2),
|
||||
},
|
||||
],
|
||||
writes,
|
||||
),
|
||||
);
|
||||
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(writes[0]![0]).toBe("index.html");
|
||||
expect(writes[0]![1]).toContain('id="b" class="clip" data-start="2" data-track-index="2"');
|
||||
});
|
||||
|
||||
it("saves nothing when every member is a no-op", async () => {
|
||||
stubReadFileContent(SOURCE);
|
||||
const writes: Array<[string, string]> = [];
|
||||
|
||||
await persistTimelineBatchEdit(
|
||||
batchInput(
|
||||
[
|
||||
{
|
||||
element: el({ id: "a", tag: "video", domId: "a", start: 1, track: 0 }),
|
||||
buildPatches: (original, target) =>
|
||||
buildTimelineMoveTimingPatch(original, target, 1, 5, 0),
|
||||
},
|
||||
],
|
||||
writes,
|
||||
),
|
||||
);
|
||||
|
||||
expect(writes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteSelectedKeyframes", () => {
|
||||
it("coalesces all removals and reloads only after the last one", () => {
|
||||
usePlayerStore.setState({
|
||||
|
||||
Reference in New Issue
Block a user