@hyperframes/studio 0.8.105 → 0.8.107
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-tnfCiVdR.js → hyperframes-player-h4mktSdm.js} +1 -1
- package/dist/assets/index-CM1xA_jD.js +394 -0
- package/dist/assets/{index-Ca6LZlVb.js → index-D8GKc2D9.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-zpe332hb.js → index-zoqPmBF6.js} +1 -1
- package/dist/{chunk-QADHJFDF.js → chunk-4OZC426H.js} +13 -3
- package/dist/chunk-4OZC426H.js.map +1 -0
- package/dist/index.d.ts +381 -14
- package/dist/index.html +11 -2
- package/dist/index.js +9974 -9045
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -9
- package/src/App.tsx +5 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +12 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/StudioHeader.dom.test.tsx +31 -9
- package/src/components/StudioHeader.tsx +4 -1
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/DomEditOverlay.tsx +31 -5
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/domEditOverlayStartGesture.ts +132 -117
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +338 -0
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +58 -2
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +4 -1
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/mediaAudioEdits.test.ts +212 -0
- package/src/components/editor/mediaAudioEdits.ts +253 -0
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +286 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +69 -46
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +165 -2
- package/src/components/editor/useDomEditNudge.ts +43 -25
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +49 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +4 -5
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useClipboard.source.test.ts +99 -19
- package/src/hooks/useClipboard.ts +145 -124
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +9 -46
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +122 -3
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +21 -5
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +8 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +9 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +14 -6
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +16 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/studio.css +6 -0
- package/src/styles/theme.css +10 -0
- package/src/utils/clipboardPayload.ts +10 -1
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +63 -0
- package/src/utils/studioPendingEdits.ts +37 -7
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +21 -1
- package/src/utils/typingTarget.ts +8 -0
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/assets/index-Cre9X4RS.css +0 -1
- package/dist/assets/index-DEDhPmPD.js +0 -399
- package/dist/chunk-QADHJFDF.js.map +0 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import type
|
|
7
|
-
import { TimelineClipFades } from "./TimelineClipFades";
|
|
6
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
7
|
+
import { TimelineClipFades, useClipFadeDraft } from "./TimelineClipFades";
|
|
8
8
|
import { TimelineEditProvider } from "../../contexts/TimelineEditContext";
|
|
9
9
|
|
|
10
10
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -14,8 +14,15 @@ const mounted: Root[] = [];
|
|
|
14
14
|
afterEach(() => {
|
|
15
15
|
act(() => mounted.splice(0).forEach((root) => root.unmount()));
|
|
16
16
|
document.body.innerHTML = "";
|
|
17
|
+
usePlayerStore.setState({ currentTime: 0, elements: [], timelineSnapEnabled: true });
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
/** TimelineClip owns the draft; this stands in for it. */
|
|
21
|
+
function Fades(props: { el: TimelineElement; showHandles: boolean; focusable?: boolean }) {
|
|
22
|
+
const fade = useClipFadeDraft(props.el);
|
|
23
|
+
return <TimelineClipFades {...props} pps={100} widthPx={props.el.duration * 100} fade={fade} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
const clip: TimelineElement = {
|
|
20
27
|
id: "music",
|
|
21
28
|
tag: "audio",
|
|
@@ -29,7 +36,12 @@ const clip: TimelineElement = {
|
|
|
29
36
|
|
|
30
37
|
function render(
|
|
31
38
|
el: TimelineElement,
|
|
32
|
-
options: {
|
|
39
|
+
options: {
|
|
40
|
+
showHandles?: boolean;
|
|
41
|
+
provide?: boolean;
|
|
42
|
+
focusable?: boolean;
|
|
43
|
+
onClipPointerDown?: () => void;
|
|
44
|
+
} = {},
|
|
33
45
|
) {
|
|
34
46
|
const host = document.createElement("div");
|
|
35
47
|
document.body.append(host);
|
|
@@ -39,36 +51,30 @@ function render(
|
|
|
39
51
|
const onSetElementAttributeQuiet = vi.fn().mockResolvedValue(undefined);
|
|
40
52
|
const onRevertElementAttributeLive = vi.fn();
|
|
41
53
|
// Stands in for the TimelineClip button the handles live inside.
|
|
42
|
-
const
|
|
54
|
+
const tree = (el: TimelineElement) => (
|
|
43
55
|
<div data-testid="clip" onPointerDown={options.onClipPointerDown}>
|
|
44
|
-
<
|
|
45
|
-
el={el}
|
|
46
|
-
pps={100}
|
|
47
|
-
widthPx={el.duration * 100}
|
|
48
|
-
showHandles={options.showHandles ?? true}
|
|
49
|
-
/>
|
|
56
|
+
<Fades el={el} showHandles={options.showHandles ?? true} focusable={options.focusable} />
|
|
50
57
|
</div>
|
|
51
58
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</TimelineEditProvider>
|
|
66
|
-
),
|
|
59
|
+
const wrap = (el: TimelineElement) =>
|
|
60
|
+
options.provide === false ? (
|
|
61
|
+
tree(el)
|
|
62
|
+
) : (
|
|
63
|
+
<TimelineEditProvider
|
|
64
|
+
value={{
|
|
65
|
+
onSetElementAttributeLive,
|
|
66
|
+
onSetElementAttributeQuiet,
|
|
67
|
+
onRevertElementAttributeLive,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{tree(el)}
|
|
71
|
+
</TimelineEditProvider>
|
|
67
72
|
);
|
|
68
|
-
|
|
73
|
+
act(() => root.render(wrap(el)));
|
|
69
74
|
return {
|
|
70
75
|
host,
|
|
71
76
|
root,
|
|
77
|
+
rerender: (next: TimelineElement) => act(() => root.render(wrap(next))),
|
|
72
78
|
onSetElementAttributeLive,
|
|
73
79
|
onSetElementAttributeQuiet,
|
|
74
80
|
onRevertElementAttributeLive,
|
|
@@ -81,6 +87,19 @@ function pointer(type: string, clientX: number, pointerId = 1, clientY = 0) {
|
|
|
81
87
|
return event;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** Pointer down at the first point, then a move to each later one; [clientX, pointerId?]. */
|
|
91
|
+
function press(handle: HTMLElement, down: [number, number?], ...moves: Array<[number, number?]>) {
|
|
92
|
+
act(() => handle.dispatchEvent(pointer("pointerdown", ...down)));
|
|
93
|
+
for (const move of moves) act(() => handle.dispatchEvent(pointer("pointermove", ...move)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function armedHandle(host: HTMLElement, edge: "in" | "out") {
|
|
97
|
+
const handle = host.querySelector<HTMLElement>(`[data-testid="clip-fade-handle-${edge}"]`);
|
|
98
|
+
if (!handle) throw new Error(`expected a fade-${edge} handle`);
|
|
99
|
+
armCapture(handle);
|
|
100
|
+
return handle;
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
function armCapture(handle: HTMLElement) {
|
|
85
104
|
let captured = false;
|
|
86
105
|
Object.defineProperty(handle, "setPointerCapture", { value: () => (captured = true) });
|
|
@@ -122,13 +141,9 @@ describe("TimelineClipFades", () => {
|
|
|
122
141
|
|
|
123
142
|
it("drags the fade-in dot to the right, previewing live and committing once on release", () => {
|
|
124
143
|
const { host, root, onSetElementAttributeLive, onSetElementAttributeQuiet } = render(clip);
|
|
125
|
-
const handle = host
|
|
126
|
-
if (!handle) throw new Error("expected a fade-in handle");
|
|
127
|
-
armCapture(handle);
|
|
144
|
+
const handle = armedHandle(host, "in");
|
|
128
145
|
// 150 px of rightward travel adds 1.5 s to the 1 s fade-in, wherever the press lands.
|
|
129
|
-
|
|
130
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 200)));
|
|
131
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 250)));
|
|
146
|
+
press(handle, [100], [200], [250]);
|
|
132
147
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "2.5");
|
|
133
148
|
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
134
149
|
// The ramp follows the pointer before anything is persisted.
|
|
@@ -143,12 +158,9 @@ describe("TimelineClipFades", () => {
|
|
|
143
158
|
|
|
144
159
|
it("drags the fade-out dot to the left, growing the fade, and never past the fade-in", () => {
|
|
145
160
|
const { host, root, onSetElementAttributeLive, onSetElementAttributeQuiet } = render(clip);
|
|
146
|
-
const handle = host
|
|
147
|
-
if (!handle) throw new Error("expected a fade-out handle");
|
|
148
|
-
armCapture(handle);
|
|
161
|
+
const handle = armedHandle(host, "out");
|
|
149
162
|
// 300 px of leftward travel adds 3 s to the 2 s fade-out, wherever the press lands.
|
|
150
|
-
|
|
151
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 700)));
|
|
163
|
+
press(handle, [1000], [700]);
|
|
152
164
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-out", "5");
|
|
153
165
|
// Way past the start: clamps to duration − fadeIn = 9 s.
|
|
154
166
|
act(() => handle.dispatchEvent(pointer("pointermove", 0)));
|
|
@@ -160,11 +172,8 @@ describe("TimelineClipFades", () => {
|
|
|
160
172
|
|
|
161
173
|
it("removes the attribute when dragged back to zero", () => {
|
|
162
174
|
const { host, root, onSetElementAttributeQuiet } = render(clip);
|
|
163
|
-
const handle = host
|
|
164
|
-
|
|
165
|
-
armCapture(handle);
|
|
166
|
-
act(() => handle.dispatchEvent(pointer("pointerdown", 200)));
|
|
167
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 0)));
|
|
175
|
+
const handle = armedHandle(host, "in");
|
|
176
|
+
press(handle, [200], [0]);
|
|
168
177
|
act(() => handle.dispatchEvent(pointer("pointerup", 0)));
|
|
169
178
|
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(clip, "data-fade-in", null, "Fade in");
|
|
170
179
|
act(() => root.unmount());
|
|
@@ -178,11 +187,8 @@ describe("TimelineClipFades", () => {
|
|
|
178
187
|
onRevertElementAttributeLive,
|
|
179
188
|
onSetElementAttributeQuiet,
|
|
180
189
|
} = render(clip);
|
|
181
|
-
const handle = host
|
|
182
|
-
|
|
183
|
-
armCapture(handle);
|
|
184
|
-
act(() => handle.dispatchEvent(pointer("pointerdown", 100)));
|
|
185
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 300)));
|
|
190
|
+
const handle = armedHandle(host, "in");
|
|
191
|
+
press(handle, [100], [300]);
|
|
186
192
|
act(() => handle.dispatchEvent(pointer("pointercancel", 300)));
|
|
187
193
|
// Written back live for a host without the revert, then ended through the lanes' revert.
|
|
188
194
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "1");
|
|
@@ -196,11 +202,8 @@ describe("TimelineClipFades", () => {
|
|
|
196
202
|
const elsewhere = document.createElement("button");
|
|
197
203
|
document.body.append(elsewhere);
|
|
198
204
|
elsewhere.focus();
|
|
199
|
-
const handle = host
|
|
200
|
-
|
|
201
|
-
armCapture(handle);
|
|
202
|
-
act(() => handle.dispatchEvent(pointer("pointerdown", 100)));
|
|
203
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 300)));
|
|
205
|
+
const handle = armedHandle(host, "in");
|
|
206
|
+
press(handle, [100], [300]);
|
|
204
207
|
expect(document.activeElement).toBe(elsewhere);
|
|
205
208
|
act(() => {
|
|
206
209
|
document.activeElement?.dispatchEvent(
|
|
@@ -221,11 +224,8 @@ describe("TimelineClipFades", () => {
|
|
|
221
224
|
])("restores the fade and saves nothing when released %s", (_, clientX, clientY) => {
|
|
222
225
|
const { host, root, onSetElementAttributeLive, onRevertElementAttributeLive, ...rest } =
|
|
223
226
|
render(clip);
|
|
224
|
-
const handle = host
|
|
225
|
-
|
|
226
|
-
armCapture(handle);
|
|
227
|
-
act(() => handle.dispatchEvent(pointer("pointerdown", 100)));
|
|
228
|
-
act(() => handle.dispatchEvent(pointer("pointermove", 300)));
|
|
227
|
+
const handle = armedHandle(host, "in");
|
|
228
|
+
press(handle, [100], [300]);
|
|
229
229
|
act(() => handle.dispatchEvent(pointer("pointerup", clientX, 1, clientY)));
|
|
230
230
|
expect(rest.onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
231
231
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "1");
|
|
@@ -257,12 +257,28 @@ describe("TimelineClipFades", () => {
|
|
|
257
257
|
},
|
|
258
258
|
);
|
|
259
259
|
|
|
260
|
-
it.each([
|
|
260
|
+
it.each([
|
|
261
|
+
[0.3, "18px"],
|
|
262
|
+
[3, "288px"],
|
|
263
|
+
])("puts the fade-in tab at the end of a %s s fade", (fadeIn, left) => {
|
|
261
264
|
const { host } = render({ ...clip, fadeIn, fadeOut: 0.4 });
|
|
262
265
|
const inHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
263
|
-
const
|
|
266
|
+
const outHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-out"]');
|
|
267
|
+
// The 24 px box centres on the fade's end: 0.4 s out of a 1000 px clip ends at 960.
|
|
268
|
+
expect(inHandle?.style.left).toBe(left);
|
|
269
|
+
expect(outHandle?.style.left).toBe("948px");
|
|
270
|
+
const tab = inHandle?.firstElementChild as HTMLElement;
|
|
271
|
+
expect(tab.style.left).toBe("10px");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("keeps the tab off the rounded end when there is no fade", () => {
|
|
275
|
+
const { host } = render({ ...clip, fadeIn: undefined, fadeOut: undefined });
|
|
276
|
+
const inHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
277
|
+
const outHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-out"]');
|
|
264
278
|
expect(inHandle?.style.left).toBe("0px");
|
|
265
|
-
expect(
|
|
279
|
+
expect(outHandle?.style.left).toBe("976px");
|
|
280
|
+
// The tab itself sits 7 px in from each end: 5 px is its left edge, 2 px either side of 7.
|
|
281
|
+
expect(inHandle?.querySelector<HTMLElement>(".timeline-fade-tab")?.style.left).toBe("5px");
|
|
266
282
|
});
|
|
267
283
|
|
|
268
284
|
it("lengthens a fade by the pointer's inward travel from wherever the press lands", () => {
|
|
@@ -272,19 +288,17 @@ describe("TimelineClipFades", () => {
|
|
|
272
288
|
if (!fadeIn || !fadeOut) throw new Error("expected both handles");
|
|
273
289
|
armCapture(fadeIn);
|
|
274
290
|
armCapture(fadeOut);
|
|
275
|
-
|
|
276
|
-
act(() => fadeIn.dispatchEvent(pointer("pointermove", 162)));
|
|
291
|
+
press(fadeIn, [12], [162]);
|
|
277
292
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "2.5");
|
|
278
293
|
act(() => fadeIn.dispatchEvent(pointer("pointerup", 162)));
|
|
279
|
-
|
|
280
|
-
act(() => fadeOut.dispatchEvent(pointer("pointermove", 888, 2)));
|
|
294
|
+
press(fadeOut, [988, 2], [888, 2]);
|
|
281
295
|
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-out", "3");
|
|
282
296
|
});
|
|
283
297
|
|
|
284
298
|
it("paints the handles above the clip's waveform and thumbnail layers", () => {
|
|
285
299
|
const { host } = render(clip);
|
|
286
|
-
const
|
|
287
|
-
expect(Number(
|
|
300
|
+
const layer = host.querySelector<HTMLElement>('[data-testid="clip"]')?.firstElementChild;
|
|
301
|
+
expect(Number((layer as HTMLElement).style.zIndex)).toBeGreaterThan(10);
|
|
288
302
|
});
|
|
289
303
|
|
|
290
304
|
it("splits a clip narrower than two targets between the handles", () => {
|
|
@@ -293,19 +307,307 @@ describe("TimelineClipFades", () => {
|
|
|
293
307
|
const fadeOut = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-out"]');
|
|
294
308
|
expect([fadeIn?.style.left, fadeIn?.style.width]).toEqual(["0px", "15px"]);
|
|
295
309
|
expect([fadeOut?.style.left, fadeOut?.style.width]).toEqual(["15px", "15px"]);
|
|
296
|
-
const
|
|
297
|
-
expect(
|
|
298
|
-
// Layout is not real here; tests/e2e/fade-handles.mjs measures the
|
|
299
|
-
expect(
|
|
300
|
-
|
|
310
|
+
const tab = fadeIn?.firstElementChild as HTMLElement;
|
|
311
|
+
expect([tab.style.width, tab.style.height]).toEqual(["4px", "15px"]);
|
|
312
|
+
// Layout is not real here; tests/e2e/fade-handles.mjs measures the tab in Chrome.
|
|
313
|
+
expect(tab.style.pointerEvents).toBe("none");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("snaps the fade's end to the playhead within the timeline's snap radius", () => {
|
|
317
|
+
usePlayerStore.setState({ currentTime: 4.5 });
|
|
318
|
+
const { host, onSetElementAttributeLive } = render(clip);
|
|
319
|
+
const handle = armedHandle(host, "in");
|
|
320
|
+
// 146 px makes a 2.46 s fade ending at 4.46 s, 4 px from the playhead: it lands on 4.5.
|
|
321
|
+
press(handle, [100], [246]);
|
|
322
|
+
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "2.5");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("leaves the fade unsnapped when the timeline's snapping is off", () => {
|
|
326
|
+
usePlayerStore.setState({ currentTime: 4.5, timelineSnapEnabled: false });
|
|
327
|
+
const { host, onSetElementAttributeLive } = render(clip);
|
|
328
|
+
const handle = armedHandle(host, "in");
|
|
329
|
+
press(handle, [100], [246]);
|
|
330
|
+
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "2.46");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("snaps to another clip's edge", () => {
|
|
334
|
+
usePlayerStore.setState({
|
|
335
|
+
elements: [clip, { id: "title", tag: "div", start: 6, duration: 2, track: 0 }],
|
|
336
|
+
});
|
|
337
|
+
const { host, onSetElementAttributeLive } = render(clip);
|
|
338
|
+
const handle = armedHandle(host, "out");
|
|
339
|
+
// The 2 s fade-out starts at 10 s; 395 px left moves that to 6.05 s, onto the title's start.
|
|
340
|
+
press(handle, [1000], [605]);
|
|
341
|
+
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-out", "6");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it.each([
|
|
345
|
+
[1, null],
|
|
346
|
+
[undefined, "0.5"],
|
|
347
|
+
])("double-click on a %s s fade-in saves %s", (fadeIn, saved) => {
|
|
348
|
+
const { host, onSetElementAttributeQuiet } = render({ ...clip, fadeIn });
|
|
349
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
350
|
+
act(() => handle?.dispatchEvent(new MouseEvent("dblclick", { bubbles: true })));
|
|
351
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledTimes(1);
|
|
352
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(
|
|
353
|
+
{ ...clip, fadeIn },
|
|
354
|
+
"data-fade-in",
|
|
355
|
+
saved,
|
|
356
|
+
"Fade in",
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const key = (handle: HTMLElement | null, type: "keydown" | "keyup", init: KeyboardEventInit) =>
|
|
361
|
+
act(() => {
|
|
362
|
+
handle?.dispatchEvent(new KeyboardEvent(type, { ...init, bubbles: true }));
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it.each([
|
|
366
|
+
[{ key: "ArrowRight" }, "1.1"],
|
|
367
|
+
[{ key: "ArrowRight", shiftKey: true }, "2"],
|
|
368
|
+
[{ key: "ArrowLeft" }, "0.9"],
|
|
369
|
+
[{ key: "Home" }, null],
|
|
370
|
+
[{ key: "Delete" }, null],
|
|
371
|
+
[{ key: "Backspace" }, null],
|
|
372
|
+
[{ key: "End" }, "8"],
|
|
373
|
+
])("saves a key press once it is released: %o", (init, saved) => {
|
|
374
|
+
const outer = vi.fn();
|
|
375
|
+
const { host, onSetElementAttributeQuiet } = render(clip, { focusable: true });
|
|
376
|
+
document.body.addEventListener("keydown", outer);
|
|
377
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
378
|
+
expect(handle?.tabIndex).toBe(0);
|
|
379
|
+
key(handle, "keydown", init);
|
|
380
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
381
|
+
key(handle, "keyup", init);
|
|
382
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledTimes(1);
|
|
383
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(clip, "data-fade-in", saved, "Fade in");
|
|
384
|
+
// The timeline's own arrow-key handling never sees a key the handle used.
|
|
385
|
+
expect(outer).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it.each(["ArrowRight", "End"])("saves nothing for %s on a lone fade past the clip's end", (k) => {
|
|
389
|
+
const long = { ...clip, duration: 3, fadeIn: 5, fadeOut: undefined };
|
|
390
|
+
const { host, onSetElementAttributeQuiet } = render(long, { focusable: true });
|
|
391
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
392
|
+
key(handle, "keydown", { key: k });
|
|
393
|
+
key(handle, "keyup", { key: k });
|
|
394
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("previews a held key live and saves it as one step", () => {
|
|
398
|
+
const { host, onSetElementAttributeLive, onSetElementAttributeQuiet } = render(clip, {
|
|
399
|
+
focusable: true,
|
|
400
|
+
});
|
|
401
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
402
|
+
for (const repeat of [false, true, true]) key(handle, "keydown", { key: "ArrowRight", repeat });
|
|
403
|
+
expect(onSetElementAttributeLive.mock.calls.map(([, , value]) => value)).toEqual([
|
|
404
|
+
"1.1",
|
|
405
|
+
"1.2",
|
|
406
|
+
"1.3",
|
|
407
|
+
]);
|
|
408
|
+
key(handle, "keyup", { key: "ArrowRight" });
|
|
409
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledTimes(1);
|
|
410
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(clip, "data-fade-in", "1.3", "Fade in");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("ignores keys while the handle is being dragged", () => {
|
|
414
|
+
const { host, onSetElementAttributeQuiet } = render(clip, { focusable: true });
|
|
415
|
+
const handle = armedHandle(host, "in");
|
|
416
|
+
press(handle, [100], [200]);
|
|
417
|
+
key(handle, "keydown", { key: "Home" });
|
|
418
|
+
key(handle, "keyup", { key: "Home" });
|
|
419
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("drops the draft when the save fails, leaving the live value to the save path", async () => {
|
|
423
|
+
const { host, onSetElementAttributeQuiet, onRevertElementAttributeLive } = render(clip);
|
|
424
|
+
onSetElementAttributeQuiet.mockResolvedValueOnce({ status: "failed", reason: "disk full" });
|
|
425
|
+
const handle = armedHandle(host, "in");
|
|
426
|
+
press(handle, [100], [300]);
|
|
427
|
+
act(() => handle.dispatchEvent(pointer("pointerup", 300)));
|
|
428
|
+
await act(async () => {});
|
|
429
|
+
expect(onRevertElementAttributeLive).not.toHaveBeenCalled();
|
|
430
|
+
expect(host.querySelector('[data-testid="clip-fade-in"]')?.getAttribute("points")).toBe(
|
|
431
|
+
"0,0 100,0 0,100",
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("steps from a pressed value whose save has not landed yet", () => {
|
|
436
|
+
const { host, onSetElementAttributeQuiet } = render(clip, { focusable: true });
|
|
437
|
+
onSetElementAttributeQuiet.mockReturnValue(new Promise(() => {}));
|
|
438
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
439
|
+
for (let tap = 0; tap < 2; tap++) {
|
|
440
|
+
key(handle, "keydown", { key: "ArrowRight" });
|
|
441
|
+
key(handle, "keyup", { key: "ArrowRight" });
|
|
442
|
+
}
|
|
443
|
+
expect(onSetElementAttributeQuiet.mock.calls.map(([, , value]) => value)).toEqual([
|
|
444
|
+
"1.1",
|
|
445
|
+
"1.2",
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("saves a held key before a press on the handle starts a drag", () => {
|
|
450
|
+
const { host, onSetElementAttributeQuiet } = render(clip, { focusable: true });
|
|
451
|
+
const handle = armedHandle(host, "in");
|
|
452
|
+
key(handle, "keydown", { key: "ArrowRight" });
|
|
453
|
+
press(handle, [100]);
|
|
454
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(clip, "data-fade-in", "1.1", "Fade in");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("keeps a handle drawn while it is dragged to where it can no longer move", () => {
|
|
458
|
+
// A fade-in that fills the clip next to a 2 s fade-out, as a trim leaves it.
|
|
459
|
+
const trimmed = { ...clip, fadeIn: 10, fadeOut: 2 };
|
|
460
|
+
const { host, onSetElementAttributeQuiet } = render(trimmed);
|
|
461
|
+
const handle = armedHandle(host, "out");
|
|
462
|
+
// 200 px right shrinks the 2 s fade-out to 0, where it has no room left to move.
|
|
463
|
+
press(handle, [800], [1000]);
|
|
464
|
+
act(() => handle.dispatchEvent(pointer("pointerup", 1000)));
|
|
465
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(
|
|
466
|
+
trimmed,
|
|
467
|
+
"data-fade-out",
|
|
468
|
+
null,
|
|
469
|
+
"Fade out",
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("hides a handle with less than one 0.01 s step to move", () => {
|
|
474
|
+
const { host } = render({ ...clip, duration: 10.0333, fadeIn: 10.03, fadeOut: undefined });
|
|
475
|
+
expect(host.querySelector('[data-testid="clip-fade-handle-out"]')).toBeNull();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("never lets a grow key shrink a fade when the pair overruns the clip", () => {
|
|
479
|
+
const overrun = { ...clip, fadeIn: 2, fadeOut: 10 };
|
|
480
|
+
const { host, onSetElementAttributeQuiet } = render(overrun, { focusable: true });
|
|
481
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
482
|
+
key(handle, "keydown", { key: "ArrowRight" });
|
|
483
|
+
key(handle, "keyup", { key: "ArrowRight" });
|
|
484
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("keeps stepping from each press while earlier saves land", async () => {
|
|
488
|
+
const saves: Array<() => void> = [];
|
|
489
|
+
const { host, onSetElementAttributeQuiet, rerender } = render(clip, { focusable: true });
|
|
490
|
+
onSetElementAttributeQuiet.mockImplementation(
|
|
491
|
+
() => new Promise<undefined>((resolve) => saves.push(() => resolve(undefined))),
|
|
492
|
+
);
|
|
493
|
+
const handle = () => host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
494
|
+
const tap = () => {
|
|
495
|
+
key(handle(), "keydown", { key: "ArrowRight" });
|
|
496
|
+
key(handle(), "keyup", { key: "ArrowRight" });
|
|
497
|
+
};
|
|
498
|
+
tap();
|
|
499
|
+
tap();
|
|
500
|
+
// The first save lands: the store re-reads 1.1 while 1.2 is still on its way.
|
|
501
|
+
rerender({ ...clip, fadeIn: 1.1 });
|
|
502
|
+
await act(async () => saves[0]());
|
|
503
|
+
tap();
|
|
504
|
+
expect(onSetElementAttributeQuiet.mock.calls.map(([, , value]) => value)).toEqual([
|
|
505
|
+
"1.1",
|
|
506
|
+
"1.2",
|
|
507
|
+
"1.3",
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("keeps a focused handle drawn when a key takes it to where it cannot move", () => {
|
|
512
|
+
const trimmed = { ...clip, fadeIn: 10, fadeOut: 2 };
|
|
513
|
+
const { host, onSetElementAttributeQuiet } = render(trimmed, { focusable: true });
|
|
514
|
+
const handle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-out"]');
|
|
515
|
+
act(() => handle?.focus());
|
|
516
|
+
key(handle, "keydown", { key: "Home" });
|
|
517
|
+
key(handle, "keyup", { key: "Home" });
|
|
518
|
+
expect(onSetElementAttributeQuiet).toHaveBeenCalledWith(
|
|
519
|
+
trimmed,
|
|
520
|
+
"data-fade-out",
|
|
521
|
+
null,
|
|
522
|
+
"Fade out",
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("never lets a grow drag shrink a fade when the pair overruns the clip", () => {
|
|
527
|
+
const trimmed = { ...clip, fadeIn: 10, fadeOut: 2 };
|
|
528
|
+
const { host, onSetElementAttributeLive, onSetElementAttributeQuiet } = render(trimmed);
|
|
529
|
+
const handle = armedHandle(host, "out");
|
|
530
|
+
// Leftward grows a fade-out; the fade-in already fills the clip, so it holds at 2 s.
|
|
531
|
+
press(handle, [900], [880]);
|
|
532
|
+
act(() => handle.dispatchEvent(pointer("pointerup", 880)));
|
|
533
|
+
expect(onSetElementAttributeLive).not.toHaveBeenCalledWith(trimmed, "data-fade-out", null);
|
|
534
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("keeps a pending save's draft through a click that never moves", () => {
|
|
538
|
+
const { host, onSetElementAttributeQuiet } = render({ ...clip, fadeIn: undefined });
|
|
539
|
+
onSetElementAttributeQuiet.mockReturnValue(new Promise(() => {}));
|
|
540
|
+
const handle = armedHandle(host, "in");
|
|
541
|
+
const doubleClick = () => {
|
|
542
|
+
press(handle, [12]);
|
|
543
|
+
act(() => handle.dispatchEvent(pointer("pointerup", 12)));
|
|
544
|
+
act(() => handle.dispatchEvent(new MouseEvent("dblclick", { bubbles: true })));
|
|
545
|
+
};
|
|
546
|
+
doubleClick();
|
|
547
|
+
doubleClick();
|
|
548
|
+
// Add 0.5 s, then take it away again, though the first save never landed.
|
|
549
|
+
expect(onSetElementAttributeQuiet.mock.calls.map(([, , value]) => value)).toEqual([
|
|
550
|
+
"0.5",
|
|
551
|
+
null,
|
|
552
|
+
]);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("shrinks a single fade longer than its clip from the clip's end, with no dead travel", () => {
|
|
556
|
+
const long = { ...clip, duration: 3, fadeIn: 5, fadeOut: undefined };
|
|
557
|
+
const { host, onSetElementAttributeLive } = render(long);
|
|
558
|
+
const handle = armedHandle(host, "in");
|
|
559
|
+
press(handle, [200], [195], [100]);
|
|
560
|
+
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(long, "data-fade-in", "2");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("gives a fade that fills the clip the only handle at its end", () => {
|
|
564
|
+
const { host } = render({ ...clip, fadeIn: 10, fadeOut: undefined });
|
|
565
|
+
// The fade-out can neither grow nor shrink, so it does not sit on top of the fade-in's tab.
|
|
566
|
+
expect(host.querySelector('[data-testid="clip-fade-handle-out"]')).toBeNull();
|
|
567
|
+
const inHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
568
|
+
expect([inHandle?.style.left, inHandle?.style.width]).toEqual(["976px", "24px"]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("splits two nearby boxes at the midpoint between their tabs on a tiny clip", () => {
|
|
572
|
+
// A 40 px clip with a 30 px fade-in: tabs at 30 and 33 px, boxes meet at 31.5.
|
|
573
|
+
const { host } = render({ ...clip, duration: 0.4, fadeIn: 0.3, fadeOut: undefined });
|
|
574
|
+
const inHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]');
|
|
575
|
+
const outHandle = host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-out"]');
|
|
576
|
+
expect([inHandle?.style.left, inHandle?.style.width]).toEqual(["11.5px", "20px"]);
|
|
577
|
+
expect([outHandle?.style.left, outHandle?.style.width]).toEqual(["31.5px", "8.5px"]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("keeps a long fade's hit box on its tab", () => {
|
|
581
|
+
const { host } = render({ ...clip, fadeIn: 8, fadeOut: 0.4 });
|
|
582
|
+
// The 8 s fade ends at 800 px; its 24 px box centres there, not in its half.
|
|
583
|
+
expect(host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]')?.style.left).toBe(
|
|
584
|
+
"788px",
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("never snaps a fade to its own clip's edges", () => {
|
|
589
|
+
usePlayerStore.setState({
|
|
590
|
+
currentTime: 2,
|
|
591
|
+
elements: [{ id: "before", tag: "audio", start: 0, duration: 2, track: 1 }, clip],
|
|
592
|
+
});
|
|
593
|
+
const { host, onSetElementAttributeLive } = render(clip);
|
|
594
|
+
const handle = armedHandle(host, "in");
|
|
595
|
+
// 0.05 s from the clip's start, where the playhead and the neighbour's end both sit.
|
|
596
|
+
press(handle, [100], [5]);
|
|
597
|
+
expect(onSetElementAttributeLive).toHaveBeenLastCalledWith(clip, "data-fade-in", "0.05");
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("keeps an unselected clip's handles out of the tab order", () => {
|
|
601
|
+
const { host } = render(clip);
|
|
602
|
+
expect(host.querySelector<HTMLElement>('[data-testid="clip-fade-handle-in"]')?.tabIndex).toBe(
|
|
603
|
+
-1,
|
|
604
|
+
);
|
|
301
605
|
});
|
|
302
606
|
|
|
303
607
|
it("does not let a press on the dot start the clip's own move gesture", () => {
|
|
304
608
|
const outer = vi.fn();
|
|
305
609
|
const { host, root } = render(clip, { onClipPointerDown: outer });
|
|
306
|
-
const handle = host
|
|
307
|
-
if (!handle) throw new Error("expected a fade-in handle");
|
|
308
|
-
armCapture(handle);
|
|
610
|
+
const handle = armedHandle(host, "in");
|
|
309
611
|
act(() => handle.dispatchEvent(pointer("pointerdown", 100)));
|
|
310
612
|
expect(outer).not.toHaveBeenCalled();
|
|
311
613
|
act(() => root.unmount());
|