@hyperframes/studio 0.7.90 → 0.7.93
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-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- 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/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- 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/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- 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/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- 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/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- 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 +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- 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 +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- 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/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -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 +26 -29
- 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/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- 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/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -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(), onShiftClickKeyframe = vi.fn()) {
|
|
50
|
+
const host = createTimelineHost();
|
|
23
51
|
const root = createRoot(host);
|
|
24
52
|
act(() => {
|
|
25
53
|
root.render(
|
|
@@ -33,17 +61,130 @@ function renderDiamonds(onClickKeyframe = vi.fn()) {
|
|
|
33
61
|
}}
|
|
34
62
|
clipWidthPx={200}
|
|
35
63
|
clipHeightPx={48}
|
|
36
|
-
clipDuration={10}
|
|
37
64
|
accentColor="#4ba3d2"
|
|
38
65
|
isSelected
|
|
39
66
|
currentPercentage={0}
|
|
40
67
|
elementId="clip-1"
|
|
68
|
+
clipStart={10}
|
|
69
|
+
clipDuration={10}
|
|
41
70
|
selectedKeyframes={new Set()}
|
|
42
71
|
onClickKeyframe={onClickKeyframe}
|
|
72
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
return { host, root, onClickKeyframe, onShiftClickKeyframe };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it("pins authored keyframes outside the clip to an inspectable boundary marker", () => {
|
|
80
|
+
const host = createTimelineHost();
|
|
81
|
+
const root = createRoot(host);
|
|
82
|
+
act(() => {
|
|
83
|
+
root.render(
|
|
84
|
+
<TimelineDiamondLane
|
|
85
|
+
keyframesData={{
|
|
86
|
+
format: "percentage",
|
|
87
|
+
keyframes: [
|
|
88
|
+
{ percentage: -40, properties: { x: 0 }, propertyGroup: "position" },
|
|
89
|
+
{ percentage: -20, properties: { x: 25 }, propertyGroup: "position" },
|
|
90
|
+
{ percentage: 5, properties: { x: 50 }, propertyGroup: "position" },
|
|
91
|
+
{ percentage: 120, properties: { x: 100 }, propertyGroup: "position" },
|
|
92
|
+
],
|
|
93
|
+
}}
|
|
94
|
+
clipWidthPx={200}
|
|
95
|
+
clipHeightPx={48}
|
|
96
|
+
accentColor="#4ba3d2"
|
|
97
|
+
isSelected
|
|
98
|
+
currentPercentage={5}
|
|
99
|
+
elementId="clip-1"
|
|
100
|
+
clipStart={10}
|
|
101
|
+
clipDuration={10}
|
|
102
|
+
selectedKeyframes={new Set()}
|
|
43
103
|
/>,
|
|
44
104
|
);
|
|
45
105
|
});
|
|
46
|
-
|
|
106
|
+
|
|
107
|
+
const before = host.querySelectorAll<HTMLButtonElement>('[data-keyframe-outside-clip="before"]');
|
|
108
|
+
const after = host.querySelector<HTMLButtonElement>('[data-keyframe-outside-clip="after"]');
|
|
109
|
+
expect(Array.from(before, (marker) => marker.style.left)).toEqual(["-35px", "-23px"]);
|
|
110
|
+
expect(before[0]?.getAttribute("aria-label")).toBe("position keyframe at 6s (before clip)");
|
|
111
|
+
expect(after?.style.left).toBe("201px");
|
|
112
|
+
expect(after?.getAttribute("aria-label")).toBe("position keyframe at 22s (after clip)");
|
|
113
|
+
expect(host.querySelectorAll('button[aria-label*="keyframe at"]')).toHaveLength(4);
|
|
114
|
+
|
|
115
|
+
act(() => root.unmount());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function renderRetimeLane(
|
|
119
|
+
onMoveKeyframe = vi.fn().mockResolvedValue(true),
|
|
120
|
+
strict = false,
|
|
121
|
+
options: {
|
|
122
|
+
elementId?: string;
|
|
123
|
+
host?: HTMLDivElement;
|
|
124
|
+
keyframes?: TimelineDiamondKeyframe[];
|
|
125
|
+
} = {},
|
|
126
|
+
) {
|
|
127
|
+
const elementId = options.elementId ?? RETIME_ELEMENT.id;
|
|
128
|
+
if (!options.host) {
|
|
129
|
+
usePlayerStore.setState({ elements: [{ ...RETIME_ELEMENT, id: elementId, key: elementId }] });
|
|
130
|
+
}
|
|
131
|
+
const host = options.host ?? createTimelineHost();
|
|
132
|
+
const keyframes = options.keyframes ?? [
|
|
133
|
+
{
|
|
134
|
+
percentage: 0,
|
|
135
|
+
tweenPercentage: 0,
|
|
136
|
+
propertyGroup: "position",
|
|
137
|
+
animationId: "anim-1",
|
|
138
|
+
properties: { x: 0 },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
percentage: 50,
|
|
142
|
+
tweenPercentage: 50,
|
|
143
|
+
propertyGroup: "position",
|
|
144
|
+
animationId: "anim-1",
|
|
145
|
+
properties: { x: 100 },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
percentage: 100,
|
|
149
|
+
tweenPercentage: 100,
|
|
150
|
+
propertyGroup: "position",
|
|
151
|
+
animationId: "anim-1",
|
|
152
|
+
properties: { x: 200 },
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
const onClickKeyframe = vi.fn();
|
|
156
|
+
const mountLane = () => {
|
|
157
|
+
const laneHost = document.createElement("div");
|
|
158
|
+
host.append(laneHost);
|
|
159
|
+
const root = createRoot(laneHost);
|
|
160
|
+
const lane = (
|
|
161
|
+
<TimelineDiamondLane
|
|
162
|
+
keyframesData={{
|
|
163
|
+
format: "percentage",
|
|
164
|
+
keyframes,
|
|
165
|
+
}}
|
|
166
|
+
clipWidthPx={200}
|
|
167
|
+
clipHeightPx={48}
|
|
168
|
+
accentColor="#4ba3d2"
|
|
169
|
+
isSelected
|
|
170
|
+
currentPercentage={0}
|
|
171
|
+
elementId={elementId}
|
|
172
|
+
selectedKeyframes={new Set()}
|
|
173
|
+
onClickKeyframe={onClickKeyframe}
|
|
174
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
175
|
+
groupAware
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
act(() => {
|
|
179
|
+
root.render(strict ? <React.StrictMode>{lane}</React.StrictMode> : lane);
|
|
180
|
+
});
|
|
181
|
+
const diamond = laneHost.querySelector<HTMLButtonElement>(
|
|
182
|
+
`button[title="${keyframes[1]?.percentage}%"]`,
|
|
183
|
+
);
|
|
184
|
+
expect(diamond).not.toBeNull();
|
|
185
|
+
return { diamond: diamond!, root };
|
|
186
|
+
};
|
|
187
|
+
return { ...mountLane(), host, mountLane, onClickKeyframe, onMoveKeyframe };
|
|
47
188
|
}
|
|
48
189
|
|
|
49
190
|
describe("TimelineClipDiamonds", () => {
|
|
@@ -161,6 +302,50 @@ describe("TimelineClipDiamonds", () => {
|
|
|
161
302
|
act(() => root.unmount());
|
|
162
303
|
});
|
|
163
304
|
|
|
305
|
+
it("gives keyframes time-based names and native keyboard selection semantics", () => {
|
|
306
|
+
const { host, root, onClickKeyframe } = renderDiamonds();
|
|
307
|
+
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]')!;
|
|
308
|
+
expect(diamond.getAttribute("aria-label")).toBe("Motion keyframe at 15s");
|
|
309
|
+
expect(diamond.getAttribute("aria-pressed")).toBe("false");
|
|
310
|
+
act(() => diamond.dispatchEvent(new MouseEvent("click", { bubbles: true, detail: 0 })));
|
|
311
|
+
expect(onClickKeyframe).toHaveBeenCalledWith(
|
|
312
|
+
"clip-1",
|
|
313
|
+
expect.objectContaining({ percentage: 50 }),
|
|
314
|
+
);
|
|
315
|
+
act(() => root.unmount());
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("uses Shift+Space's native click for additive keyframe selection", () => {
|
|
319
|
+
const { host, root, onClickKeyframe, onShiftClickKeyframe } = renderDiamonds();
|
|
320
|
+
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]')!;
|
|
321
|
+
act(() =>
|
|
322
|
+
diamond.dispatchEvent(new MouseEvent("click", { bubbles: true, detail: 0, shiftKey: true })),
|
|
323
|
+
);
|
|
324
|
+
expect(onShiftClickKeyframe).toHaveBeenCalledWith(
|
|
325
|
+
"clip-1",
|
|
326
|
+
expect.objectContaining({ percentage: 50 }),
|
|
327
|
+
);
|
|
328
|
+
expect(onClickKeyframe).not.toHaveBeenCalled();
|
|
329
|
+
act(() => root.unmount());
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("publishes retime previews after StrictMode effect replay", () => {
|
|
333
|
+
const { diamond, host, root } = renderRetimeLane(undefined, true);
|
|
334
|
+
const initialLeft = diamond.style.left;
|
|
335
|
+
act(() => {
|
|
336
|
+
diamond.dispatchEvent(
|
|
337
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
338
|
+
);
|
|
339
|
+
window.dispatchEvent(
|
|
340
|
+
pointerEvent("pointermove", { bubbles: true, clientX: 120, pointerId: 7 }),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
expect(host.querySelector<HTMLButtonElement>('button[title="50%"]')?.style.left).not.toBe(
|
|
344
|
+
initialLeft,
|
|
345
|
+
);
|
|
346
|
+
act(() => root.unmount());
|
|
347
|
+
});
|
|
348
|
+
|
|
164
349
|
it("treats primary pointerup without drag as a keyframe click", () => {
|
|
165
350
|
const { host, root, onClickKeyframe } = renderDiamonds();
|
|
166
351
|
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]');
|
|
@@ -246,8 +431,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
246
431
|
it("treats a drag-armed press that resolves to a no-op move as a click", () => {
|
|
247
432
|
const onClickKeyframe = vi.fn();
|
|
248
433
|
const onMoveKeyframe = vi.fn();
|
|
249
|
-
const host =
|
|
250
|
-
document.body.append(host);
|
|
434
|
+
const host = createTimelineHost();
|
|
251
435
|
const root = createRoot(host);
|
|
252
436
|
act(() => {
|
|
253
437
|
root.render(
|
|
@@ -314,8 +498,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
314
498
|
it("reselects a retimed keyframe with its post-move tween percentage", () => {
|
|
315
499
|
const onClickKeyframe = vi.fn();
|
|
316
500
|
const onMoveKeyframe = vi.fn().mockResolvedValue(true);
|
|
317
|
-
const host =
|
|
318
|
-
document.body.append(host);
|
|
501
|
+
const host = createTimelineHost();
|
|
319
502
|
const root = createRoot(host);
|
|
320
503
|
act(() => {
|
|
321
504
|
root.render(
|
|
@@ -474,8 +657,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
474
657
|
|
|
475
658
|
it("composes a rapid second retime from the pending position", () => {
|
|
476
659
|
const onMoveKeyframe = vi.fn().mockResolvedValue(true);
|
|
477
|
-
const host =
|
|
478
|
-
document.body.append(host);
|
|
660
|
+
const host = createTimelineHost();
|
|
479
661
|
const root = createRoot(host);
|
|
480
662
|
act(() => {
|
|
481
663
|
root.render(
|
|
@@ -548,16 +730,120 @@ describe("TimelineClipDiamonds", () => {
|
|
|
548
730
|
},
|
|
549
731
|
85,
|
|
550
732
|
);
|
|
733
|
+
|
|
734
|
+
act(() => {
|
|
735
|
+
usePlayerStore.setState({ timelineSessionEpoch: 1 });
|
|
736
|
+
diamond!.dispatchEvent(
|
|
737
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100 }),
|
|
738
|
+
);
|
|
739
|
+
diamond!.dispatchEvent(pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120 }));
|
|
740
|
+
});
|
|
741
|
+
expect(onMoveKeyframe).toHaveBeenNthCalledWith(
|
|
742
|
+
3,
|
|
743
|
+
expect.objectContaining({ percentage: 50 }),
|
|
744
|
+
60,
|
|
745
|
+
);
|
|
551
746
|
act(() => root.unmount());
|
|
552
747
|
});
|
|
553
748
|
|
|
749
|
+
it("preserves another element's pending retime when the active source is removed", () => {
|
|
750
|
+
const host = createTimelineHost();
|
|
751
|
+
const elementA = { ...RETIME_ELEMENT, id: "clip-a", key: "clip-a" };
|
|
752
|
+
const elementB = { ...RETIME_ELEMENT, id: "clip-b", key: "clip-b" };
|
|
753
|
+
usePlayerStore.setState({ elements: [elementA, elementB] });
|
|
754
|
+
const laneA = renderRetimeLane(vi.fn().mockResolvedValue(true), false, {
|
|
755
|
+
elementId: "clip-a",
|
|
756
|
+
host,
|
|
757
|
+
});
|
|
758
|
+
const onMoveB = vi.fn().mockResolvedValue(true);
|
|
759
|
+
const laneB = renderRetimeLane(onMoveB, false, { elementId: "clip-b", host });
|
|
760
|
+
|
|
761
|
+
act(() => {
|
|
762
|
+
laneB.diamond.dispatchEvent(
|
|
763
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
764
|
+
);
|
|
765
|
+
window.dispatchEvent(
|
|
766
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 150, pointerId: 7 }),
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
laneA.diamond.dispatchEvent(
|
|
770
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
771
|
+
);
|
|
772
|
+
usePlayerStore.setState({ elements: [elementB] });
|
|
773
|
+
|
|
774
|
+
laneB.diamond.dispatchEvent(
|
|
775
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 150, pointerId: 11 }),
|
|
776
|
+
);
|
|
777
|
+
window.dispatchEvent(
|
|
778
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 170, pointerId: 11 }),
|
|
779
|
+
);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
expect(onMoveB).toHaveBeenNthCalledWith(2, expect.objectContaining({ percentage: 75 }), 85);
|
|
783
|
+
act(() => {
|
|
784
|
+
laneA.root.unmount();
|
|
785
|
+
laneB.root.unmount();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("retires a pending retime once the cache exposes its destination identity", () => {
|
|
790
|
+
const first = renderRetimeLane();
|
|
791
|
+
act(() => {
|
|
792
|
+
first.diamond.dispatchEvent(
|
|
793
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
794
|
+
);
|
|
795
|
+
window.dispatchEvent(
|
|
796
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 150, pointerId: 7 }),
|
|
797
|
+
);
|
|
798
|
+
});
|
|
799
|
+
expect(readPendingTimelineKeyframeRetimes(first.host).size).toBe(1);
|
|
800
|
+
act(() => first.root.unmount());
|
|
801
|
+
|
|
802
|
+
const destination = renderRetimeLane(vi.fn().mockResolvedValue(true), false, {
|
|
803
|
+
host: first.host,
|
|
804
|
+
keyframes: [
|
|
805
|
+
{
|
|
806
|
+
percentage: 0,
|
|
807
|
+
tweenPercentage: 0,
|
|
808
|
+
propertyGroup: "position",
|
|
809
|
+
animationId: "anim-1",
|
|
810
|
+
properties: { x: 0 },
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
percentage: 75,
|
|
814
|
+
tweenPercentage: 75,
|
|
815
|
+
propertyGroup: "position",
|
|
816
|
+
animationId: "anim-1",
|
|
817
|
+
properties: { x: 100 },
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
percentage: 100,
|
|
821
|
+
tweenPercentage: 100,
|
|
822
|
+
propertyGroup: "position",
|
|
823
|
+
animationId: "anim-1",
|
|
824
|
+
properties: { x: 200 },
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
});
|
|
828
|
+
act(() => {
|
|
829
|
+
destination.diamond.dispatchEvent(
|
|
830
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 150, pointerId: 9 }),
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
expect(readPendingTimelineKeyframeRetimes(first.host).size).toBe(0);
|
|
834
|
+
|
|
835
|
+
act(() => {
|
|
836
|
+
window.dispatchEvent(pointerEvent("pointercancel", { bubbles: true, pointerId: 9 }));
|
|
837
|
+
destination.root.unmount();
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
554
841
|
it.each([
|
|
555
842
|
["returns false", () => Promise.resolve(false)],
|
|
556
843
|
["rejects", () => Promise.reject(new Error("retime failed"))],
|
|
557
844
|
])("clears a failed pending retime when the callback %s", async (_label, settle) => {
|
|
558
845
|
const onMoveKeyframe = vi.fn().mockImplementationOnce(settle).mockResolvedValue(true);
|
|
559
|
-
const host =
|
|
560
|
-
document.body.append(host);
|
|
846
|
+
const host = createTimelineHost();
|
|
561
847
|
const root = createRoot(host);
|
|
562
848
|
act(() => {
|
|
563
849
|
root.render(
|
|
@@ -678,17 +964,224 @@ describe("TimelineClipDiamonds", () => {
|
|
|
678
964
|
act(() => root.unmount());
|
|
679
965
|
});
|
|
680
966
|
|
|
967
|
+
it("commits once from the stable viewport after the source lane unmounts", () => {
|
|
968
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
969
|
+
|
|
970
|
+
act(() => {
|
|
971
|
+
diamond.dispatchEvent(
|
|
972
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
973
|
+
);
|
|
974
|
+
root.unmount();
|
|
975
|
+
window.dispatchEvent(
|
|
976
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
977
|
+
);
|
|
978
|
+
window.dispatchEvent(
|
|
979
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 140, pointerId: 7 }),
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(
|
|
984
|
+
{
|
|
985
|
+
percentage: 50,
|
|
986
|
+
tweenPercentage: 50,
|
|
987
|
+
propertyGroup: "position",
|
|
988
|
+
animationId: "anim-1",
|
|
989
|
+
},
|
|
990
|
+
60,
|
|
991
|
+
);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("keeps the retime preview coherent when the source lane remounts", () => {
|
|
995
|
+
const { diamond, mountLane, onMoveKeyframe, root } = renderRetimeLane();
|
|
996
|
+
|
|
997
|
+
act(() => {
|
|
998
|
+
diamond.dispatchEvent(
|
|
999
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1000
|
+
);
|
|
1001
|
+
window.dispatchEvent(
|
|
1002
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1003
|
+
);
|
|
1004
|
+
});
|
|
1005
|
+
const beforeUnmountLeft = Number.parseFloat(diamond.style.left);
|
|
1006
|
+
|
|
1007
|
+
act(() => {
|
|
1008
|
+
root.unmount();
|
|
1009
|
+
window.dispatchEvent(
|
|
1010
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 140, pointerId: 7 }),
|
|
1011
|
+
);
|
|
1012
|
+
});
|
|
1013
|
+
const { diamond: remountedDiamond, root: remountedRoot } = mountLane();
|
|
1014
|
+
const remountedLeft = Number.parseFloat(remountedDiamond.style.left);
|
|
1015
|
+
expect(remountedLeft).toBeGreaterThan(beforeUnmountLeft);
|
|
1016
|
+
|
|
1017
|
+
act(() => {
|
|
1018
|
+
window.dispatchEvent(
|
|
1019
|
+
pointerEvent("pointermove", { bubbles: true, button: 0, clientX: 160, pointerId: 7 }),
|
|
1020
|
+
);
|
|
1021
|
+
});
|
|
1022
|
+
expect(Number.parseFloat(remountedDiamond.style.left)).toBeGreaterThan(remountedLeft);
|
|
1023
|
+
|
|
1024
|
+
act(() => {
|
|
1025
|
+
window.dispatchEvent(
|
|
1026
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 160, pointerId: 7 }),
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(expect.any(Object), 80);
|
|
1030
|
+
act(() => remountedRoot.unmount());
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it("includes horizontal viewport scrolling in the retime destination", () => {
|
|
1034
|
+
const { diamond, host, onMoveKeyframe, root } = renderRetimeLane();
|
|
1035
|
+
|
|
1036
|
+
act(() => {
|
|
1037
|
+
diamond.dispatchEvent(
|
|
1038
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1039
|
+
);
|
|
1040
|
+
host.scrollLeft = 20;
|
|
1041
|
+
window.dispatchEvent(
|
|
1042
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
expect(onMoveKeyframe).toHaveBeenCalledExactlyOnceWith(expect.any(Object), 60);
|
|
1047
|
+
act(() => root.unmount());
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("auto-scrolls horizontally without virtualizing the source row away", () => {
|
|
1051
|
+
let frame: FrameRequestCallback | null = null;
|
|
1052
|
+
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((callback) => {
|
|
1053
|
+
frame = callback;
|
|
1054
|
+
return 1;
|
|
1055
|
+
});
|
|
1056
|
+
vi.spyOn(globalThis, "cancelAnimationFrame").mockImplementation(() => undefined);
|
|
1057
|
+
const { diamond, host, root } = renderRetimeLane();
|
|
1058
|
+
configureTimelineTestViewport(host, 10_000);
|
|
1059
|
+
|
|
1060
|
+
act(() => {
|
|
1061
|
+
diamond.dispatchEvent(
|
|
1062
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1063
|
+
);
|
|
1064
|
+
window.dispatchEvent(
|
|
1065
|
+
pointerEvent("pointermove", {
|
|
1066
|
+
bubbles: true,
|
|
1067
|
+
button: 0,
|
|
1068
|
+
clientX: 790,
|
|
1069
|
+
clientY: 239,
|
|
1070
|
+
pointerId: 7,
|
|
1071
|
+
}),
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
expect(frame).not.toBeNull();
|
|
1075
|
+
act(() => frame?.(0));
|
|
1076
|
+
expect(host.scrollLeft).toBeGreaterThan(0);
|
|
1077
|
+
expect(host.scrollTop).toBe(0);
|
|
1078
|
+
|
|
1079
|
+
act(() => {
|
|
1080
|
+
window.dispatchEvent(pointerEvent("pointercancel", { bubbles: true, pointerId: 7 }));
|
|
1081
|
+
root.unmount();
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it("ignores another pointer and lets the owning pointer finish", () => {
|
|
1086
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1087
|
+
|
|
1088
|
+
act(() => {
|
|
1089
|
+
diamond.dispatchEvent(
|
|
1090
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1091
|
+
);
|
|
1092
|
+
window.dispatchEvent(
|
|
1093
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 140, pointerId: 8 }),
|
|
1094
|
+
);
|
|
1095
|
+
window.dispatchEvent(
|
|
1096
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1097
|
+
);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1101
|
+
act(() => root.unmount());
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("cancels without mutation on pointer cancel or Escape and allows the next retime", () => {
|
|
1105
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1106
|
+
|
|
1107
|
+
act(() => {
|
|
1108
|
+
diamond.dispatchEvent(
|
|
1109
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1110
|
+
);
|
|
1111
|
+
window.dispatchEvent(
|
|
1112
|
+
pointerEvent("pointercancel", {
|
|
1113
|
+
bubbles: true,
|
|
1114
|
+
button: 0,
|
|
1115
|
+
clientX: 120,
|
|
1116
|
+
pointerId: 7,
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
window.dispatchEvent(
|
|
1120
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
diamond.dispatchEvent(
|
|
1124
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
1125
|
+
);
|
|
1126
|
+
window.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
|
|
1127
|
+
window.dispatchEvent(
|
|
1128
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 9 }),
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
diamond.dispatchEvent(
|
|
1132
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 11 }),
|
|
1133
|
+
);
|
|
1134
|
+
window.dispatchEvent(
|
|
1135
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 11 }),
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1140
|
+
act(() => root.unmount());
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("cancels on project switch or source removal without poisoning the next gesture", () => {
|
|
1144
|
+
const { diamond, onMoveKeyframe, root } = renderRetimeLane();
|
|
1145
|
+
|
|
1146
|
+
act(() => {
|
|
1147
|
+
diamond.dispatchEvent(
|
|
1148
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 7 }),
|
|
1149
|
+
);
|
|
1150
|
+
usePlayerStore.setState({ timelineSessionEpoch: 1 });
|
|
1151
|
+
window.dispatchEvent(
|
|
1152
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 7 }),
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
diamond.dispatchEvent(
|
|
1156
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 9 }),
|
|
1157
|
+
);
|
|
1158
|
+
usePlayerStore.setState({ elements: [] });
|
|
1159
|
+
window.dispatchEvent(
|
|
1160
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 9 }),
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
usePlayerStore.setState({ elements: [RETIME_ELEMENT] });
|
|
1164
|
+
diamond.dispatchEvent(
|
|
1165
|
+
pointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100, pointerId: 11 }),
|
|
1166
|
+
);
|
|
1167
|
+
window.dispatchEvent(
|
|
1168
|
+
pointerEvent("pointerup", { bubbles: true, button: 0, clientX: 120, pointerId: 11 }),
|
|
1169
|
+
);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
expect(onMoveKeyframe).toHaveBeenCalledOnce();
|
|
1173
|
+
act(() => root.unmount());
|
|
1174
|
+
});
|
|
1175
|
+
|
|
681
1176
|
// Regression: onClickKeyframe's state updates can re-render the diamond
|
|
682
1177
|
// button out from under the gesture before the browser auto-synthesizes the
|
|
683
1178
|
// "click" event that follows a button's pointerdown+pointerup. That orphaned
|
|
684
|
-
// click then bubbles to the ancestor clip's onClick
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// own clip. suppressClickRef lets that ancestor ignore the stray click.
|
|
1179
|
+
// click then bubbles to the ancestor clip's onClick. That stray click can
|
|
1180
|
+
// replace keyframe focus or collapse a marquee selection, so suppressClickRef
|
|
1181
|
+
// lets the ancestor ignore it.
|
|
688
1182
|
it("arms suppressClickRef synchronously on a keyframe click", () => {
|
|
689
1183
|
const suppressClickRef = { current: false };
|
|
690
|
-
const host =
|
|
691
|
-
document.body.append(host);
|
|
1184
|
+
const host = createTimelineHost();
|
|
692
1185
|
const root = createRoot(host);
|
|
693
1186
|
act(() => {
|
|
694
1187
|
root.render(
|
|
@@ -722,9 +1215,9 @@ describe("TimelineClipDiamonds", () => {
|
|
|
722
1215
|
});
|
|
723
1216
|
|
|
724
1217
|
const renderSegmentLane = (lastAmbiguous: boolean, clipWidthPx = 200) => {
|
|
725
|
-
const host =
|
|
726
|
-
document.body.append(host);
|
|
1218
|
+
const host = createTimelineHost();
|
|
727
1219
|
const root = createRoot(host);
|
|
1220
|
+
const onSelectSegment = vi.fn();
|
|
728
1221
|
const kf = (percentage: number, extra: Record<string, unknown> = {}) => ({
|
|
729
1222
|
percentage,
|
|
730
1223
|
tweenPercentage: percentage,
|
|
@@ -757,13 +1250,15 @@ describe("TimelineClipDiamonds", () => {
|
|
|
757
1250
|
isSelected
|
|
758
1251
|
currentPercentage={0}
|
|
759
1252
|
elementId="clip-1"
|
|
1253
|
+
clipStart={10}
|
|
1254
|
+
clipDuration={10}
|
|
760
1255
|
selectedKeyframes={new Set()}
|
|
761
|
-
onSelectSegment={
|
|
1256
|
+
onSelectSegment={onSelectSegment}
|
|
762
1257
|
groupAware
|
|
763
1258
|
/>,
|
|
764
1259
|
);
|
|
765
1260
|
});
|
|
766
|
-
return { host, root };
|
|
1261
|
+
return { host, onSelectSegment, root };
|
|
767
1262
|
};
|
|
768
1263
|
|
|
769
1264
|
it("shows the inline ease button on a colliding merged segment (bulk edit)", () => {
|
|
@@ -776,8 +1271,23 @@ describe("TimelineClipDiamonds", () => {
|
|
|
776
1271
|
});
|
|
777
1272
|
|
|
778
1273
|
it("shows the inline ease button on single-animation merged segments", () => {
|
|
779
|
-
const { host, root } = renderSegmentLane(false);
|
|
1274
|
+
const { host, onSelectSegment, root } = renderSegmentLane(false);
|
|
780
1275
|
expect(host.querySelectorAll("[data-keyframe-ease-segment]").length).toBe(2);
|
|
1276
|
+
const ease = host.querySelector<HTMLButtonElement>("[data-keyframe-ease-button]")!;
|
|
1277
|
+
expect(ease.getAttribute("aria-label")).toBe("Edit none easing after 10s");
|
|
1278
|
+
expect(ease.classList.contains("opacity-0")).toBe(true);
|
|
1279
|
+
act(() => ease.click());
|
|
1280
|
+
expect(onSelectSegment).toHaveBeenCalledOnce();
|
|
1281
|
+
expect(usePlayerStore.getState().requestedSeekTime).toBeNull();
|
|
1282
|
+
act(() => root.unmount());
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
it("ends connectors at the diamond boundaries", () => {
|
|
1286
|
+
const { host, root } = renderSegmentLane(false);
|
|
1287
|
+
const connectors = host.querySelectorAll<HTMLElement>("[data-keyframe-connector]");
|
|
1288
|
+
|
|
1289
|
+
expect(Array.from(connectors, (connector) => connector.style.left)).toEqual(["11px", "111px"]);
|
|
1290
|
+
expect(Array.from(connectors, (connector) => connector.style.width)).toEqual(["78px", "78px"]);
|
|
781
1291
|
act(() => root.unmount());
|
|
782
1292
|
});
|
|
783
1293
|
|
|
@@ -811,8 +1321,7 @@ describe("TimelineClipDiamonds", () => {
|
|
|
811
1321
|
it("hides the inline ease button on a segment with no source animation id", () => {
|
|
812
1322
|
// A runtime-scanned keyframe has no animationId, so there is no tween to
|
|
813
1323
|
// target; the segment ending on it must not render a (dead) ease button.
|
|
814
|
-
const host =
|
|
815
|
-
document.body.append(host);
|
|
1324
|
+
const host = createTimelineHost();
|
|
816
1325
|
const root = createRoot(host);
|
|
817
1326
|
const kf = (percentage: number, animationId?: string) => ({
|
|
818
1327
|
percentage,
|