@hyperframes/studio 0.7.89 → 0.7.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-DVTnBvSO.js → hyperframes-player-9qOa08Tv.js} +1 -1
- package/dist/assets/{index-Qvx7boWH.js → index-Bw4lNdtO.js} +1 -1
- package/dist/assets/{index-ulQEdWKo.js → index-CDTN2ZVa.js} +1 -1
- package/dist/assets/index-DIntkrQl.js +428 -0
- package/dist/index.d.ts +20 -16
- package/dist/index.html +1 -1
- package/dist/index.js +6495 -5429
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelTypes.ts +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/Timeline.test.ts +2 -3
- package/src/player/components/Timeline.tsx +47 -48
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.tsx +6 -3
- package/src/player/components/TimelineClipDiamonds.test.tsx +442 -18
- package/src/player/components/TimelineClipDiamonds.tsx +66 -211
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +0 -1
- package/src/player/components/TimelineLanes.tsx +3 -2
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +5 -15
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineLaneProps.ts +0 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +4 -4
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +11 -4
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/playerStore.test.ts +76 -0
- package/src/player/store/playerStore.ts +19 -11
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-CddKwfQ1.js +0 -428
|
@@ -3,23 +3,51 @@
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import {
|
|
6
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
7
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
8
|
+
import {
|
|
9
|
+
TimelineClipDiamonds,
|
|
10
|
+
TimelineDiamondLane,
|
|
11
|
+
type TimelineDiamondKeyframe,
|
|
12
|
+
} from "./TimelineClipDiamonds";
|
|
7
13
|
import { timelineKeyframeSelectionKey } from "./timelineKeyframeIdentity";
|
|
14
|
+
import { configureTimelineTestViewport } from "./timelineTestViewport";
|
|
15
|
+
import { readPendingTimelineKeyframeRetimes } from "./useTimelineKeyframeHandlers";
|
|
8
16
|
|
|
9
17
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
18
|
|
|
11
19
|
afterEach(() => {
|
|
12
20
|
document.body.innerHTML = "";
|
|
21
|
+
usePlayerStore.setState({ elements: [], timelineSessionEpoch: 0 });
|
|
13
22
|
});
|
|
14
23
|
|
|
24
|
+
const RETIME_ELEMENT: TimelineElement = {
|
|
25
|
+
id: "clip-1",
|
|
26
|
+
label: "Clip",
|
|
27
|
+
tag: "div",
|
|
28
|
+
start: 0,
|
|
29
|
+
duration: 10,
|
|
30
|
+
track: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
15
33
|
function pointerEvent(type: string, init: PointerEventInit): Event {
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
const event =
|
|
35
|
+
typeof PointerEvent === "function" ? new PointerEvent(type, init) : new MouseEvent(type, init);
|
|
36
|
+
if (!("pointerId" in event)) {
|
|
37
|
+
Object.defineProperty(event, "pointerId", { value: init.pointerId ?? 0 });
|
|
38
|
+
}
|
|
39
|
+
return event;
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
function
|
|
42
|
+
function createTimelineHost() {
|
|
21
43
|
const host = document.createElement("div");
|
|
44
|
+
host.setAttribute("data-timeline-scroll-viewport", "");
|
|
22
45
|
document.body.append(host);
|
|
46
|
+
return host;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderDiamonds(onClickKeyframe = vi.fn()) {
|
|
50
|
+
const host = createTimelineHost();
|
|
23
51
|
const root = createRoot(host);
|
|
24
52
|
act(() => {
|
|
25
53
|
root.render(
|
|
@@ -46,6 +74,78 @@ function renderDiamonds(onClickKeyframe = vi.fn()) {
|
|
|
46
74
|
return { host, root, onClickKeyframe };
|
|
47
75
|
}
|
|
48
76
|
|
|
77
|
+
function renderRetimeLane(
|
|
78
|
+
onMoveKeyframe = vi.fn().mockResolvedValue(true),
|
|
79
|
+
strict = false,
|
|
80
|
+
options: {
|
|
81
|
+
elementId?: string;
|
|
82
|
+
host?: HTMLDivElement;
|
|
83
|
+
keyframes?: TimelineDiamondKeyframe[];
|
|
84
|
+
} = {},
|
|
85
|
+
) {
|
|
86
|
+
const elementId = options.elementId ?? RETIME_ELEMENT.id;
|
|
87
|
+
if (!options.host) {
|
|
88
|
+
usePlayerStore.setState({ elements: [{ ...RETIME_ELEMENT, id: elementId, key: elementId }] });
|
|
89
|
+
}
|
|
90
|
+
const host = options.host ?? createTimelineHost();
|
|
91
|
+
const keyframes = options.keyframes ?? [
|
|
92
|
+
{
|
|
93
|
+
percentage: 0,
|
|
94
|
+
tweenPercentage: 0,
|
|
95
|
+
propertyGroup: "position",
|
|
96
|
+
animationId: "anim-1",
|
|
97
|
+
properties: { x: 0 },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
percentage: 50,
|
|
101
|
+
tweenPercentage: 50,
|
|
102
|
+
propertyGroup: "position",
|
|
103
|
+
animationId: "anim-1",
|
|
104
|
+
properties: { x: 100 },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
percentage: 100,
|
|
108
|
+
tweenPercentage: 100,
|
|
109
|
+
propertyGroup: "position",
|
|
110
|
+
animationId: "anim-1",
|
|
111
|
+
properties: { x: 200 },
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const onClickKeyframe = vi.fn();
|
|
115
|
+
const mountLane = () => {
|
|
116
|
+
const laneHost = document.createElement("div");
|
|
117
|
+
host.append(laneHost);
|
|
118
|
+
const root = createRoot(laneHost);
|
|
119
|
+
const lane = (
|
|
120
|
+
<TimelineDiamondLane
|
|
121
|
+
keyframesData={{
|
|
122
|
+
format: "percentage",
|
|
123
|
+
keyframes,
|
|
124
|
+
}}
|
|
125
|
+
clipWidthPx={200}
|
|
126
|
+
clipHeightPx={48}
|
|
127
|
+
accentColor="#4ba3d2"
|
|
128
|
+
isSelected
|
|
129
|
+
currentPercentage={0}
|
|
130
|
+
elementId={elementId}
|
|
131
|
+
selectedKeyframes={new Set()}
|
|
132
|
+
onClickKeyframe={onClickKeyframe}
|
|
133
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
134
|
+
groupAware
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
act(() => {
|
|
138
|
+
root.render(strict ? <React.StrictMode>{lane}</React.StrictMode> : lane);
|
|
139
|
+
});
|
|
140
|
+
const diamond = laneHost.querySelector<HTMLButtonElement>(
|
|
141
|
+
`button[title="${keyframes[1]?.percentage}%"]`,
|
|
142
|
+
);
|
|
143
|
+
expect(diamond).not.toBeNull();
|
|
144
|
+
return { diamond: diamond!, root };
|
|
145
|
+
};
|
|
146
|
+
return { ...mountLane(), host, mountLane, onClickKeyframe, onMoveKeyframe };
|
|
147
|
+
}
|
|
148
|
+
|
|
49
149
|
describe("TimelineClipDiamonds", () => {
|
|
50
150
|
it("marks only the nearest keyframe in a dense lane as under the playhead", () => {
|
|
51
151
|
const host = document.createElement("div");
|
|
@@ -161,6 +261,23 @@ describe("TimelineClipDiamonds", () => {
|
|
|
161
261
|
act(() => root.unmount());
|
|
162
262
|
});
|
|
163
263
|
|
|
264
|
+
it("publishes retime previews after StrictMode effect replay", () => {
|
|
265
|
+
const { diamond, host, root } = renderRetimeLane(undefined, true);
|
|
266
|
+
const initialLeft = diamond.style.left;
|
|
267
|
+
act(() => {
|
|
268
|
+
diamond.dispatchEvent(
|
|
269
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
270
|
+
);
|
|
271
|
+
window.dispatchEvent(
|
|
272
|
+
pointerEvent("pointermove", { bubbles: true, clientX: 120, pointerId: 7 }),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
expect(host.querySelector<HTMLButtonElement>('button[title="50%"]')?.style.left).not.toBe(
|
|
276
|
+
initialLeft,
|
|
277
|
+
);
|
|
278
|
+
act(() => root.unmount());
|
|
279
|
+
});
|
|
280
|
+
|
|
164
281
|
it("treats primary pointerup without drag as a keyframe click", () => {
|
|
165
282
|
const { host, root, onClickKeyframe } = renderDiamonds();
|
|
166
283
|
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]');
|
|
@@ -246,8 +363,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
246
363
|
it("treats a drag-armed press that resolves to a no-op move as a click", () => {
|
|
247
364
|
const onClickKeyframe = vi.fn();
|
|
248
365
|
const onMoveKeyframe = vi.fn();
|
|
249
|
-
const host =
|
|
250
|
-
document.body.append(host);
|
|
366
|
+
const host = createTimelineHost();
|
|
251
367
|
const root = createRoot(host);
|
|
252
368
|
act(() => {
|
|
253
369
|
root.render(
|
|
@@ -314,8 +430,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
314
430
|
it("reselects a retimed keyframe with its post-move tween percentage", () => {
|
|
315
431
|
const onClickKeyframe = vi.fn();
|
|
316
432
|
const onMoveKeyframe = vi.fn().mockResolvedValue(true);
|
|
317
|
-
const host =
|
|
318
|
-
document.body.append(host);
|
|
433
|
+
const host = createTimelineHost();
|
|
319
434
|
const root = createRoot(host);
|
|
320
435
|
act(() => {
|
|
321
436
|
root.render(
|
|
@@ -474,8 +589,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
474
589
|
|
|
475
590
|
it("composes a rapid second retime from the pending position", () => {
|
|
476
591
|
const onMoveKeyframe = vi.fn().mockResolvedValue(true);
|
|
477
|
-
const host =
|
|
478
|
-
document.body.append(host);
|
|
592
|
+
const host = createTimelineHost();
|
|
479
593
|
const root = createRoot(host);
|
|
480
594
|
act(() => {
|
|
481
595
|
root.render(
|
|
@@ -548,16 +662,120 @@ describe("TimelineClipDiamonds", () => {
|
|
|
548
662
|
},
|
|
549
663
|
85,
|
|
550
664
|
);
|
|
665
|
+
|
|
666
|
+
act(() => {
|
|
667
|
+
usePlayerStore.setState({ timelineSessionEpoch: 1 });
|
|
668
|
+
diamond!.dispatchEvent(
|
|
669
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100 }),
|
|
670
|
+
);
|
|
671
|
+
diamond!.dispatchEvent(pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120 }));
|
|
672
|
+
});
|
|
673
|
+
expect(onMoveKeyframe).toHaveBeenNthCalledWith(
|
|
674
|
+
3,
|
|
675
|
+
expect.objectContaining({ percentage: 50 }),
|
|
676
|
+
60,
|
|
677
|
+
);
|
|
551
678
|
act(() => root.unmount());
|
|
552
679
|
});
|
|
553
680
|
|
|
681
|
+
it("preserves another element's pending retime when the active source is removed", () => {
|
|
682
|
+
const host = createTimelineHost();
|
|
683
|
+
const elementA = { ...RETIME_ELEMENT, id: "clip-a", key: "clip-a" };
|
|
684
|
+
const elementB = { ...RETIME_ELEMENT, id: "clip-b", key: "clip-b" };
|
|
685
|
+
usePlayerStore.setState({ elements: [elementA, elementB] });
|
|
686
|
+
const laneA = renderRetimeLane(vi.fn().mockResolvedValue(true), false, {
|
|
687
|
+
elementId: "clip-a",
|
|
688
|
+
host,
|
|
689
|
+
});
|
|
690
|
+
const onMoveB = vi.fn().mockResolvedValue(true);
|
|
691
|
+
const laneB = renderRetimeLane(onMoveB, false, { elementId: "clip-b", host });
|
|
692
|
+
|
|
693
|
+
act(() => {
|
|
694
|
+
laneB.diamond.dispatchEvent(
|
|
695
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
696
|
+
);
|
|
697
|
+
window.dispatchEvent(
|
|
698
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 150, pointerId: 7 }),
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
laneA.diamond.dispatchEvent(
|
|
702
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
703
|
+
);
|
|
704
|
+
usePlayerStore.setState({ elements: [elementB] });
|
|
705
|
+
|
|
706
|
+
laneB.diamond.dispatchEvent(
|
|
707
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 150, pointerId: 11 }),
|
|
708
|
+
);
|
|
709
|
+
window.dispatchEvent(
|
|
710
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 170, pointerId: 11 }),
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
expect(onMoveB).toHaveBeenNthCalledWith(2, expect.objectContaining({ percentage: 75 }), 85);
|
|
715
|
+
act(() => {
|
|
716
|
+
laneA.root.unmount();
|
|
717
|
+
laneB.root.unmount();
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("retires a pending retime once the cache exposes its destination identity", () => {
|
|
722
|
+
const first = renderRetimeLane();
|
|
723
|
+
act(() => {
|
|
724
|
+
first.diamond.dispatchEvent(
|
|
725
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
726
|
+
);
|
|
727
|
+
window.dispatchEvent(
|
|
728
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 150, pointerId: 7 }),
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
expect(readPendingTimelineKeyframeRetimes(first.host).size).toBe(1);
|
|
732
|
+
act(() => first.root.unmount());
|
|
733
|
+
|
|
734
|
+
const destination = renderRetimeLane(vi.fn().mockResolvedValue(true), false, {
|
|
735
|
+
host: first.host,
|
|
736
|
+
keyframes: [
|
|
737
|
+
{
|
|
738
|
+
percentage: 0,
|
|
739
|
+
tweenPercentage: 0,
|
|
740
|
+
propertyGroup: "position",
|
|
741
|
+
animationId: "anim-1",
|
|
742
|
+
properties: { x: 0 },
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
percentage: 75,
|
|
746
|
+
tweenPercentage: 75,
|
|
747
|
+
propertyGroup: "position",
|
|
748
|
+
animationId: "anim-1",
|
|
749
|
+
properties: { x: 100 },
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
percentage: 100,
|
|
753
|
+
tweenPercentage: 100,
|
|
754
|
+
propertyGroup: "position",
|
|
755
|
+
animationId: "anim-1",
|
|
756
|
+
properties: { x: 200 },
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
});
|
|
760
|
+
act(() => {
|
|
761
|
+
destination.diamond.dispatchEvent(
|
|
762
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 150, pointerId: 9 }),
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
expect(readPendingTimelineKeyframeRetimes(first.host).size).toBe(0);
|
|
766
|
+
|
|
767
|
+
act(() => {
|
|
768
|
+
window.dispatchEvent(pointerEvent("pointercancel", { bubbles: true, pointerId: 9 }));
|
|
769
|
+
destination.root.unmount();
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
554
773
|
it.each([
|
|
555
774
|
["returns false", () => Promise.resolve(false)],
|
|
556
775
|
["rejects", () => Promise.reject(new Error("retime failed"))],
|
|
557
776
|
])("clears a failed pending retime when the callback %s", async (_label, settle) => {
|
|
558
777
|
const onMoveKeyframe = vi.fn().mockImplementationOnce(settle).mockResolvedValue(true);
|
|
559
|
-
const host =
|
|
560
|
-
document.body.append(host);
|
|
778
|
+
const host = createTimelineHost();
|
|
561
779
|
const root = createRoot(host);
|
|
562
780
|
act(() => {
|
|
563
781
|
root.render(
|
|
@@ -678,6 +896,215 @@ describe("TimelineClipDiamonds", () => {
|
|
|
678
896
|
act(() => root.unmount());
|
|
679
897
|
});
|
|
680
898
|
|
|
899
|
+
it("commits once from the stable viewport after the source lane unmounts", () => {
|
|
900
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
901
|
+
|
|
902
|
+
act(() => {
|
|
903
|
+
diamond.dispatchEvent(
|
|
904
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
905
|
+
);
|
|
906
|
+
root.unmount();
|
|
907
|
+
window.dispatchEvent(
|
|
908
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
909
|
+
);
|
|
910
|
+
window.dispatchEvent(
|
|
911
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 140, pointerId: 7 }),
|
|
912
|
+
);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(
|
|
916
|
+
{
|
|
917
|
+
percentage: 50,
|
|
918
|
+
tweenPercentage: 50,
|
|
919
|
+
propertyGroup: "position",
|
|
920
|
+
animationId: "anim-1",
|
|
921
|
+
},
|
|
922
|
+
60,
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("keeps the retime preview coherent when the source lane remounts", () => {
|
|
927
|
+
const { diamond, mountLane, onMoveKeyframe, root } = renderRetimeLane();
|
|
928
|
+
|
|
929
|
+
act(() => {
|
|
930
|
+
diamond.dispatchEvent(
|
|
931
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
932
|
+
);
|
|
933
|
+
window.dispatchEvent(
|
|
934
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
935
|
+
);
|
|
936
|
+
});
|
|
937
|
+
const beforeUnmountLeft = Number.parseFloat(diamond.style.left);
|
|
938
|
+
|
|
939
|
+
act(() => {
|
|
940
|
+
root.unmount();
|
|
941
|
+
window.dispatchEvent(
|
|
942
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 140, pointerId: 7 }),
|
|
943
|
+
);
|
|
944
|
+
});
|
|
945
|
+
const { diamond: remountedDiamond, root: remountedRoot } = mountLane();
|
|
946
|
+
const remountedLeft = Number.parseFloat(remountedDiamond.style.left);
|
|
947
|
+
expect(remountedLeft).toBeGreaterThan(beforeUnmountLeft);
|
|
948
|
+
|
|
949
|
+
act(() => {
|
|
950
|
+
window.dispatchEvent(
|
|
951
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 160, pointerId: 7 }),
|
|
952
|
+
);
|
|
953
|
+
});
|
|
954
|
+
expect(Number.parseFloat(remountedDiamond.style.left)).toBeGreaterThan(remountedLeft);
|
|
955
|
+
|
|
956
|
+
act(() => {
|
|
957
|
+
window.dispatchEvent(
|
|
958
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 160, pointerId: 7 }),
|
|
959
|
+
);
|
|
960
|
+
});
|
|
961
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(expect.any(Object), 80);
|
|
962
|
+
act(() => remountedRoot.unmount());
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("includes horizontal viewport scrolling in the retime destination", () => {
|
|
966
|
+
const { diamond, host, onMoveKeyframe, root } = renderRetimeLane();
|
|
967
|
+
|
|
968
|
+
act(() => {
|
|
969
|
+
diamond.dispatchEvent(
|
|
970
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
971
|
+
);
|
|
972
|
+
host.scrollLeft = 20;
|
|
973
|
+
window.dispatchEvent(
|
|
974
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(expect.any(Object), 60);
|
|
979
|
+
act(() => root.unmount());
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it("auto-scrolls horizontally without virtualizing the source row away", () => {
|
|
983
|
+
let frame: FrameRequestCallback | null = null;
|
|
984
|
+
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((callback) => {
|
|
985
|
+
frame = callback;
|
|
986
|
+
return 1;
|
|
987
|
+
});
|
|
988
|
+
vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => undefined);
|
|
989
|
+
const { diamond, host, root } = renderRetimeLane();
|
|
990
|
+
configureTimelineTestViewport(host, 10_000);
|
|
991
|
+
|
|
992
|
+
act(() => {
|
|
993
|
+
diamond.dispatchEvent(
|
|
994
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
995
|
+
);
|
|
996
|
+
window.dispatchEvent(
|
|
997
|
+
pointerEvent("pointermove", {
|
|
998
|
+
bubbles: true,
|
|
999
|
+
button: 0,
|
|
1000
|
+
clientX: 790,
|
|
1001
|
+
clientY: 239,
|
|
1002
|
+
pointerId: 7,
|
|
1003
|
+
}),
|
|
1004
|
+
);
|
|
1005
|
+
});
|
|
1006
|
+
expect(frame).not.toBeNull();
|
|
1007
|
+
act(() => frame?.(0));
|
|
1008
|
+
expect(host.scrollLeft).toBeGreaterThan(0);
|
|
1009
|
+
expect(host.scrollTop).toBe(0);
|
|
1010
|
+
|
|
1011
|
+
act(() => {
|
|
1012
|
+
window.dispatchEvent(pointerEvent("pointercancel", { bubbles: true, pointerId: 7 }));
|
|
1013
|
+
root.unmount();
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it("ignores another pointer and lets the owning pointer finish", () => {
|
|
1018
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1019
|
+
|
|
1020
|
+
act(() => {
|
|
1021
|
+
diamond.dispatchEvent(
|
|
1022
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1023
|
+
);
|
|
1024
|
+
window.dispatchEvent(
|
|
1025
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 140, pointerId: 8 }),
|
|
1026
|
+
);
|
|
1027
|
+
window.dispatchEvent(
|
|
1028
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1029
|
+
);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1033
|
+
act(() => root.unmount());
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it("cancels without mutation on pointer cancel or Escape and allows the next retime", () => {
|
|
1037
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1038
|
+
|
|
1039
|
+
act(() => {
|
|
1040
|
+
diamond.dispatchEvent(
|
|
1041
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1042
|
+
);
|
|
1043
|
+
window.dispatchEvent(
|
|
1044
|
+
pointerEvent("pointercancel", {
|
|
1045
|
+
bubbles: true,
|
|
1046
|
+
button: 0,
|
|
1047
|
+
clientX: 120,
|
|
1048
|
+
pointerId: 7,
|
|
1049
|
+
}),
|
|
1050
|
+
);
|
|
1051
|
+
window.dispatchEvent(
|
|
1052
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
diamond.dispatchEvent(
|
|
1056
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
1057
|
+
);
|
|
1058
|
+
window.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
|
|
1059
|
+
window.dispatchEvent(
|
|
1060
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 9 }),
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
diamond.dispatchEvent(
|
|
1064
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 11 }),
|
|
1065
|
+
);
|
|
1066
|
+
window.dispatchEvent(
|
|
1067
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 11 }),
|
|
1068
|
+
);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1072
|
+
act(() => root.unmount());
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it("cancels on project switch or source removal without poisoning the next gesture", () => {
|
|
1076
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1077
|
+
|
|
1078
|
+
act(() => {
|
|
1079
|
+
diamond.dispatchEvent(
|
|
1080
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1081
|
+
);
|
|
1082
|
+
usePlayerStore.setState({ timelineSessionEpoch: 1 });
|
|
1083
|
+
window.dispatchEvent(
|
|
1084
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
diamond.dispatchEvent(
|
|
1088
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
1089
|
+
);
|
|
1090
|
+
usePlayerStore.setState({ elements: [] });
|
|
1091
|
+
window.dispatchEvent(
|
|
1092
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 9 }),
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
usePlayerStore.setState({ elements: [RETIME_ELEMENT] });
|
|
1096
|
+
diamond.dispatchEvent(
|
|
1097
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 11 }),
|
|
1098
|
+
);
|
|
1099
|
+
window.dispatchEvent(
|
|
1100
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 11 }),
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1105
|
+
act(() => root.unmount());
|
|
1106
|
+
});
|
|
1107
|
+
|
|
681
1108
|
// Regression: onClickKeyframe's state updates can re-render the diamond
|
|
682
1109
|
// button out from under the gesture before the browser auto-synthesizes the
|
|
683
1110
|
// "click" event that follows a button's pointerdown+pointerup. That orphaned
|
|
@@ -687,8 +1114,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
687
1114
|
// own clip. suppressClickRef lets that ancestor ignore the stray click.
|
|
688
1115
|
it("arms suppressClickRef synchronously on a keyframe click", () => {
|
|
689
1116
|
const suppressClickRef = { current: false };
|
|
690
|
-
const host =
|
|
691
|
-
document.body.append(host);
|
|
1117
|
+
const host = createTimelineHost();
|
|
692
1118
|
const root = createRoot(host);
|
|
693
1119
|
act(() => {
|
|
694
1120
|
root.render(
|
|
@@ -722,8 +1148,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
722
1148
|
});
|
|
723
1149
|
|
|
724
1150
|
const renderSegmentLane = (lastAmbiguous: boolean, clipWidthPx = 200) => {
|
|
725
|
-
const host =
|
|
726
|
-
document.body.append(host);
|
|
1151
|
+
const host = createTimelineHost();
|
|
727
1152
|
const root = createRoot(host);
|
|
728
1153
|
const kf = (percentage: number, extra: Record<string, unknown> = {}) => ({
|
|
729
1154
|
percentage,
|
|
@@ -811,8 +1236,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
811
1236
|
it("hides the inline ease button on a segment with no source animation id", () => {
|
|
812
1237
|
// A runtime-scanned keyframe has no animationId, so there is no tween to
|
|
813
1238
|
// target; the segment ending on it must not render a (dead) ease button.
|
|
814
|
-
const host =
|
|
815
|
-
document.body.append(host);
|
|
1239
|
+
const host = createTimelineHost();
|
|
816
1240
|
const root = createRoot(host);
|
|
817
1241
|
const kf = (percentage: number, animationId?: string) => ({
|
|
818
1242
|
percentage,
|