Files
hyperframes/packages/studio/src/player/components/timelineRevealScroll.test.ts
T
ukimsanov a33b3f35e1 fix(studio): address PR #2347 review findings (rounds 1-2)
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
2026-07-13 16:48:52 -07:00

134 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
});
});