@hyperframes/studio 0.7.56 → 0.7.57
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--Z69cEkE.js +459 -0
- package/dist/assets/{index-C4csZims.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/{index-CmVCjZjp.js → index-DfmYkU44.js} +196 -196
- package/dist/assets/{index-BWnOxAiH.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.html +2 -2
- package/dist/index.js +4345 -3812
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditOverlay.tsx +31 -62
- package/src/components/editor/LayersPanel.tsx +10 -1
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +36 -16
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/timelineEditingHelpers.test.ts +91 -0
- package/src/hooks/timelineEditingHelpers.ts +31 -244
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +5 -3
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +92 -1
- package/src/hooks/useDomEditCommits.ts +63 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +165 -0
- package/src/hooks/useElementLifecycleOps.ts +20 -7
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +378 -212
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineGroupEditing.ts +153 -132
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +85 -8
- package/src/player/components/Timeline.tsx +9 -9
- package/src/player/components/TimelineCanvas.tsx +11 -2
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/timelineClipDragCommit.test.ts +91 -8
- package/src/player/components/timelineClipDragCommit.ts +106 -9
- package/src/player/components/timelineLayout.ts +99 -26
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -1
- package/src/player/components/useTimelineStackingSync.ts +13 -2
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.ts +9 -0
- package/src/player/lib/timelineElementHelpers.ts +19 -0
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +37 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
|
@@ -77,6 +77,21 @@ function timelineElement(input: {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Mount a harness component under act() and return its unmount hook. */
|
|
81
|
+
function mountHarness(node: React.ReactElement): { unmount: () => void } {
|
|
82
|
+
const host = document.createElement("div");
|
|
83
|
+
document.body.append(host);
|
|
84
|
+
const root = createRoot(host);
|
|
85
|
+
act(() => {
|
|
86
|
+
root.render(node);
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
unmount: () => {
|
|
90
|
+
act(() => root.unmount());
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
function renderTimelineEditingHook(input: {
|
|
81
96
|
timelineElements: TimelineElement[];
|
|
82
97
|
iframe: HTMLIFrameElement;
|
|
@@ -92,17 +107,20 @@ function renderTimelineEditingHook(input: {
|
|
|
92
107
|
reloadPreview?: () => void;
|
|
93
108
|
sdkSession?: Awaited<ReturnType<typeof openComposition>> | null;
|
|
94
109
|
forceReloadSdkSession?: () => void;
|
|
110
|
+
showToast?: (message: string, kind?: string) => void;
|
|
95
111
|
}): {
|
|
96
112
|
move: ReturnType<typeof useTimelineEditing>["handleTimelineElementMove"];
|
|
97
113
|
resize: ReturnType<typeof useTimelineEditing>["handleTimelineElementResize"];
|
|
98
114
|
groupMove: ReturnType<typeof useTimelineEditing>["handleTimelineGroupMove"];
|
|
99
115
|
groupResize: ReturnType<typeof useTimelineEditing>["handleTimelineGroupResize"];
|
|
116
|
+
del: ReturnType<typeof useTimelineEditing>["handleTimelineElementDelete"];
|
|
100
117
|
unmount: () => void;
|
|
101
118
|
} {
|
|
102
119
|
let move: ReturnType<typeof useTimelineEditing>["handleTimelineElementMove"] | null = null;
|
|
103
120
|
let resize: ReturnType<typeof useTimelineEditing>["handleTimelineElementResize"] | null = null;
|
|
104
121
|
let groupMove: ReturnType<typeof useTimelineEditing>["handleTimelineGroupMove"] | null = null;
|
|
105
122
|
let groupResize: ReturnType<typeof useTimelineEditing>["handleTimelineGroupResize"] | null = null;
|
|
123
|
+
let del: ReturnType<typeof useTimelineEditing>["handleTimelineElementDelete"] | null = null;
|
|
106
124
|
|
|
107
125
|
function Harness() {
|
|
108
126
|
const commitRef = useRef(input.onZIndexCommit);
|
|
@@ -111,7 +129,7 @@ function renderTimelineEditingHook(input: {
|
|
|
111
129
|
projectId: input.projectId ?? null,
|
|
112
130
|
activeCompPath: "index.html",
|
|
113
131
|
timelineElements: input.timelineElements,
|
|
114
|
-
showToast: vi.fn(),
|
|
132
|
+
showToast: input.showToast ?? vi.fn(),
|
|
115
133
|
writeProjectFile: input.writeProjectFile ?? vi.fn(),
|
|
116
134
|
recordEdit: input.recordEdit ?? vi.fn(),
|
|
117
135
|
domEditSaveTimestampRef: { current: 0 },
|
|
@@ -127,29 +145,17 @@ function renderTimelineEditingHook(input: {
|
|
|
127
145
|
resize = hook.handleTimelineElementResize;
|
|
128
146
|
groupMove = hook.handleTimelineGroupMove;
|
|
129
147
|
groupResize = hook.handleTimelineGroupResize;
|
|
148
|
+
del = hook.handleTimelineElementDelete;
|
|
130
149
|
return null;
|
|
131
150
|
}
|
|
132
151
|
|
|
133
|
-
const
|
|
134
|
-
document.body.append(host);
|
|
135
|
-
const root = createRoot(host);
|
|
136
|
-
act(() => {
|
|
137
|
-
root.render(<Harness />);
|
|
138
|
-
});
|
|
139
|
-
|
|
152
|
+
const { unmount } = mountHarness(<Harness />);
|
|
140
153
|
if (!move) throw new Error("Expected hook to expose move handler");
|
|
141
154
|
if (!resize) throw new Error("Expected hook to expose resize handler");
|
|
142
155
|
if (!groupMove) throw new Error("Expected hook to expose group move handler");
|
|
143
156
|
if (!groupResize) throw new Error("Expected hook to expose group resize handler");
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
resize,
|
|
147
|
-
groupMove,
|
|
148
|
-
groupResize,
|
|
149
|
-
unmount: () => {
|
|
150
|
-
act(() => root.unmount());
|
|
151
|
-
},
|
|
152
|
-
};
|
|
157
|
+
if (!del) throw new Error("Expected hook to expose delete handler");
|
|
158
|
+
return { move, resize, groupMove, groupResize, del, unmount };
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
type TimelineRecordEdit = NonNullable<
|
|
@@ -198,20 +204,9 @@ function renderTimelineEditingHookWithLifecycle(input: {
|
|
|
198
204
|
return null;
|
|
199
205
|
}
|
|
200
206
|
|
|
201
|
-
const
|
|
202
|
-
document.body.append(host);
|
|
203
|
-
const root = createRoot(host);
|
|
204
|
-
act(() => {
|
|
205
|
-
root.render(<Harness />);
|
|
206
|
-
});
|
|
207
|
-
|
|
207
|
+
const { unmount } = mountHarness(<Harness />);
|
|
208
208
|
if (!move) throw new Error("Expected hook to expose move handler");
|
|
209
|
-
return {
|
|
210
|
-
move,
|
|
211
|
-
unmount: () => {
|
|
212
|
-
act(() => root.unmount());
|
|
213
|
-
},
|
|
214
|
-
};
|
|
209
|
+
return { move, unmount };
|
|
215
210
|
}
|
|
216
211
|
|
|
217
212
|
function jsonResponse(body: unknown): Response {
|
|
@@ -233,137 +228,118 @@ async function flushAsyncWork(): Promise<void> {
|
|
|
233
228
|
}
|
|
234
229
|
}
|
|
235
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Stub global fetch for project "p1": serves file contents (a single source
|
|
233
|
+
* string, or a path → content map) and answers the GSAP-mutation endpoint
|
|
234
|
+
* with `gsapBody`. Returns the mock for call inspection.
|
|
235
|
+
*/
|
|
236
|
+
function stubProjectFetch(
|
|
237
|
+
files: string | Record<string, string>,
|
|
238
|
+
gsapBody: unknown = { ok: true },
|
|
239
|
+
) {
|
|
240
|
+
const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
241
|
+
const url = requestUrl(input);
|
|
242
|
+
if (url.includes("/api/projects/p1/files/")) {
|
|
243
|
+
if (typeof files === "string") return jsonResponse({ content: files });
|
|
244
|
+
const path = decodeURIComponent(url.split("/files/")[1] ?? "index.html");
|
|
245
|
+
return jsonResponse({ content: files[path] });
|
|
246
|
+
}
|
|
247
|
+
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse(gsapBody);
|
|
248
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
249
|
+
});
|
|
250
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
251
|
+
return fetchMock;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const ROOT_DURATION_FALLBACK_SOURCE = [
|
|
255
|
+
`<div data-composition-id="main" data-duration="4">`,
|
|
256
|
+
` <div id="clip" data-hf-id="hf-clip" data-start="0" data-duration="2"></div>`,
|
|
257
|
+
`</div>`,
|
|
258
|
+
].join("\n");
|
|
259
|
+
|
|
260
|
+
/** Shared setup for the SDK-fallback root-duration tests: one 2s clip in a 4s comp. */
|
|
261
|
+
async function setupRootDurationFallback() {
|
|
262
|
+
const iframe = createPreviewIframe([{ id: "clip", track: 0 }]);
|
|
263
|
+
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0 });
|
|
264
|
+
const sdkSession = await openComposition(ROOT_DURATION_FALLBACK_SOURCE);
|
|
265
|
+
const setTimingSpy = vi.spyOn(sdkSession, "setTiming");
|
|
266
|
+
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
267
|
+
const recordEdit = vi.fn<TimelineRecordEdit>(async () => {});
|
|
268
|
+
const forceReloadSdkSession = vi.fn();
|
|
269
|
+
const reloadPreview = vi.fn();
|
|
270
|
+
const iframeWindow = iframe.contentWindow;
|
|
271
|
+
if (!iframeWindow) throw new Error("Expected iframe window");
|
|
272
|
+
const postMessageSpy = vi.spyOn(iframeWindow, "postMessage");
|
|
273
|
+
stubProjectFetch(ROOT_DURATION_FALLBACK_SOURCE, { ok: true, mutated: false });
|
|
274
|
+
usePlayerStore.getState().setDuration(4);
|
|
275
|
+
const hook = renderTimelineEditingHook({
|
|
276
|
+
timelineElements: [clip],
|
|
277
|
+
iframe,
|
|
278
|
+
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
279
|
+
projectId: "p1",
|
|
280
|
+
writeProjectFile,
|
|
281
|
+
recordEdit,
|
|
282
|
+
sdkSession,
|
|
283
|
+
forceReloadSdkSession,
|
|
284
|
+
reloadPreview,
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
hook,
|
|
288
|
+
clip,
|
|
289
|
+
setTimingSpy,
|
|
290
|
+
writeProjectFile,
|
|
291
|
+
forceReloadSdkSession,
|
|
292
|
+
reloadPreview,
|
|
293
|
+
postMessageSpy,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Shared assertions: the fallback path grew the root to 5s and did ONE full reload. */
|
|
298
|
+
function expectRootDurationExtendedViaFallback(
|
|
299
|
+
ctx: Awaited<ReturnType<typeof setupRootDurationFallback>>,
|
|
300
|
+
): void {
|
|
301
|
+
expect(ctx.setTimingSpy).not.toHaveBeenCalled();
|
|
302
|
+
expect(ctx.writeProjectFile.mock.calls[0]![1]).toContain(
|
|
303
|
+
'data-composition-id="main" data-duration="5"',
|
|
304
|
+
);
|
|
305
|
+
expect(usePlayerStore.getState().duration).toBe(5);
|
|
306
|
+
expect(ctx.forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
307
|
+
// The GSAP endpoint returned no rewritten scriptText, so the timing sync
|
|
308
|
+
// escalates from the flash-free soft reload to ONE full reload. The root
|
|
309
|
+
// duration travels via the persisted content-driven `data-duration` (above),
|
|
310
|
+
// not a `set-root-duration` postMessage.
|
|
311
|
+
expect(ctx.reloadPreview).toHaveBeenCalledTimes(1);
|
|
312
|
+
expect(ctx.postMessageSpy).not.toHaveBeenCalledWith(
|
|
313
|
+
expect.objectContaining({ action: "set-root-duration" }),
|
|
314
|
+
"*",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
236
318
|
describe("useTimelineEditing timeline z-index reorder", () => {
|
|
237
319
|
it("extends root duration through the fallback path when an SDK-backed move passes the end", async () => {
|
|
238
|
-
const
|
|
239
|
-
`<div data-composition-id="main" data-duration="4">`,
|
|
240
|
-
` <div id="clip" data-hf-id="hf-clip" data-start="0" data-duration="2"></div>`,
|
|
241
|
-
`</div>`,
|
|
242
|
-
].join("\n");
|
|
243
|
-
const iframe = createPreviewIframe([{ id: "clip", track: 0 }]);
|
|
244
|
-
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0 });
|
|
245
|
-
const sdkSession = await openComposition(source);
|
|
246
|
-
const setTimingSpy = vi.spyOn(sdkSession, "setTiming");
|
|
247
|
-
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
248
|
-
const recordEdit = vi.fn<TimelineRecordEdit>(async () => {});
|
|
249
|
-
const forceReloadSdkSession = vi.fn();
|
|
250
|
-
const reloadPreview = vi.fn();
|
|
251
|
-
const iframeWindow = iframe.contentWindow;
|
|
252
|
-
if (!iframeWindow) throw new Error("Expected iframe window");
|
|
253
|
-
const postMessageSpy = vi.spyOn(iframeWindow, "postMessage");
|
|
254
|
-
vi.stubGlobal(
|
|
255
|
-
"fetch",
|
|
256
|
-
vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
257
|
-
const url = requestUrl(input);
|
|
258
|
-
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: source });
|
|
259
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
260
|
-
return jsonResponse({ ok: true, mutated: false });
|
|
261
|
-
}
|
|
262
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
usePlayerStore.getState().setDuration(4);
|
|
266
|
-
const { move, unmount } = renderTimelineEditingHook({
|
|
267
|
-
timelineElements: [clip],
|
|
268
|
-
iframe,
|
|
269
|
-
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
270
|
-
projectId: "p1",
|
|
271
|
-
writeProjectFile,
|
|
272
|
-
recordEdit,
|
|
273
|
-
sdkSession,
|
|
274
|
-
forceReloadSdkSession,
|
|
275
|
-
reloadPreview,
|
|
276
|
-
});
|
|
320
|
+
const ctx = await setupRootDurationFallback();
|
|
277
321
|
|
|
278
322
|
await act(async () => {
|
|
279
|
-
await move(clip, { start: 3, track: clip.track });
|
|
323
|
+
await ctx.hook.move(ctx.clip, { start: 3, track: ctx.clip.track });
|
|
280
324
|
});
|
|
281
325
|
|
|
282
|
-
expect(
|
|
283
|
-
|
|
284
|
-
'data-composition-id="main" data-duration="5"',
|
|
285
|
-
);
|
|
286
|
-
expect(writeProjectFile.mock.calls[0]![1]).toContain('data-start="3"');
|
|
287
|
-
expect(usePlayerStore.getState().duration).toBe(5);
|
|
288
|
-
expect(forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
289
|
-
expect(reloadPreview).not.toHaveBeenCalled();
|
|
290
|
-
expect(postMessageSpy).toHaveBeenCalledWith(
|
|
291
|
-
{
|
|
292
|
-
source: "hf-parent",
|
|
293
|
-
type: "control",
|
|
294
|
-
action: "set-root-duration",
|
|
295
|
-
durationSeconds: 5,
|
|
296
|
-
},
|
|
297
|
-
"*",
|
|
298
|
-
);
|
|
326
|
+
expect(ctx.writeProjectFile.mock.calls[0]![1]).toContain('data-start="3"');
|
|
327
|
+
expectRootDurationExtendedViaFallback(ctx);
|
|
299
328
|
|
|
300
|
-
unmount();
|
|
329
|
+
ctx.hook.unmount();
|
|
301
330
|
});
|
|
302
331
|
|
|
303
332
|
it("extends root duration through the fallback path when an SDK-backed resize passes the end", async () => {
|
|
304
|
-
const
|
|
305
|
-
`<div data-composition-id="main" data-duration="4">`,
|
|
306
|
-
` <div id="clip" data-hf-id="hf-clip" data-start="0" data-duration="2"></div>`,
|
|
307
|
-
`</div>`,
|
|
308
|
-
].join("\n");
|
|
309
|
-
const iframe = createPreviewIframe([{ id: "clip", track: 0 }]);
|
|
310
|
-
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0 });
|
|
311
|
-
const sdkSession = await openComposition(source);
|
|
312
|
-
const setTimingSpy = vi.spyOn(sdkSession, "setTiming");
|
|
313
|
-
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
314
|
-
const recordEdit = vi.fn<TimelineRecordEdit>(async () => {});
|
|
315
|
-
const forceReloadSdkSession = vi.fn();
|
|
316
|
-
const reloadPreview = vi.fn();
|
|
317
|
-
const iframeWindow = iframe.contentWindow;
|
|
318
|
-
if (!iframeWindow) throw new Error("Expected iframe window");
|
|
319
|
-
const postMessageSpy = vi.spyOn(iframeWindow, "postMessage");
|
|
320
|
-
vi.stubGlobal(
|
|
321
|
-
"fetch",
|
|
322
|
-
vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
323
|
-
const url = requestUrl(input);
|
|
324
|
-
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: source });
|
|
325
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
326
|
-
return jsonResponse({ ok: true, mutated: false });
|
|
327
|
-
}
|
|
328
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
329
|
-
}),
|
|
330
|
-
);
|
|
331
|
-
usePlayerStore.getState().setDuration(4);
|
|
332
|
-
const { resize, unmount } = renderTimelineEditingHook({
|
|
333
|
-
timelineElements: [clip],
|
|
334
|
-
iframe,
|
|
335
|
-
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
336
|
-
projectId: "p1",
|
|
337
|
-
writeProjectFile,
|
|
338
|
-
recordEdit,
|
|
339
|
-
sdkSession,
|
|
340
|
-
forceReloadSdkSession,
|
|
341
|
-
reloadPreview,
|
|
342
|
-
});
|
|
333
|
+
const ctx = await setupRootDurationFallback();
|
|
343
334
|
|
|
344
335
|
await act(async () => {
|
|
345
|
-
await resize(clip, { start: 0, duration: 5, playbackStart: undefined });
|
|
336
|
+
await ctx.hook.resize(ctx.clip, { start: 0, duration: 5, playbackStart: undefined });
|
|
346
337
|
});
|
|
347
338
|
|
|
348
|
-
expect(
|
|
349
|
-
|
|
350
|
-
'data-composition-id="main" data-duration="5"',
|
|
351
|
-
);
|
|
352
|
-
expect(writeProjectFile.mock.calls[0]![1]).toContain('data-duration="5"></div>');
|
|
353
|
-
expect(usePlayerStore.getState().duration).toBe(5);
|
|
354
|
-
expect(forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
355
|
-
expect(reloadPreview).not.toHaveBeenCalled();
|
|
356
|
-
expect(postMessageSpy).toHaveBeenCalledWith(
|
|
357
|
-
{
|
|
358
|
-
source: "hf-parent",
|
|
359
|
-
type: "control",
|
|
360
|
-
action: "set-root-duration",
|
|
361
|
-
durationSeconds: 5,
|
|
362
|
-
},
|
|
363
|
-
"*",
|
|
364
|
-
);
|
|
339
|
+
expect(ctx.writeProjectFile.mock.calls[0]![1]).toContain('data-duration="5"></div>');
|
|
340
|
+
expectRootDurationExtendedViaFallback(ctx);
|
|
365
341
|
|
|
366
|
-
unmount();
|
|
342
|
+
ctx.hook.unmount();
|
|
367
343
|
});
|
|
368
344
|
|
|
369
345
|
it("routes a vertical drag through the shared z-index commit without writing track-index", async () => {
|
|
@@ -638,24 +614,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
638
614
|
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
639
615
|
const recordEdit = vi.fn<TimelineRecordEdit>(async (_entry) => {});
|
|
640
616
|
const reloadPreview = vi.fn();
|
|
641
|
-
const fetchMock =
|
|
642
|
-
async (
|
|
643
|
-
input: Parameters<typeof fetch>[0],
|
|
644
|
-
_init?: Parameters<typeof fetch>[1],
|
|
645
|
-
): Promise<Response> => {
|
|
646
|
-
const url = requestUrl(input);
|
|
647
|
-
if (url.includes("/api/projects/p1/files/")) {
|
|
648
|
-
return jsonResponse({
|
|
649
|
-
content: '<div id="clip" data-start="0" data-track-index="0"></div>',
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) {
|
|
653
|
-
return jsonResponse({ ok: true });
|
|
654
|
-
}
|
|
655
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
656
|
-
},
|
|
657
|
-
);
|
|
658
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
617
|
+
const fetchMock = stubProjectFetch('<div id="clip" data-start="0" data-track-index="0"></div>');
|
|
659
618
|
const { move, unmount } = renderTimelineEditingHook({
|
|
660
619
|
timelineElements: [clip],
|
|
661
620
|
iframe,
|
|
@@ -685,6 +644,41 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
685
644
|
unmount();
|
|
686
645
|
});
|
|
687
646
|
|
|
647
|
+
it("persists a vertical-only lane move (start unchanged) through the single-element fallback", async () => {
|
|
648
|
+
// Regression: `if (!startChanged) return` ran BEFORE the file persist, so a
|
|
649
|
+
// pure lane change routed through onMoveElement (no onMoveElements wired)
|
|
650
|
+
// wrote NOTHING — the lane snapped back on the next reload.
|
|
651
|
+
const iframe = createPreviewIframe([{ id: "clip", track: 0 }]);
|
|
652
|
+
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0 });
|
|
653
|
+
const commit = vi.fn<(entries: ZIndexEntry[]) => Promise<void>>().mockResolvedValue(undefined);
|
|
654
|
+
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
655
|
+
stubProjectFetch('<div id="clip" data-start="0" data-track-index="0"></div>');
|
|
656
|
+
const { move, unmount } = renderTimelineEditingHook({
|
|
657
|
+
timelineElements: [clip],
|
|
658
|
+
iframe,
|
|
659
|
+
onZIndexCommit: commit,
|
|
660
|
+
projectId: "p1",
|
|
661
|
+
writeProjectFile,
|
|
662
|
+
recordEdit: vi.fn(async () => {}),
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
await act(async () => {
|
|
666
|
+
// Vertical-only: same start, new track (already authored-space on this path).
|
|
667
|
+
await move(clip, { start: clip.start, track: 2 });
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const doc = iframe.contentDocument;
|
|
671
|
+
if (!doc) throw new Error("Expected iframe document");
|
|
672
|
+
// Live DOM patched so a pre-reload re-discovery doesn't snap the lane back...
|
|
673
|
+
expect(doc.getElementById("clip")?.getAttribute("data-track-index")).toBe("2");
|
|
674
|
+
// ...and the file write carries the new data-track-index with start intact.
|
|
675
|
+
expect(writeProjectFile).toHaveBeenCalled();
|
|
676
|
+
expect(writeProjectFile.mock.calls[0]![1]).toContain('data-track-index="2"');
|
|
677
|
+
expect(writeProjectFile.mock.calls[0]![1]).toContain('data-start="0"');
|
|
678
|
+
|
|
679
|
+
unmount();
|
|
680
|
+
});
|
|
681
|
+
|
|
688
682
|
it("orders the timing write after the z-index commit so a diagonal drag can't clobber the restack", async () => {
|
|
689
683
|
const iframe = createPreviewIframe([
|
|
690
684
|
{ id: "clip", track: 0, style: "position: relative; z-index: 0" },
|
|
@@ -697,17 +691,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
697
691
|
});
|
|
698
692
|
const commit = vi.fn<(entries: ZIndexEntry[]) => Promise<void>>().mockReturnValue(commitGate);
|
|
699
693
|
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
700
|
-
|
|
701
|
-
const url = requestUrl(input);
|
|
702
|
-
if (url.includes("/api/projects/p1/files/")) {
|
|
703
|
-
return jsonResponse({
|
|
704
|
-
content: '<div id="clip" data-start="0" data-track-index="0"></div>',
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse({ ok: true });
|
|
708
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
709
|
-
});
|
|
710
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
694
|
+
stubProjectFetch('<div id="clip" data-start="0" data-track-index="0"></div>');
|
|
711
695
|
const { move, unmount } = renderTimelineEditingHook({
|
|
712
696
|
timelineElements: [clip],
|
|
713
697
|
iframe,
|
|
@@ -766,15 +750,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
766
750
|
];
|
|
767
751
|
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
768
752
|
const recordEdit = vi.fn<TimelineRecordEdit>(async (_entry) => {});
|
|
769
|
-
|
|
770
|
-
"fetch",
|
|
771
|
-
vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
772
|
-
const url = requestUrl(input);
|
|
773
|
-
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: source });
|
|
774
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse({ ok: true });
|
|
775
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
776
|
-
}),
|
|
777
|
-
);
|
|
753
|
+
stubProjectFetch(source);
|
|
778
754
|
const { groupMove, unmount } = renderTimelineEditingHook({
|
|
779
755
|
timelineElements: clips,
|
|
780
756
|
iframe,
|
|
@@ -823,18 +799,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
823
799
|
});
|
|
824
800
|
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
825
801
|
const recordEdit = vi.fn<TimelineRecordEdit>(async (_entry) => {});
|
|
826
|
-
|
|
827
|
-
"fetch",
|
|
828
|
-
vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
829
|
-
const url = requestUrl(input);
|
|
830
|
-
if (url.includes("/api/projects/p1/files/")) {
|
|
831
|
-
const path = decodeURIComponent(url.split("/files/")[1] ?? "index.html");
|
|
832
|
-
return jsonResponse({ content: files[path] });
|
|
833
|
-
}
|
|
834
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse({ ok: true });
|
|
835
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
836
|
-
}),
|
|
837
|
-
);
|
|
802
|
+
stubProjectFetch(files);
|
|
838
803
|
const { groupMove, unmount } = renderTimelineEditingHook({
|
|
839
804
|
timelineElements: [a, b],
|
|
840
805
|
iframe,
|
|
@@ -875,15 +840,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
875
840
|
releaseCommit = resolve;
|
|
876
841
|
});
|
|
877
842
|
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
878
|
-
|
|
879
|
-
"fetch",
|
|
880
|
-
vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
881
|
-
const url = requestUrl(input);
|
|
882
|
-
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: source });
|
|
883
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse({ ok: true });
|
|
884
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
885
|
-
}),
|
|
886
|
-
);
|
|
843
|
+
stubProjectFetch(source);
|
|
887
844
|
const { groupMove, unmount } = renderTimelineEditingHook({
|
|
888
845
|
timelineElements: [clip],
|
|
889
846
|
iframe,
|
|
@@ -913,13 +870,7 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
913
870
|
it("matches the single-clip move output when a group move contains one clip", async () => {
|
|
914
871
|
const source = '<div id="clip" data-start="0" data-duration="1"></div>';
|
|
915
872
|
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0, start: 0, duration: 1 });
|
|
916
|
-
|
|
917
|
-
const url = requestUrl(input);
|
|
918
|
-
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: source });
|
|
919
|
-
if (url.includes("/api/projects/p1/gsap-mutations/")) return jsonResponse({ ok: true });
|
|
920
|
-
throw new Error(`Unexpected fetch: ${url}`);
|
|
921
|
-
});
|
|
922
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
873
|
+
stubProjectFetch(source);
|
|
923
874
|
|
|
924
875
|
const singleWrite = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
925
876
|
const single = renderTimelineEditingHook({
|
|
@@ -952,3 +903,218 @@ describe("useTimelineEditing timeline z-index reorder", () => {
|
|
|
952
903
|
group.unmount();
|
|
953
904
|
});
|
|
954
905
|
});
|
|
906
|
+
|
|
907
|
+
describe("useTimelineEditing duration rollback on failed persist", () => {
|
|
908
|
+
const ROLLBACK_SOURCE = [
|
|
909
|
+
`<div data-composition-id="main" data-duration="4">`,
|
|
910
|
+
` <div id="clip" data-start="0" data-duration="2" data-track-index="0"></div>`,
|
|
911
|
+
`</div>`,
|
|
912
|
+
].join("\n");
|
|
913
|
+
|
|
914
|
+
/** Iframe with a comp root so the optimistic sync (and its rollback) can patch data-duration. */
|
|
915
|
+
function createRootedIframe(source: string = ROLLBACK_SOURCE): HTMLIFrameElement {
|
|
916
|
+
const iframe = document.createElement("iframe");
|
|
917
|
+
document.body.append(iframe);
|
|
918
|
+
const doc = iframe.contentDocument;
|
|
919
|
+
if (!doc) throw new Error("Expected iframe document");
|
|
920
|
+
doc.body.innerHTML = source;
|
|
921
|
+
return iframe;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function rootDurationAttr(iframe: HTMLIFrameElement): string | null | undefined {
|
|
925
|
+
return iframe.contentDocument
|
|
926
|
+
?.querySelector("[data-composition-id]")
|
|
927
|
+
?.getAttribute("data-duration");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function setupFailedPersist() {
|
|
931
|
+
const iframe = createRootedIframe();
|
|
932
|
+
const clip = timelineElement({ id: "clip", track: 0, zIndex: 0 });
|
|
933
|
+
const writeError = new Error("write failed");
|
|
934
|
+
const writeProjectFile = vi
|
|
935
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
936
|
+
.mockRejectedValue(writeError);
|
|
937
|
+
stubProjectFetch(ROLLBACK_SOURCE);
|
|
938
|
+
usePlayerStore.getState().setDuration(4);
|
|
939
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
940
|
+
const hook = renderTimelineEditingHook({
|
|
941
|
+
timelineElements: [clip],
|
|
942
|
+
iframe,
|
|
943
|
+
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
944
|
+
projectId: "p1",
|
|
945
|
+
writeProjectFile,
|
|
946
|
+
recordEdit: vi.fn(async () => {}),
|
|
947
|
+
reloadPreview: vi.fn(),
|
|
948
|
+
});
|
|
949
|
+
return { iframe, clip, hook, writeError };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
it("rolls back the store duration and live root when a move persist fails", async () => {
|
|
953
|
+
const { iframe, clip, hook, writeError } = setupFailedPersist();
|
|
954
|
+
|
|
955
|
+
let rejection: unknown;
|
|
956
|
+
await act(async () => {
|
|
957
|
+
// Move past the end: the optimistic sync grows the readout to 5s.
|
|
958
|
+
await hook.move(clip, { start: 3, track: clip.track }).catch((error) => {
|
|
959
|
+
rejection = error;
|
|
960
|
+
});
|
|
961
|
+
await flushAsyncWork();
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
expect(rejection).toBe(writeError);
|
|
965
|
+
expect(usePlayerStore.getState().duration).toBe(4);
|
|
966
|
+
expect(rootDurationAttr(iframe)).toBe("4");
|
|
967
|
+
|
|
968
|
+
hook.unmount();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("rolls back the store duration and live root when a resize persist fails", async () => {
|
|
972
|
+
const { iframe, clip, hook, writeError } = setupFailedPersist();
|
|
973
|
+
|
|
974
|
+
let rejection: unknown;
|
|
975
|
+
await act(async () => {
|
|
976
|
+
await hook
|
|
977
|
+
.resize(clip, { start: 0, duration: 6, playbackStart: undefined })
|
|
978
|
+
.catch((error) => {
|
|
979
|
+
rejection = error;
|
|
980
|
+
});
|
|
981
|
+
await flushAsyncWork();
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
expect(rejection).toBe(writeError);
|
|
985
|
+
expect(usePlayerStore.getState().duration).toBe(4);
|
|
986
|
+
expect(rootDurationAttr(iframe)).toBe("4");
|
|
987
|
+
|
|
988
|
+
hook.unmount();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("rolls back the store duration and live root when a group move persist fails", async () => {
|
|
992
|
+
const { iframe, clip, hook, writeError } = setupFailedPersist();
|
|
993
|
+
|
|
994
|
+
let rejection: unknown;
|
|
995
|
+
await act(async () => {
|
|
996
|
+
await hook.groupMove([{ element: clip, start: 3.5 }]).catch((error) => {
|
|
997
|
+
rejection = error;
|
|
998
|
+
});
|
|
999
|
+
await flushAsyncWork();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
expect(rejection).toBe(writeError);
|
|
1003
|
+
expect(usePlayerStore.getState().duration).toBe(4);
|
|
1004
|
+
expect(rootDurationAttr(iframe)).toBe("4");
|
|
1005
|
+
|
|
1006
|
+
hook.unmount();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it("rolls back the store duration and live root when a group resize persist fails", async () => {
|
|
1010
|
+
const { iframe, clip, hook, writeError } = setupFailedPersist();
|
|
1011
|
+
|
|
1012
|
+
let rejection: unknown;
|
|
1013
|
+
await act(async () => {
|
|
1014
|
+
await hook.groupResize([{ element: clip, start: 0, duration: 7 }]).catch((error) => {
|
|
1015
|
+
rejection = error;
|
|
1016
|
+
});
|
|
1017
|
+
await flushAsyncWork();
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
expect(rejection).toBe(writeError);
|
|
1021
|
+
expect(usePlayerStore.getState().duration).toBe(4);
|
|
1022
|
+
expect(rootDurationAttr(iframe)).toBe("4");
|
|
1023
|
+
|
|
1024
|
+
hook.unmount();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("rolls back the store duration and live root when a delete persist fails", async () => {
|
|
1028
|
+
// Two clips; deleting the furthest one shrinks the content-driven duration
|
|
1029
|
+
// optimistically (4s -> 2s), so a failed write must roll that shrink back.
|
|
1030
|
+
const DELETE_SOURCE = [
|
|
1031
|
+
`<div data-composition-id="main" data-duration="4">`,
|
|
1032
|
+
` <div id="clip" data-start="0" data-duration="2" data-track-index="0"></div>`,
|
|
1033
|
+
` <div id="tail" data-start="2" data-duration="2" data-track-index="0"></div>`,
|
|
1034
|
+
`</div>`,
|
|
1035
|
+
].join("\n");
|
|
1036
|
+
const DELETE_REMOVED_SOURCE = [
|
|
1037
|
+
`<div data-composition-id="main" data-duration="4">`,
|
|
1038
|
+
` <div id="clip" data-start="0" data-duration="2" data-track-index="0"></div>`,
|
|
1039
|
+
`</div>`,
|
|
1040
|
+
].join("\n");
|
|
1041
|
+
|
|
1042
|
+
const iframe = createRootedIframe(DELETE_SOURCE);
|
|
1043
|
+
const tail = timelineElement({ id: "tail", track: 0, zIndex: 0, start: 2, duration: 2 });
|
|
1044
|
+
const writeError = new Error("write failed");
|
|
1045
|
+
const writeProjectFile = vi
|
|
1046
|
+
.fn<(...args: unknown[]) => Promise<void>>()
|
|
1047
|
+
.mockRejectedValue(writeError);
|
|
1048
|
+
// The delete path reads the file, then asks the server-side remove-element
|
|
1049
|
+
// mutation for the post-removal source before persisting it.
|
|
1050
|
+
const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
|
|
1051
|
+
const url = requestUrl(input);
|
|
1052
|
+
if (url.includes("/api/projects/p1/files/")) {
|
|
1053
|
+
return jsonResponse({ content: DELETE_SOURCE });
|
|
1054
|
+
}
|
|
1055
|
+
if (url.includes("/api/projects/p1/file-mutations/remove-element/")) {
|
|
1056
|
+
return jsonResponse({ changed: true, content: DELETE_REMOVED_SOURCE });
|
|
1057
|
+
}
|
|
1058
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
1059
|
+
});
|
|
1060
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1061
|
+
usePlayerStore.getState().setDuration(4);
|
|
1062
|
+
const showToast = vi.fn();
|
|
1063
|
+
const hook = renderTimelineEditingHook({
|
|
1064
|
+
timelineElements: [tail],
|
|
1065
|
+
iframe,
|
|
1066
|
+
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
1067
|
+
projectId: "p1",
|
|
1068
|
+
writeProjectFile,
|
|
1069
|
+
recordEdit: vi.fn(async () => {}),
|
|
1070
|
+
reloadPreview: vi.fn(),
|
|
1071
|
+
showToast,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
await act(async () => {
|
|
1075
|
+
// Unlike move/resize, the delete handler swallows the persist failure
|
|
1076
|
+
// into a toast, so the promise resolves.
|
|
1077
|
+
await hook.del(tail);
|
|
1078
|
+
await flushAsyncWork();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// The optimistic shrink reached the persist attempt (root patched to the
|
|
1082
|
+
// furthest remaining clip end, 2s)...
|
|
1083
|
+
expect(writeProjectFile).toHaveBeenCalledTimes(1);
|
|
1084
|
+
expect(String(writeProjectFile.mock.calls[0]![1])).toContain(
|
|
1085
|
+
'data-composition-id="main" data-duration="2"',
|
|
1086
|
+
);
|
|
1087
|
+
expect(showToast).toHaveBeenCalledWith("write failed");
|
|
1088
|
+
// ...and the failed write rolled the readout AND the live root back.
|
|
1089
|
+
expect(usePlayerStore.getState().duration).toBe(4);
|
|
1090
|
+
expect(rootDurationAttr(iframe)).toBe("4");
|
|
1091
|
+
|
|
1092
|
+
hook.unmount();
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it("keeps the grown duration when the persist succeeds", async () => {
|
|
1096
|
+
const { iframe, clip, hook } = setupFailedPersist();
|
|
1097
|
+
// Same harness, but with a write that succeeds this time.
|
|
1098
|
+
hook.unmount();
|
|
1099
|
+
const writeProjectFile = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
|
1100
|
+
const succeeding = renderTimelineEditingHook({
|
|
1101
|
+
timelineElements: [clip],
|
|
1102
|
+
iframe,
|
|
1103
|
+
onZIndexCommit: vi.fn().mockResolvedValue(undefined),
|
|
1104
|
+
projectId: "p1",
|
|
1105
|
+
writeProjectFile,
|
|
1106
|
+
recordEdit: vi.fn(async () => {}),
|
|
1107
|
+
reloadPreview: vi.fn(),
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await act(async () => {
|
|
1111
|
+
await succeeding.move(clip, { start: 3, track: clip.track });
|
|
1112
|
+
await flushAsyncWork();
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
expect(usePlayerStore.getState().duration).toBe(5);
|
|
1116
|
+
expect(rootDurationAttr(iframe)).toBe("5");
|
|
1117
|
+
|
|
1118
|
+
succeeding.unmount();
|
|
1119
|
+
});
|
|
1120
|
+
});
|