mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
Review 1 (restore commit): - asset reveal now clears any open preview overlay (stuck-overlay repro: preview on A, click already-added B — A stayed open over the reveal) - duration readout rolls back on failed persist: captureDurationRollback snapshots store + live root data-duration before the optimistic sync and restores both in every move/resize/delete/group catch (golden's previousDuration pattern) - asset preview opened during running playback dismisses immediately (the RAF loop bypasses the store, so the subscription alone never fired) - persistTimelineBatchEdit resolves the target (findTagByTarget) before treating identical output as a no-op — a mistargeted member now throws like the single-element path instead of being silently dropped - a post-mutation history-fold failure no longer suppresses the preview sync: fold errors are surfaced separately and the rewritten script still syncs (previously the preview kept stale GSAP positions with no recovery) - timelineRevealScroll guards degenerate viewports (windowSize <= 0) - CodeQL: encodeURIComponent(projectId) at all timelineTimingSync fetches Review 2 (single-source-of-truth pass): - createTimelineElementFromManifestClip — the one manifest->element boundary — now carries authoredTrack and stackingContextId; expanded sub-comp children preserve both (authoredTrack in their OWN file's space) - authoredTrackForLane scopes occupants to the dragged clip's sourceFile (a foreign file's authored values are a different coordinate space); nearest-same-file-lane offset fallback - optimistic store updates mirror the persisted track into authoredTrack (and roll it back on failure), so consecutive drags before a reload resolve from fresh data - spill sub-lanes: documented decision — dropping onto a spill lane is a legitimate same-track join (occupants share the authored track by construction); false 'never a lane-move target' docstring rewritten - single-element fallback persists vertical-only moves (early return now requires neither start nor track changed; live DOM patch includes data-track-index) - canonical contextKey helper for stacking-context normalization - new pipeline test crosses the REAL factory boundary (sparse authored tracks -> factory -> expansion -> normalize -> drag commit -> persisted attribute), no injected fields
134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import {
|
||
computeRevealScroll,
|
||
REVEAL_SCROLL_PADDING_PX,
|
||
type RevealScrollInput,
|
||
} from "./timelineRevealScroll";
|
||
|
||
/** A 1000×400 viewport with a 32px sticky gutter and 24px sticky ruler. */
|
||
function makeInput(overrides: Partial<RevealScrollInput> = {}): RevealScrollInput {
|
||
return {
|
||
scrollLeft: 0,
|
||
scrollTop: 0,
|
||
viewportWidth: 1000,
|
||
viewportHeight: 400,
|
||
clipLeft: 100,
|
||
clipRight: 200,
|
||
clipTop: 100,
|
||
clipBottom: 148,
|
||
stickyLeft: 32,
|
||
stickyTop: 24,
|
||
allowHorizontal: true,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe("computeRevealScroll", () => {
|
||
it("returns null on both axes when the clip is fully visible", () => {
|
||
expect(computeRevealScroll(makeInput())).toEqual({ left: null, top: null });
|
||
});
|
||
|
||
it("scrolls right minimally when the clip end is past the right edge", () => {
|
||
const result = computeRevealScroll(makeInput({ clipLeft: 1500, clipRight: 1600 }));
|
||
// Clip end lands padding px inside the right edge.
|
||
expect(result.left).toBe(1600 - 1000 + REVEAL_SCROLL_PADDING_PX);
|
||
expect(result.top).toBeNull();
|
||
});
|
||
|
||
it("scrolls left when the clip start is hidden (including under the sticky gutter)", () => {
|
||
const result = computeRevealScroll(
|
||
makeInput({ scrollLeft: 500, clipLeft: 510, clipRight: 610 }),
|
||
);
|
||
// clipLeft 510 sits under the 32px sticky gutter (window starts at 500+32+pad).
|
||
expect(result.left).toBe(510 - 32 - REVEAL_SCROLL_PADDING_PX);
|
||
});
|
||
|
||
it("aligns the start edge when the clip is wider than the viewport", () => {
|
||
const result = computeRevealScroll(makeInput({ clipLeft: 2000, clipRight: 4000 }));
|
||
expect(result.left).toBe(2000 - 32 - REVEAL_SCROLL_PADDING_PX);
|
||
});
|
||
|
||
it("never returns a negative scroll target", () => {
|
||
const result = computeRevealScroll(
|
||
makeInput({
|
||
scrollLeft: 300,
|
||
clipLeft: 10,
|
||
clipRight: 60,
|
||
scrollTop: 200,
|
||
clipTop: 4,
|
||
clipBottom: 20,
|
||
}),
|
||
);
|
||
expect(result.left).toBe(0);
|
||
expect(result.top).toBe(0);
|
||
});
|
||
|
||
it("suppresses horizontal scroll when allowHorizontal is false (fit zoom)", () => {
|
||
const result = computeRevealScroll(
|
||
makeInput({
|
||
allowHorizontal: false,
|
||
clipLeft: 1500,
|
||
clipRight: 1600,
|
||
clipTop: 700,
|
||
clipBottom: 748,
|
||
}),
|
||
);
|
||
expect(result.left).toBeNull();
|
||
// Vertical reveal still happens.
|
||
expect(result.top).toBe(748 - 400 + REVEAL_SCROLL_PADDING_PX);
|
||
});
|
||
|
||
it("scrolls up when the clip lane is hidden under the sticky ruler", () => {
|
||
const result = computeRevealScroll(
|
||
makeInput({ scrollTop: 200, clipTop: 210, clipBottom: 258 }),
|
||
);
|
||
// clipTop 210 sits under the 24px sticky ruler (window starts at 200+24+pad).
|
||
expect(result.top).toBe(210 - 24 - REVEAL_SCROLL_PADDING_PX);
|
||
});
|
||
|
||
it("scrolls down minimally when the clip lane is below the viewport", () => {
|
||
const result = computeRevealScroll(makeInput({ clipTop: 500, clipBottom: 548 }));
|
||
expect(result.top).toBe(548 - 400 + REVEAL_SCROLL_PADDING_PX);
|
||
expect(result.left).toBeNull();
|
||
});
|
||
|
||
it("never scrolls on an axis whose visible window is degenerate", () => {
|
||
// Horizontal window collapses: 40px viewport minus the 32px gutter and
|
||
// 2x12px padding is negative; vertical is intact and still reveals.
|
||
const result = computeRevealScroll(
|
||
makeInput({
|
||
viewportWidth: 40,
|
||
clipLeft: 1500,
|
||
clipRight: 1600,
|
||
clipTop: 500,
|
||
clipBottom: 548,
|
||
}),
|
||
);
|
||
expect(result.left).toBeNull();
|
||
expect(result.top).toBe(548 - 400 + REVEAL_SCROLL_PADDING_PX);
|
||
|
||
// Exactly-zero window (viewport == sticky + 2x padding) is degenerate too.
|
||
const zero = computeRevealScroll(
|
||
makeInput({
|
||
viewportWidth: 32 + 2 * REVEAL_SCROLL_PADDING_PX,
|
||
clipLeft: 1500,
|
||
clipRight: 1600,
|
||
}),
|
||
);
|
||
expect(zero.left).toBeNull();
|
||
|
||
// Both axes degenerate: no scroll at all.
|
||
const both = computeRevealScroll(
|
||
makeInput({
|
||
viewportWidth: 10,
|
||
viewportHeight: 10,
|
||
clipLeft: 1500,
|
||
clipRight: 1600,
|
||
clipTop: 500,
|
||
clipBottom: 548,
|
||
}),
|
||
);
|
||
expect(both).toEqual({ left: null, top: null });
|
||
});
|
||
});
|