@hyperframes/studio 0.7.57 → 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--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
- package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
- package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-Bf1x1y8H.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 +15 -4
- package/dist/index.html +2 -2
- package/dist/index.js +5954 -4474
- 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 +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- 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/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- 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/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +143 -78
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +174 -81
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineEditing.test.tsx +302 -154
- package/src/hooks/useTimelineEditing.ts +97 -93
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +35 -3
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- 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 +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- 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/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- 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 +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -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/dist/assets/index-_pqzyxB1.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,13 @@ 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;
|
|
21
36
|
skipReload?: boolean;
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -38,26 +53,21 @@ type ReorderCommit = (
|
|
|
38
53
|
}>,
|
|
39
54
|
coalesceKeyOverride?: string,
|
|
40
55
|
actionKind?: string,
|
|
41
|
-
) => Promise<void>;
|
|
56
|
+
) => Promise<DomEditPatchBatchesResult | undefined | void>;
|
|
42
57
|
|
|
43
58
|
function renderReorderHook(
|
|
44
59
|
capturedCalls: CapturedBatchCall[],
|
|
45
60
|
onReady: (commit: ReorderCommit) => void,
|
|
46
61
|
) {
|
|
47
62
|
function Harness() {
|
|
48
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
clearDomSelection: vi.fn(),
|
|
57
|
-
commitDomEditPatchBatches: async (batches, options) => {
|
|
58
|
-
capturedCalls.push({ batches, options });
|
|
59
|
-
},
|
|
60
|
-
});
|
|
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
|
+
);
|
|
61
71
|
onReady(handleDomZIndexReorderCommit);
|
|
62
72
|
return null;
|
|
63
73
|
}
|
|
@@ -219,10 +229,15 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
219
229
|
});
|
|
220
230
|
|
|
221
231
|
// Same element set, different actions — the keys must differ so the two
|
|
222
|
-
// edits never coalesce into one undo step
|
|
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).
|
|
223
235
|
expect(captured).toHaveLength(2);
|
|
224
|
-
expect(captured[0]?.options.coalesceKey).
|
|
225
|
-
expect(captured[1]?.options.coalesceKey).
|
|
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);
|
|
226
241
|
act(() => root.unmount());
|
|
227
242
|
});
|
|
228
243
|
|
|
@@ -245,26 +260,22 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
245
260
|
|
|
246
261
|
let commit: ReorderCommit | undefined;
|
|
247
262
|
let resolveBatch: (() => void) | undefined;
|
|
263
|
+
const batchResult = { durable: true, allMatched: true, changed: true };
|
|
248
264
|
function Harness() {
|
|
249
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
clearDomSelection: vi.fn(),
|
|
258
|
-
// Persist stays pending so the assertion below can only be satisfied
|
|
259
|
-
// by the SYNCHRONOUS store update (the lane-sync path's requirement).
|
|
260
|
-
commitDomEditPatchBatches: () => new Promise((resolve) => (resolveBatch = resolve)),
|
|
261
|
-
});
|
|
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
|
+
);
|
|
262
273
|
commit = handleDomZIndexReorderCommit;
|
|
263
274
|
return null;
|
|
264
275
|
}
|
|
265
276
|
const root = mountReactHarness(<Harness />);
|
|
266
277
|
|
|
267
|
-
let pending: Promise<
|
|
278
|
+
let pending: Promise<unknown> | undefined;
|
|
268
279
|
act(() => {
|
|
269
280
|
pending = commit!([
|
|
270
281
|
{
|
|
@@ -301,22 +312,16 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
301
312
|
|
|
302
313
|
let commit: ReorderCommit | undefined;
|
|
303
314
|
function Harness() {
|
|
304
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
clearDomSelection: vi.fn(),
|
|
313
|
-
commitDomEditPatchBatches: vi.fn(async () => {
|
|
314
|
-
// The live styles were applied by the hook before persist ran.
|
|
315
|
-
expect(el.style.zIndex).toBe("2");
|
|
316
|
-
expect(el.style.position).toBe("relative");
|
|
317
|
-
throw failure;
|
|
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
|
+
}),
|
|
318
323
|
}),
|
|
319
|
-
|
|
324
|
+
);
|
|
320
325
|
commit = handleDomZIndexReorderCommit;
|
|
321
326
|
return null;
|
|
322
327
|
}
|
|
@@ -341,6 +346,370 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
341
346
|
act(() => root.unmount());
|
|
342
347
|
});
|
|
343
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
|
+
|
|
344
713
|
it("rolls back only live and store state after an atomic reorder failure", async () => {
|
|
345
714
|
const writeProjectFile = vi.fn(async () => {});
|
|
346
715
|
const recordEdit = vi.fn(async () => {});
|
|
@@ -367,20 +736,17 @@ describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
|
367
736
|
|
|
368
737
|
let commit: ReorderCommit | undefined;
|
|
369
738
|
function Harness() {
|
|
370
|
-
const { handleDomZIndexReorderCommit } = useElementLifecycleOps(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
forceReloadSdkSession,
|
|
380
|
-
commitDomEditPatchBatches: vi.fn(async () => {
|
|
381
|
-
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
|
+
}),
|
|
382
748
|
}),
|
|
383
|
-
|
|
749
|
+
);
|
|
384
750
|
commit = handleDomZIndexReorderCommit;
|
|
385
751
|
return null;
|
|
386
752
|
}
|