@hyperframes/studio 0.7.56 → 0.7.58
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-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -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 +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- 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 +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- 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 +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.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
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { usePlayerStore } from "../player";
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
LAYER_REVEAL_PRIOR_POSITION_ATTR,
|
|
8
|
+
LAYER_REVEAL_PRIOR_Z_ATTR,
|
|
9
|
+
} from "../player/lib/timelineElementHelpers";
|
|
10
|
+
import {
|
|
11
|
+
LAYER_REVEAL_LIFT_Z,
|
|
12
|
+
LAYER_REVEAL_PENDING_COMMIT_ATTR,
|
|
13
|
+
liftElementToTop,
|
|
14
|
+
restoreLiftedElement,
|
|
15
|
+
useLayerRevealOverride,
|
|
16
|
+
} from "../components/editor/useLayerRevealOverride";
|
|
17
|
+
import type { DomEditPatchBatch, DomEditPatchBatchesResult } from "./domEditCommitTypes";
|
|
7
18
|
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
|
19
|
+
import { makeLifecycleOpsParams } from "./elementLifecycleOpsTestUtils";
|
|
8
20
|
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
21
|
+
import { runZLaneGesture } from "../components/nle/zLaneGesture";
|
|
9
22
|
|
|
10
23
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
24
|
|
|
@@ -13,11 +26,14 @@ afterEach(() => {
|
|
|
13
26
|
document.body.innerHTML = "";
|
|
14
27
|
usePlayerStore.getState().setElements([]);
|
|
15
28
|
vi.unstubAllGlobals();
|
|
29
|
+
vi.useRealTimers();
|
|
16
30
|
});
|
|
17
31
|
|
|
18
32
|
interface BatchOptions {
|
|
19
33
|
label: string;
|
|
20
34
|
coalesceKey: string;
|
|
35
|
+
coalesceMs?: number;
|
|
36
|
+
skipReload?: boolean;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
interface CapturedBatchCall {
|
|
@@ -36,26 +52,22 @@ type ReorderCommit = (
|
|
|
36
52
|
key?: string;
|
|
37
53
|
}>,
|
|
38
54
|
coalesceKeyOverride?: string,
|
|
39
|
-
|
|
55
|
+
actionKind?: string,
|
|
56
|
+
) => Promise<DomEditPatchBatchesResult | undefined | void>;
|
|
40
57
|
|
|
41
58
|
function renderReorderHook(
|
|
42
59
|
capturedCalls: CapturedBatchCall[],
|
|
43
60
|
onReady: (commit: ReorderCommit) => void,
|
|
44
61
|
) {
|
|
45
62
|
function Harness() {
|
|
46
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
clearDomSelection: vi.fn(),
|
|
55
|
-
commitDomEditPatchBatches: async (batches, options) => {
|
|
56
|
-
capturedCalls.push({ batches, options });
|
|
57
|
-
},
|
|
58
|
-
});
|
|
63
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
64
|
+
makeLifecycleOpsParams({
|
|
65
|
+
commitDomEditPatchBatches: async (batches, options) => {
|
|
66
|
+
capturedCalls.push({ batches, options });
|
|
67
|
+
return { durable: true, allMatched: true, changed: true };
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
59
71
|
onReady(handleDomZIndexReorderCommit);
|
|
60
72
|
return null;
|
|
61
73
|
}
|
|
@@ -109,6 +121,25 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
109
121
|
act(() => root.unmount());
|
|
110
122
|
});
|
|
111
123
|
|
|
124
|
+
it("requests skipReload on every z-reorder persist (live DOM already final)", async () => {
|
|
125
|
+
// The commit applies the z-index (and any injected position) to the live
|
|
126
|
+
// iframe DOM and the store synchronously, so the persisted style-only patch
|
|
127
|
+
// adds nothing the preview doesn't already show — the batch commit is asked
|
|
128
|
+
// to skip the iframe remount. commitDomEditPatchBatches still falls back to
|
|
129
|
+
// reloading when the server can't confirm every patch target matched.
|
|
130
|
+
const el = document.createElement("div");
|
|
131
|
+
el.id = "clip-z";
|
|
132
|
+
|
|
133
|
+
const { captured, root } = await runReorderCommit(el, [
|
|
134
|
+
{ element: el, zIndex: 4, id: "clip-z", sourceFile: "index.html" },
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(captured).toHaveLength(1);
|
|
138
|
+
expect(captured[0]?.options.skipReload).toBe(true);
|
|
139
|
+
|
|
140
|
+
act(() => root.unmount());
|
|
141
|
+
});
|
|
142
|
+
|
|
112
143
|
it("preserves a real id when the element has one", async () => {
|
|
113
144
|
const el = document.createElement("video");
|
|
114
145
|
el.id = "v-hero";
|
|
@@ -176,6 +207,509 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
176
207
|
act(() => root.unmount());
|
|
177
208
|
});
|
|
178
209
|
|
|
210
|
+
it("keeps distinct actions in distinct default coalesce keys", async () => {
|
|
211
|
+
const el = document.createElement("div");
|
|
212
|
+
el.id = "clip-a";
|
|
213
|
+
document.body.appendChild(el);
|
|
214
|
+
const captured: CapturedBatchCall[] = [];
|
|
215
|
+
let commit: ReorderCommit | undefined;
|
|
216
|
+
const root = renderReorderHook(captured, (fn) => (commit = fn));
|
|
217
|
+
|
|
218
|
+
await act(async () => {
|
|
219
|
+
await commit!(
|
|
220
|
+
[{ element: el, zIndex: 1, id: "clip-a", sourceFile: "index.html" }],
|
|
221
|
+
undefined,
|
|
222
|
+
"bring-forward",
|
|
223
|
+
);
|
|
224
|
+
await commit!(
|
|
225
|
+
[{ element: el, zIndex: 0, id: "clip-a", sourceFile: "index.html" }],
|
|
226
|
+
undefined,
|
|
227
|
+
"send-backward",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Same element set, different actions — the keys must differ so the two
|
|
232
|
+
// edits never coalesce into one undo step. Each key also carries a fresh
|
|
233
|
+
// gesture sequence, which is what makes the commit's unbounded per-gesture
|
|
234
|
+
// coalesce window safe (see zReorderCoalesceKey).
|
|
235
|
+
expect(captured).toHaveLength(2);
|
|
236
|
+
expect(captured[0]?.options.coalesceKey).toMatch(/^z-reorder:bring-forward:clip-a:g\d+$/);
|
|
237
|
+
expect(captured[1]?.options.coalesceKey).toMatch(/^z-reorder:send-backward:clip-a:g\d+$/);
|
|
238
|
+
expect(captured[0]?.options.coalesceKey).not.toBe(captured[1]?.options.coalesceKey);
|
|
239
|
+
// The two-phase gesture fold rides an unbounded window on both records.
|
|
240
|
+
expect(captured[0]?.options.coalesceMs).toBe(Number.POSITIVE_INFINITY);
|
|
241
|
+
act(() => root.unmount());
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("updates the store zIndex synchronously for entries that carry a store key", async () => {
|
|
245
|
+
const el = document.createElement("div");
|
|
246
|
+
el.id = "clip-a";
|
|
247
|
+
document.body.appendChild(el);
|
|
248
|
+
usePlayerStore.getState().setElements([
|
|
249
|
+
{
|
|
250
|
+
id: "clip-a",
|
|
251
|
+
key: "index.html#clip-a",
|
|
252
|
+
tag: "div",
|
|
253
|
+
start: 0,
|
|
254
|
+
duration: 1,
|
|
255
|
+
track: 0,
|
|
256
|
+
zIndex: 0,
|
|
257
|
+
hasExplicitZIndex: false,
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
let commit: ReorderCommit | undefined;
|
|
262
|
+
let resolveBatch: (() => void) | undefined;
|
|
263
|
+
const batchResult = { durable: true, allMatched: true, changed: true };
|
|
264
|
+
function Harness() {
|
|
265
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
266
|
+
makeLifecycleOpsParams({
|
|
267
|
+
// Persist stays pending so the assertion below can only be satisfied
|
|
268
|
+
// by the SYNCHRONOUS store update (the lane-sync path's requirement).
|
|
269
|
+
commitDomEditPatchBatches: () =>
|
|
270
|
+
new Promise((resolve) => (resolveBatch = () => resolve(batchResult))),
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
commit = handleDomZIndexReorderCommit;
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const root = mountReactHarness(<Harness />);
|
|
277
|
+
|
|
278
|
+
let pending: Promise<unknown> | undefined;
|
|
279
|
+
act(() => {
|
|
280
|
+
pending = commit!([
|
|
281
|
+
{
|
|
282
|
+
element: el,
|
|
283
|
+
zIndex: 5,
|
|
284
|
+
id: "clip-a",
|
|
285
|
+
sourceFile: "index.html",
|
|
286
|
+
key: "index.html#clip-a",
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(usePlayerStore.getState().elements[0]).toMatchObject({
|
|
292
|
+
zIndex: 5,
|
|
293
|
+
hasExplicitZIndex: true,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
resolveBatch?.();
|
|
297
|
+
await act(async () => pending);
|
|
298
|
+
act(() => root.unmount());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// The canvas context-menu path: the menu no longer pre-applies styles, so the
|
|
302
|
+
// hook sees the PRISTINE element — prior styles are captured before any
|
|
303
|
+
// mutation and a failed persist restores them exactly (previously the menu's
|
|
304
|
+
// optimistic write made the "rollback" restore the already-mutated values,
|
|
305
|
+
// and the never-persisted position patch silently reverted on reload).
|
|
306
|
+
it("rolls back a static, inline-style-free element to pristine styles on failure", async () => {
|
|
307
|
+
const el = document.createElement("div");
|
|
308
|
+
el.id = "clip-a";
|
|
309
|
+
el.style.position = "static"; // happy-dom computes "" for unset position
|
|
310
|
+
document.body.appendChild(el);
|
|
311
|
+
const failure = new Error("persist failed");
|
|
312
|
+
|
|
313
|
+
let commit: ReorderCommit | undefined;
|
|
314
|
+
function Harness() {
|
|
315
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
316
|
+
makeLifecycleOpsParams({
|
|
317
|
+
commitDomEditPatchBatches: vi.fn(async () => {
|
|
318
|
+
// The live styles were applied by the hook before persist ran.
|
|
319
|
+
expect(el.style.zIndex).toBe("2");
|
|
320
|
+
expect(el.style.position).toBe("relative");
|
|
321
|
+
throw failure;
|
|
322
|
+
}),
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
commit = handleDomZIndexReorderCommit;
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const root = mountReactHarness(<Harness />);
|
|
329
|
+
|
|
330
|
+
let rejection: unknown;
|
|
331
|
+
await act(async () => {
|
|
332
|
+
try {
|
|
333
|
+
await commit!(
|
|
334
|
+
[{ element: el, zIndex: 2, id: "clip-a", sourceFile: "index.html" }],
|
|
335
|
+
undefined,
|
|
336
|
+
"bring-forward",
|
|
337
|
+
);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
rejection = error;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(rejection).toBe(failure);
|
|
344
|
+
expect(el.style.zIndex).toBe("");
|
|
345
|
+
expect(el.style.position).toBe("static");
|
|
346
|
+
act(() => root.unmount());
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("returns an active reveal lift and its original styles to their owner on failure", async () => {
|
|
350
|
+
const el = document.createElement("div");
|
|
351
|
+
el.id = "clip-a";
|
|
352
|
+
el.style.zIndex = "4";
|
|
353
|
+
el.style.position = "static";
|
|
354
|
+
document.body.appendChild(el);
|
|
355
|
+
const lift = liftElementToTop(el)!;
|
|
356
|
+
const failure = new Error("persist failed");
|
|
357
|
+
|
|
358
|
+
expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
|
|
359
|
+
expect(el.style.position).toBe("relative");
|
|
360
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe("4");
|
|
361
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR)).toBe("static");
|
|
362
|
+
expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(false);
|
|
363
|
+
|
|
364
|
+
let commit: ReorderCommit | undefined;
|
|
365
|
+
function Harness() {
|
|
366
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
367
|
+
makeLifecycleOpsParams({
|
|
368
|
+
commitDomEditPatchBatches: vi.fn(async () => {
|
|
369
|
+
expect(el.style.zIndex).toBe("8");
|
|
370
|
+
expect(el.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
|
|
371
|
+
expect(el.hasAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR)).toBe(false);
|
|
372
|
+
throw failure;
|
|
373
|
+
}),
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
commit = handleDomZIndexReorderCommit;
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const root = mountReactHarness(<Harness />);
|
|
380
|
+
|
|
381
|
+
let rejection: unknown;
|
|
382
|
+
await act(async () => {
|
|
383
|
+
try {
|
|
384
|
+
await commit!([{ element: el, zIndex: 8, id: "clip-a", sourceFile: "index.html" }]);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
rejection = error;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
expect(rejection).toBe(failure);
|
|
390
|
+
|
|
391
|
+
// The failed optimistic commit must put the still-active lift back exactly
|
|
392
|
+
// as it found it, including the metadata that lets reveal cleanup restore
|
|
393
|
+
// the authored z/position rather than abandoning the temporary lift.
|
|
394
|
+
expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
|
|
395
|
+
expect(el.style.position).toBe("relative");
|
|
396
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe("4");
|
|
397
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR)).toBe("static");
|
|
398
|
+
expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(false);
|
|
399
|
+
|
|
400
|
+
restoreLiftedElement(el, lift);
|
|
401
|
+
expect(el.style.zIndex).toBe("4");
|
|
402
|
+
expect(el.style.position).toBe("static");
|
|
403
|
+
act(() => root.unmount());
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("reconciles live, store, and reveal state when no patch target matched", async () => {
|
|
407
|
+
const el = document.createElement("div");
|
|
408
|
+
el.id = "clip-a";
|
|
409
|
+
el.style.zIndex = "4";
|
|
410
|
+
el.style.position = "static";
|
|
411
|
+
document.body.appendChild(el);
|
|
412
|
+
usePlayerStore.getState().setElements([
|
|
413
|
+
{
|
|
414
|
+
id: "clip-a",
|
|
415
|
+
tag: "div",
|
|
416
|
+
start: 0,
|
|
417
|
+
duration: 1,
|
|
418
|
+
track: 0,
|
|
419
|
+
zIndex: 4,
|
|
420
|
+
hasExplicitZIndex: true,
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
const lift = liftElementToTop(el)!;
|
|
424
|
+
const unmatched = { durable: false, allMatched: false, changed: false };
|
|
425
|
+
let commit: ReorderCommit | undefined;
|
|
426
|
+
|
|
427
|
+
function Harness() {
|
|
428
|
+
({ handleDomZIndexReorderCommit: commit } = useElementLifecycleOps(
|
|
429
|
+
makeLifecycleOpsParams({
|
|
430
|
+
commitDomEditPatchBatches: vi.fn(async () => unmatched),
|
|
431
|
+
}),
|
|
432
|
+
));
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const root = mountReactHarness(<Harness />);
|
|
436
|
+
|
|
437
|
+
let result: unknown;
|
|
438
|
+
await act(async () => {
|
|
439
|
+
result = await commit!([
|
|
440
|
+
{
|
|
441
|
+
element: el,
|
|
442
|
+
zIndex: 8,
|
|
443
|
+
id: "clip-a",
|
|
444
|
+
sourceFile: "index.html",
|
|
445
|
+
key: "clip-a",
|
|
446
|
+
},
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(result).toEqual(unmatched);
|
|
451
|
+
expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
|
|
452
|
+
expect(el.style.position).toBe("relative");
|
|
453
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe("4");
|
|
454
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR)).toBe("static");
|
|
455
|
+
expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(false);
|
|
456
|
+
expect(usePlayerStore.getState().elements[0]).toMatchObject({
|
|
457
|
+
zIndex: 4,
|
|
458
|
+
hasExplicitZIndex: true,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
restoreLiftedElement(el, lift);
|
|
462
|
+
expect(el.style.zIndex).toBe("4");
|
|
463
|
+
expect(el.style.position).toBe("static");
|
|
464
|
+
act(() => root.unmount());
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it.each(["rejection", "unmatched"] as const)(
|
|
468
|
+
"defers a delayed reveal until a pending z commit finishes with %s",
|
|
469
|
+
async (outcome) => {
|
|
470
|
+
vi.useFakeTimers();
|
|
471
|
+
const el = document.createElement("div");
|
|
472
|
+
el.id = "clip-a";
|
|
473
|
+
el.style.zIndex = "4";
|
|
474
|
+
el.style.position = "absolute";
|
|
475
|
+
document.body.appendChild(el);
|
|
476
|
+
usePlayerStore.getState().setElements([
|
|
477
|
+
{
|
|
478
|
+
id: "clip-a",
|
|
479
|
+
tag: "div",
|
|
480
|
+
start: 0,
|
|
481
|
+
duration: 1,
|
|
482
|
+
track: 0,
|
|
483
|
+
zIndex: 4,
|
|
484
|
+
hasExplicitZIndex: true,
|
|
485
|
+
},
|
|
486
|
+
]);
|
|
487
|
+
let resolvePersist: ((result: DomEditPatchBatchesResult) => void) | undefined;
|
|
488
|
+
let rejectPersist: ((error: Error) => void) | undefined;
|
|
489
|
+
const persist = vi.fn(
|
|
490
|
+
() =>
|
|
491
|
+
new Promise<DomEditPatchBatchesResult>((resolve, reject) => {
|
|
492
|
+
resolvePersist = resolve;
|
|
493
|
+
rejectPersist = reject;
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
let commit: ReorderCommit | undefined;
|
|
497
|
+
let scheduleReveal: ((element: HTMLElement, delayMs: number) => void) | undefined;
|
|
498
|
+
|
|
499
|
+
function Harness() {
|
|
500
|
+
({ scheduleReveal } = useLayerRevealOverride({
|
|
501
|
+
isPlaying: false,
|
|
502
|
+
selectedElement: el,
|
|
503
|
+
}));
|
|
504
|
+
({ handleDomZIndexReorderCommit: commit } = useElementLifecycleOps(
|
|
505
|
+
makeLifecycleOpsParams({ commitDomEditPatchBatches: persist }),
|
|
506
|
+
));
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const root = mountReactHarness(<Harness />);
|
|
510
|
+
|
|
511
|
+
let pending: Promise<unknown> | undefined;
|
|
512
|
+
act(() => {
|
|
513
|
+
scheduleReveal!(el, 10);
|
|
514
|
+
pending = commit!([
|
|
515
|
+
{ element: el, zIndex: 8, id: "clip-a", sourceFile: "index.html", key: "clip-a" },
|
|
516
|
+
]);
|
|
517
|
+
vi.advanceTimersByTime(10);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// The timer elapsed, but it must not capture optimistic z=8 as authored.
|
|
521
|
+
expect(el.style.zIndex).toBe("8");
|
|
522
|
+
expect(el.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
|
|
523
|
+
|
|
524
|
+
let rejection: unknown;
|
|
525
|
+
await act(async () => {
|
|
526
|
+
if (outcome === "rejection") rejectPersist!(new Error("save failed"));
|
|
527
|
+
else resolvePersist!({ durable: false, allMatched: false, changed: false });
|
|
528
|
+
try {
|
|
529
|
+
await pending;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
rejection = error;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
if (outcome === "rejection") expect(rejection).toBeInstanceOf(Error);
|
|
535
|
+
|
|
536
|
+
act(() => vi.advanceTimersByTime(16));
|
|
537
|
+
expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
|
|
538
|
+
expect(el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe("4");
|
|
539
|
+
expect(usePlayerStore.getState().elements[0]?.zIndex).toBe(4);
|
|
540
|
+
act(() => root.unmount());
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
it("serializes overlapping commits through the shared gesture owner", async () => {
|
|
545
|
+
const el = document.createElement("div");
|
|
546
|
+
el.id = "clip-a";
|
|
547
|
+
el.style.zIndex = "1";
|
|
548
|
+
document.body.appendChild(el);
|
|
549
|
+
usePlayerStore.getState().setElements([
|
|
550
|
+
{
|
|
551
|
+
id: "clip-a",
|
|
552
|
+
tag: "div",
|
|
553
|
+
start: 0,
|
|
554
|
+
duration: 1,
|
|
555
|
+
track: 0,
|
|
556
|
+
zIndex: 1,
|
|
557
|
+
hasExplicitZIndex: true,
|
|
558
|
+
},
|
|
559
|
+
]);
|
|
560
|
+
const firstFailure = new Error("first persist failed");
|
|
561
|
+
let rejectFirst: ((error: Error) => void) | undefined;
|
|
562
|
+
let resolveSecond: (() => void) | undefined;
|
|
563
|
+
const persist = vi
|
|
564
|
+
.fn()
|
|
565
|
+
.mockImplementationOnce(
|
|
566
|
+
() =>
|
|
567
|
+
new Promise<never>((_resolve, reject) => {
|
|
568
|
+
rejectFirst = reject;
|
|
569
|
+
}),
|
|
570
|
+
)
|
|
571
|
+
.mockImplementationOnce(
|
|
572
|
+
() =>
|
|
573
|
+
new Promise((resolve) => {
|
|
574
|
+
resolveSecond = () => resolve({ durable: true, allMatched: true, changed: true });
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
let commit: ReorderCommit | undefined;
|
|
578
|
+
|
|
579
|
+
function Harness() {
|
|
580
|
+
({ handleDomZIndexReorderCommit: commit } = useElementLifecycleOps(
|
|
581
|
+
makeLifecycleOpsParams({ commitDomEditPatchBatches: persist }),
|
|
582
|
+
));
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const root = mountReactHarness(<Harness />);
|
|
586
|
+
|
|
587
|
+
let firstPending: Promise<unknown> | undefined;
|
|
588
|
+
let secondPending: Promise<unknown> | undefined;
|
|
589
|
+
act(() => {
|
|
590
|
+
firstPending = runZLaneGesture({
|
|
591
|
+
commitZ: () =>
|
|
592
|
+
commit!([
|
|
593
|
+
{ element: el, zIndex: 2, id: "clip-a", sourceFile: "index.html", key: "clip-a" },
|
|
594
|
+
]),
|
|
595
|
+
mirror: async () => true,
|
|
596
|
+
});
|
|
597
|
+
secondPending = runZLaneGesture({
|
|
598
|
+
commitZ: () =>
|
|
599
|
+
commit!([
|
|
600
|
+
{ element: el, zIndex: 3, id: "clip-a", sourceFile: "index.html", key: "clip-a" },
|
|
601
|
+
]),
|
|
602
|
+
mirror: async () => true,
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
607
|
+
expect(el.style.zIndex).toBe("2");
|
|
608
|
+
expect(usePlayerStore.getState().elements[0]?.zIndex).toBe(2);
|
|
609
|
+
|
|
610
|
+
let rejection: unknown;
|
|
611
|
+
await act(async () => {
|
|
612
|
+
rejectFirst!(firstFailure);
|
|
613
|
+
try {
|
|
614
|
+
await firstPending;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
rejection = error;
|
|
617
|
+
}
|
|
618
|
+
await Promise.resolve();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
expect(rejection).toBe(firstFailure);
|
|
622
|
+
expect(persist).toHaveBeenCalledTimes(2);
|
|
623
|
+
expect(el.style.zIndex).toBe("3");
|
|
624
|
+
expect(usePlayerStore.getState().elements[0]?.zIndex).toBe(3);
|
|
625
|
+
|
|
626
|
+
await act(async () => {
|
|
627
|
+
resolveSecond!();
|
|
628
|
+
await secondPending;
|
|
629
|
+
});
|
|
630
|
+
expect(el.style.zIndex).toBe("3");
|
|
631
|
+
expect(usePlayerStore.getState().elements[0]?.zIndex).toBe(3);
|
|
632
|
+
act(() => root.unmount());
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it.each(["deselection", "playback"] as const)(
|
|
636
|
+
"does not resurrect a reveal released by %s while persistence is pending",
|
|
637
|
+
async (releaseBy) => {
|
|
638
|
+
vi.useFakeTimers();
|
|
639
|
+
const el = document.createElement("div");
|
|
640
|
+
el.id = "clip-a";
|
|
641
|
+
el.style.zIndex = "4";
|
|
642
|
+
el.style.position = "static";
|
|
643
|
+
const other = document.createElement("div");
|
|
644
|
+
document.body.append(el, other);
|
|
645
|
+
const failure = new Error("persist failed");
|
|
646
|
+
let rejectPersist: ((error: Error) => void) | undefined;
|
|
647
|
+
const persist = vi.fn(
|
|
648
|
+
() =>
|
|
649
|
+
new Promise<never>((_resolve, reject) => {
|
|
650
|
+
rejectPersist = reject;
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
let commit: ReorderCommit | undefined;
|
|
654
|
+
let scheduleReveal: ((element: HTMLElement, delayMs: number) => void) | undefined;
|
|
655
|
+
|
|
656
|
+
function Harness({
|
|
657
|
+
selectedElement,
|
|
658
|
+
isPlaying,
|
|
659
|
+
}: {
|
|
660
|
+
selectedElement: HTMLElement | null;
|
|
661
|
+
isPlaying: boolean;
|
|
662
|
+
}) {
|
|
663
|
+
({ scheduleReveal } = useLayerRevealOverride({ isPlaying, selectedElement }));
|
|
664
|
+
({ handleDomZIndexReorderCommit: commit } = useElementLifecycleOps(
|
|
665
|
+
makeLifecycleOpsParams({ commitDomEditPatchBatches: persist }),
|
|
666
|
+
));
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const root = mountReactHarness(<Harness selectedElement={el} isPlaying={false} />);
|
|
671
|
+
act(() => {
|
|
672
|
+
scheduleReveal!(el, 0);
|
|
673
|
+
vi.advanceTimersByTime(0);
|
|
674
|
+
});
|
|
675
|
+
expect(el.style.zIndex).toBe(LAYER_REVEAL_LIFT_Z);
|
|
676
|
+
|
|
677
|
+
let pending: Promise<unknown> | undefined;
|
|
678
|
+
act(() => {
|
|
679
|
+
pending = commit!([{ element: el, zIndex: 8, id: el.id, sourceFile: "index.html" }]);
|
|
680
|
+
});
|
|
681
|
+
expect(el.style.zIndex).toBe("8");
|
|
682
|
+
expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(true);
|
|
683
|
+
|
|
684
|
+
act(() => {
|
|
685
|
+
root.render(
|
|
686
|
+
<Harness
|
|
687
|
+
selectedElement={releaseBy === "deselection" ? other : el}
|
|
688
|
+
isPlaying={releaseBy === "playback"}
|
|
689
|
+
/>,
|
|
690
|
+
);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
let rejection: unknown;
|
|
694
|
+
await act(async () => {
|
|
695
|
+
rejectPersist!(failure);
|
|
696
|
+
try {
|
|
697
|
+
await pending;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
rejection = error;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
expect(rejection).toBe(failure);
|
|
704
|
+
expect(el.style.zIndex).toBe("4");
|
|
705
|
+
expect(el.style.position).toBe("static");
|
|
706
|
+
expect(el.hasAttribute(LAYER_REVEAL_PRIOR_Z_ATTR)).toBe(false);
|
|
707
|
+
expect(el.hasAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR)).toBe(false);
|
|
708
|
+
expect(el.hasAttribute(LAYER_REVEAL_PENDING_COMMIT_ATTR)).toBe(false);
|
|
709
|
+
act(() => root.unmount());
|
|
710
|
+
},
|
|
711
|
+
);
|
|
712
|
+
|
|
179
713
|
it("rolls back only live and store state after an atomic reorder failure", async () => {
|
|
180
714
|
const writeProjectFile = vi.fn(async () => {});
|
|
181
715
|
const recordEdit = vi.fn(async () => {});
|
|
@@ -202,20 +736,17 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
202
736
|
|
|
203
737
|
let commit: ReorderCommit | undefined;
|
|
204
738
|
function Harness() {
|
|
205
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
forceReloadSdkSession,
|
|
215
|
-
commitDomEditPatchBatches: vi.fn(async () => {
|
|
216
|
-
throw originalError;
|
|
739
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
740
|
+
makeLifecycleOpsParams({
|
|
741
|
+
writeProjectFile,
|
|
742
|
+
editHistory: { recordEdit },
|
|
743
|
+
projectIdRef: { current: "demo" },
|
|
744
|
+
forceReloadSdkSession,
|
|
745
|
+
commitDomEditPatchBatches: vi.fn(async () => {
|
|
746
|
+
throw originalError;
|
|
747
|
+
}),
|
|
217
748
|
}),
|
|
218
|
-
|
|
749
|
+
);
|
|
219
750
|
commit = handleDomZIndexReorderCommit;
|
|
220
751
|
return null;
|
|
221
752
|
}
|