@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.
Files changed (112) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
  2. package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
  3. package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
  4. package/dist/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
  5. package/dist/assets/index-uahwWkgw.css +1 -0
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +15 -4
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +5954 -4474
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +3 -3
  15. package/src/components/StudioFeedbackBar.tsx +2 -2
  16. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  17. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  18. package/src/components/editor/DomEditOverlay.tsx +5 -2
  19. package/src/components/editor/LayersPanel.tsx +110 -99
  20. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  21. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  22. package/src/components/editor/domEditingDom.ts +5 -6
  23. package/src/components/editor/domEditingElement.ts +15 -32
  24. package/src/components/editor/layersPanelSort.ts +80 -0
  25. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  26. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  27. package/src/components/nle/PreviewOverlays.tsx +27 -2
  28. package/src/components/nle/TimelinePane.test.ts +3 -0
  29. package/src/components/nle/TimelinePane.tsx +11 -1
  30. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  31. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  32. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  33. package/src/components/nle/zLaneGesture.test.ts +176 -0
  34. package/src/components/nle/zLaneGesture.ts +77 -0
  35. package/src/hooks/domEditCommitTypes.ts +17 -1
  36. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  37. package/src/hooks/fetchStubTestUtils.ts +16 -0
  38. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  39. package/src/hooks/timelineEditingHelpers.ts +32 -15
  40. package/src/hooks/timelineMoveAdapter.ts +2 -1
  41. package/src/hooks/timelineTimingSync.test.ts +703 -33
  42. package/src/hooks/timelineTimingSync.ts +282 -97
  43. package/src/hooks/timelineTrackVisibility.test.ts +3 -1
  44. package/src/hooks/timelineTrackVisibility.ts +4 -3
  45. package/src/hooks/useClipboard.ts +19 -17
  46. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  47. package/src/hooks/useDomEditCommits.ts +143 -78
  48. package/src/hooks/useDomEditSession.test.tsx +3 -1
  49. package/src/hooks/useDomEditSession.ts +1 -1
  50. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  51. package/src/hooks/useElementLifecycleOps.ts +174 -81
  52. package/src/hooks/usePreviewPersistence.ts +2 -2
  53. package/src/hooks/useTimelineEditing.test.tsx +302 -154
  54. package/src/hooks/useTimelineEditing.ts +97 -93
  55. package/src/hooks/useTimelineEditingTypes.ts +1 -1
  56. package/src/hooks/useTimelineGroupEditing.ts +35 -3
  57. package/src/player/components/Timeline.test.ts +31 -21
  58. package/src/player/components/Timeline.tsx +62 -91
  59. package/src/player/components/TimelineCanvas.tsx +36 -5
  60. package/src/player/components/TimelineLanes.tsx +32 -10
  61. package/src/player/components/TimelineOverlays.tsx +36 -0
  62. package/src/player/components/TimelineRuler.tsx +18 -5
  63. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  64. package/src/player/components/timelineCallbacks.ts +4 -1
  65. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  66. package/src/player/components/timelineClipDragCommit.ts +173 -71
  67. package/src/player/components/timelineGapCommit.test.ts +163 -0
  68. package/src/player/components/timelineGapCommit.ts +103 -0
  69. package/src/player/components/timelineGaps.test.ts +221 -0
  70. package/src/player/components/timelineGaps.ts +182 -0
  71. package/src/player/components/timelineLayout.test.ts +2 -1
  72. package/src/player/components/timelineLayout.ts +22 -6
  73. package/src/player/components/timelineMarquee.test.ts +20 -10
  74. package/src/player/components/timelineMarquee.ts +9 -2
  75. package/src/player/components/timelineStackingSync.test.ts +42 -1
  76. package/src/player/components/timelineStackingSync.ts +31 -16
  77. package/src/player/components/timelineZMirror.test.ts +667 -0
  78. package/src/player/components/timelineZMirror.ts +393 -0
  79. package/src/player/components/timelineZoom.test.ts +2 -2
  80. package/src/player/components/timelineZoom.ts +7 -3
  81. package/src/player/components/useTimelineClipDrag.ts +1 -1
  82. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  83. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  84. package/src/player/components/useTimelinePlayhead.ts +4 -3
  85. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  86. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  87. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  88. package/src/player/components/useTimelineStackingSync.ts +5 -3
  89. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  90. package/src/player/components/useTrackGapMenu.ts +155 -0
  91. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  92. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  93. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  94. package/src/player/lib/layerOrdering.test.ts +1 -15
  95. package/src/player/lib/layerOrdering.ts +5 -28
  96. package/src/player/lib/timelineDOM.test.ts +72 -0
  97. package/src/player/lib/timelineDOM.ts +3 -0
  98. package/src/player/lib/timelineElementHelpers.ts +35 -11
  99. package/src/player/store/playerStore.ts +12 -1
  100. package/src/telemetry/events.test.ts +10 -0
  101. package/src/telemetry/events.ts +1 -0
  102. package/src/utils/domEditSaveQueue.ts +4 -3
  103. package/src/utils/editHistory.test.ts +82 -0
  104. package/src/utils/editHistory.ts +14 -2
  105. package/src/utils/gsapSoftReload.test.ts +54 -115
  106. package/src/utils/gsapSoftReload.ts +42 -152
  107. package/src/utils/gsapUndoRestore.test.ts +269 -0
  108. package/src/utils/gsapUndoRestore.ts +269 -0
  109. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  110. package/dist/assets/index-_pqzyxB1.css +0 -1
  111. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  112. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -276,18 +276,20 @@ describe("useDomEditCommits z-index reorder persistence", () => {
276
276
  _init?: Parameters<typeof fetch>[1],
277
277
  ): Promise<Response> => {
278
278
  const url = requestUrl(input);
279
- if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: original });
280
- if (url.includes("/file-mutations/patch-elements-batch/")) {
279
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
281
280
  return jsonResponse({
282
- ok: true,
283
- changed: true,
284
- matched: [true, true, true],
285
- content: after,
281
+ durable: true,
282
+ files: [
283
+ {
284
+ sourceFile: "index.html",
285
+ changed: true,
286
+ matched: [true, true, true],
287
+ before: original,
288
+ after,
289
+ },
290
+ ],
286
291
  });
287
292
  }
288
- if (url.includes("/file-mutations/patch-element/")) {
289
- return jsonResponse({ ok: true, changed: true, matched: true, content: after });
290
- }
291
293
  throw new Error(`Unexpected fetch: ${url}`);
292
294
  },
293
295
  );
@@ -317,25 +319,34 @@ describe("useDomEditCommits z-index reorder persistence", () => {
317
319
  });
318
320
 
319
321
  const batchPosts = fetchMock.mock.calls.filter(([input]) =>
320
- requestUrl(input).includes("/file-mutations/patch-elements-batch/"),
322
+ requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
321
323
  );
322
324
  const singlePosts = fetchMock.mock.calls.filter(([input]) =>
323
- requestUrl(input).includes("/file-mutations/patch-element/"),
325
+ requestUrl(input).includes("/file-mutations/patch-elements-batch/"),
324
326
  );
325
327
  expect(batchPosts).toHaveLength(1);
326
328
  expect(singlePosts).toHaveLength(0);
327
329
  expect(JSON.parse(String(batchPosts[0]?.[1]?.body))).toEqual({
328
- patches: expect.arrayContaining([
329
- expect.objectContaining({ target: expect.objectContaining({ id: "a" }) }),
330
- expect.objectContaining({ target: expect.objectContaining({ id: "b" }) }),
331
- expect.objectContaining({ target: expect.objectContaining({ id: "c" }) }),
332
- ]),
330
+ batches: [
331
+ {
332
+ sourceFile: "index.html",
333
+ patches: expect.arrayContaining([
334
+ expect.objectContaining({ target: expect.objectContaining({ id: "a" }) }),
335
+ expect.objectContaining({ target: expect.objectContaining({ id: "b" }) }),
336
+ expect.objectContaining({ target: expect.objectContaining({ id: "c" }) }),
337
+ ]),
338
+ },
339
+ ],
333
340
  });
334
341
  expect(rendered.recordEdit).toHaveBeenCalledTimes(1);
335
342
  expect(rendered.recordEdit).toHaveBeenCalledWith({
336
343
  label: "Reorder layers",
337
344
  kind: "manual",
338
345
  coalesceKey: "z-reorder:test",
346
+ // Unbounded per-gesture fold window (keys are unique per gesture):
347
+ // the z entry and its mirror/lane counterpart fold across the server
348
+ // round-trip that separates them.
349
+ coalesceMs: Number.POSITIVE_INFINITY,
339
350
  files: { "index.html": { before: original, after } },
340
351
  });
341
352
  // FIX: a z-only reorder must NOT remount the preview iframe ("the blink").
@@ -354,9 +365,11 @@ describe("useDomEditCommits z-index reorder persistence", () => {
354
365
  const after = '<div id="a" style="z-index: 2"></div>';
355
366
  const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
356
367
  const url = requestUrl(input);
357
- if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: original });
358
- if (url.includes("/file-mutations/patch-elements-batch/")) {
359
- return jsonResponse({ ok: true, changed: true, content: after });
368
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
369
+ return jsonResponse({
370
+ durable: true,
371
+ files: [{ sourceFile: "index.html", changed: true, before: original, after }],
372
+ });
360
373
  }
361
374
  throw new Error(`Unexpected fetch: ${url}`);
362
375
  });
@@ -380,18 +393,26 @@ describe("useDomEditCommits z-index reorder persistence", () => {
380
393
  });
381
394
 
382
395
  it("warns and reports telemetry for unmatched batch patches without throwing", async () => {
383
- // The server reports per-patch matched[]: #b was not found in the source.
384
- // The matched subset persisted, so the commit must complete (no rollback of
385
- // applied state) while surfacing the partial failure. An unmatched target
386
- // also means the live DOM shows z-order the disk lacks, so the skip-reload
387
- // path must NOT engage — the reload reconverges the preview with disk.
396
+ // The server reports per-patch matched[]: #b was not found in the source,
397
+ // so it atomically refuses the whole multi-file gesture. The reload
398
+ // reconverges the preview with disk while the lifecycle owner rolls back
399
+ // both optimistic writes.
388
400
  const original = '<div id="a" style="z-index: 1"></div>';
389
- const after = '<div id="a" style="z-index: 2"></div>';
390
401
  const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
391
402
  const url = requestUrl(input);
392
- if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: original });
393
- if (url.includes("/file-mutations/patch-elements-batch/")) {
394
- return jsonResponse({ ok: true, changed: true, matched: [true, false], content: after });
403
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
404
+ return jsonResponse({
405
+ durable: false,
406
+ files: [
407
+ {
408
+ sourceFile: "index.html",
409
+ changed: false,
410
+ matched: [true, false],
411
+ before: original,
412
+ after: original,
413
+ },
414
+ ],
415
+ });
395
416
  }
396
417
  throw new Error(`Unexpected fetch: ${url}`);
397
418
  });
@@ -412,10 +433,10 @@ describe("useDomEditCommits z-index reorder persistence", () => {
412
433
  ]);
413
434
  });
414
435
 
415
- // No throw: the applied live state stays, the matched subset is recorded.
416
- expect(element.style.zIndex).toBe("2");
417
- expect(second.style.zIndex).toBe("1");
418
- expect(rendered.recordEdit).toHaveBeenCalledTimes(1);
436
+ // No throw: incomplete durability rolls back both optimistic writes.
437
+ expect(element.style.zIndex).toBe("");
438
+ expect(second.style.zIndex).toBe("");
439
+ expect(rendered.recordEdit).not.toHaveBeenCalled();
419
440
  expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
420
441
  expect(warnSpy).toHaveBeenCalledWith(
421
442
  expect.stringContaining("could not match 1 patch target(s) in index.html"),
@@ -435,12 +456,125 @@ describe("useDomEditCommits z-index reorder persistence", () => {
435
456
  }
436
457
  });
437
458
 
438
- it("rolls back live state after a failed batch POST without a disk write-back", async () => {
439
- const original = '<div id="a" style="z-index: 7"></div><div id="b"></div>';
459
+ it("uses one atomic request and rolls back every file when one target is unmatched", async () => {
460
+ const indexOriginal = '<div id="a" style="z-index: 4"></div>';
461
+ const sceneOriginal = '<div id="b" style="z-index: 5"></div>';
462
+ const fetchMock = vi.fn(
463
+ async (
464
+ input: Parameters<typeof fetch>[0],
465
+ init?: Parameters<typeof fetch>[1],
466
+ ): Promise<Response> => {
467
+ const url = requestUrl(input);
468
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
469
+ expect(JSON.parse(String(init?.body))).toEqual({
470
+ batches: [
471
+ expect.objectContaining({ sourceFile: "index.html" }),
472
+ expect.objectContaining({ sourceFile: "scene.html" }),
473
+ ],
474
+ });
475
+ return jsonResponse({
476
+ durable: false,
477
+ files: [
478
+ {
479
+ sourceFile: "index.html",
480
+ changed: false,
481
+ matched: [true],
482
+ before: indexOriginal,
483
+ after: indexOriginal,
484
+ },
485
+ {
486
+ sourceFile: "scene.html",
487
+ changed: false,
488
+ matched: [false],
489
+ before: sceneOriginal,
490
+ after: sceneOriginal,
491
+ },
492
+ ],
493
+ });
494
+ }
495
+ throw new Error(`Unexpected fetch: ${url}`);
496
+ },
497
+ );
498
+ vi.stubGlobal("fetch", fetchMock);
499
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
500
+ const { iframe, element } = createPreviewElement(
501
+ '<div data-hf-id="hf-card" style="z-index: 4"></div><div id="b" style="z-index: 5"></div>',
502
+ );
503
+ element.id = "a";
504
+ const second = iframe.contentDocument!.getElementById("b")!;
505
+ const rendered = renderDomEditCommits(createSelection(element), iframe);
506
+
507
+ try {
508
+ let result:
509
+ | Awaited<ReturnType<typeof rendered.hook.handleDomZIndexReorderCommit>>
510
+ | undefined;
511
+ await act(async () => {
512
+ result = await rendered.hook.handleDomZIndexReorderCommit([
513
+ { element, zIndex: 8, id: "a", sourceFile: "index.html" },
514
+ { element: second, zIndex: 9, id: "b", sourceFile: "scene.html" },
515
+ ]);
516
+ });
517
+
518
+ expect(result).toEqual({ durable: false, allMatched: false, changed: false });
519
+ expect(element.style.zIndex).toBe("4");
520
+ expect(second.style.zIndex).toBe("5");
521
+ expect(rendered.recordEdit).not.toHaveBeenCalled();
522
+ expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
523
+ expect(fetchMock).toHaveBeenCalledTimes(1);
524
+ } finally {
525
+ warnSpy.mockRestore();
526
+ rendered.cleanup();
527
+ }
528
+ });
529
+
530
+ it("reloads and reports non-durable when an unmatched batch changes no bytes", async () => {
531
+ const original = '<div id="a" style="z-index: 1"></div>';
532
+ const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
533
+ const url = requestUrl(input);
534
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
535
+ return jsonResponse({
536
+ durable: false,
537
+ files: [
538
+ {
539
+ sourceFile: "index.html",
540
+ changed: false,
541
+ matched: [false],
542
+ before: original,
543
+ after: original,
544
+ },
545
+ ],
546
+ });
547
+ }
548
+ throw new Error(`Unexpected fetch: ${url}`);
549
+ });
550
+ vi.stubGlobal("fetch", fetchMock);
551
+ const { iframe, element } = createPreviewElement();
552
+ element.id = "a";
553
+ const rendered = renderDomEditCommits(createSelection(element), iframe);
554
+
555
+ try {
556
+ let result:
557
+ | Awaited<ReturnType<typeof rendered.hook.handleDomZIndexReorderCommit>>
558
+ | undefined;
559
+ await act(async () => {
560
+ result = await rendered.hook.handleDomZIndexReorderCommit([
561
+ { element, zIndex: 2, id: "missing", sourceFile: "index.html" },
562
+ ]);
563
+ });
564
+
565
+ expect(result).toEqual({ durable: false, allMatched: false, changed: false });
566
+ expect(element.style.zIndex).toBe("");
567
+ expect(rendered.recordEdit).not.toHaveBeenCalled();
568
+ expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
569
+ } finally {
570
+ rendered.cleanup();
571
+ }
572
+ });
573
+
574
+ it("rolls back and reloads after a rejected batch POST", async () => {
440
575
  const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
441
576
  const url = requestUrl(input);
442
- if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: original });
443
- if (url.includes("/file-mutations/patch-elements-batch/")) {
577
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
444
578
  return jsonResponse({ error: "batch rejected" }, 500);
445
579
  }
446
580
  throw new Error(`Unexpected fetch: ${url}`);
@@ -485,10 +619,154 @@ describe("useDomEditCommits z-index reorder persistence", () => {
485
619
  ]);
486
620
  expect(writeProjectFile).not.toHaveBeenCalled();
487
621
  expect(rendered.recordEdit).not.toHaveBeenCalled();
488
- expect(rendered.reloadPreview).not.toHaveBeenCalled();
622
+ expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
623
+ expect(
624
+ fetchMock.mock.calls.filter(([input]) =>
625
+ requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
626
+ ),
627
+ ).toHaveLength(1);
628
+ } finally {
629
+ rendered.cleanup();
630
+ }
631
+ });
632
+
633
+ it("reloads after the server commits but the aggregate response is lost", async () => {
634
+ const original = '<div data-hf-id="hf-card" id="a" style="z-index: 7"></div>';
635
+ const after = '<div data-hf-id="hf-card" id="a" style="z-index: 2"></div>';
636
+ let diskContent = original;
637
+ const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
638
+ const url = requestUrl(input);
639
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
640
+ diskContent = after;
641
+ throw new TypeError("connection reset after commit");
642
+ }
643
+ throw new Error(`Unexpected fetch: ${url}`);
644
+ });
645
+ vi.stubGlobal("fetch", fetchMock);
646
+ const { iframe, element } = createPreviewElement(original);
647
+ usePlayerStore.getState().setElements([
648
+ {
649
+ id: "a",
650
+ tag: "div",
651
+ start: 0,
652
+ duration: 1,
653
+ track: 0,
654
+ zIndex: 7,
655
+ hasExplicitZIndex: true,
656
+ },
657
+ ]);
658
+ const rendered = renderDomEditCommits(createSelection(element), iframe);
659
+
660
+ try {
661
+ let rejection: unknown;
662
+ await act(async () => {
663
+ try {
664
+ await rendered.hook.handleDomZIndexReorderCommit([
665
+ { element, zIndex: 2, id: "a", sourceFile: "index.html", key: "a" },
666
+ ]);
667
+ } catch (error) {
668
+ rejection = error;
669
+ }
670
+ });
671
+
672
+ expect(rejection).toBeInstanceOf(Error);
673
+ expect((rejection as Error).message).toContain("connection reset after commit");
674
+ expect(diskContent).toBe(after);
675
+ expect(element.style.zIndex).toBe("7");
676
+ expect(usePlayerStore.getState().elements[0]).toMatchObject({
677
+ zIndex: 7,
678
+ hasExplicitZIndex: true,
679
+ });
680
+ expect(rendered.recordEdit).not.toHaveBeenCalled();
681
+ expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
682
+ expect(
683
+ fetchMock.mock.calls.filter(([input]) =>
684
+ requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
685
+ ),
686
+ ).toHaveLength(1);
687
+ } finally {
688
+ rendered.cleanup();
689
+ }
690
+ });
691
+
692
+ it.each([
693
+ [
694
+ "changed=false with different snapshots",
695
+ {
696
+ durable: true,
697
+ files: [
698
+ {
699
+ sourceFile: "index.html",
700
+ changed: false,
701
+ matched: [true],
702
+ before: '<div id="a" style="z-index: 1"></div>',
703
+ after: '<div id="a" style="z-index: 2"></div>',
704
+ },
705
+ ],
706
+ },
707
+ ],
708
+ [
709
+ "changed=true with identical snapshots",
710
+ {
711
+ durable: true,
712
+ files: [
713
+ {
714
+ sourceFile: "index.html",
715
+ changed: true,
716
+ matched: [true],
717
+ before: '<div id="a" style="z-index: 1"></div>',
718
+ after: '<div id="a" style="z-index: 1"></div>',
719
+ },
720
+ ],
721
+ },
722
+ ],
723
+ [
724
+ "durable=false with a changed file",
725
+ {
726
+ durable: false,
727
+ files: [
728
+ {
729
+ sourceFile: "index.html",
730
+ changed: true,
731
+ matched: [true],
732
+ before: '<div id="a" style="z-index: 1"></div>',
733
+ after: '<div id="a" style="z-index: 2"></div>',
734
+ },
735
+ ],
736
+ },
737
+ ],
738
+ ])("rejects and reloads for malformed 200 response: %s", async (_label, responseBody) => {
739
+ const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0]): Promise<Response> => {
740
+ const url = requestUrl(input);
741
+ if (url.endsWith("/file-mutations/patch-element-batches")) {
742
+ return jsonResponse(responseBody);
743
+ }
744
+ throw new Error(`Unexpected fetch: ${url}`);
745
+ });
746
+ vi.stubGlobal("fetch", fetchMock);
747
+ const { iframe, element } = createPreviewElement();
748
+ element.id = "a";
749
+ const rendered = renderDomEditCommits(createSelection(element), iframe);
750
+
751
+ try {
752
+ let rejection: unknown;
753
+ await act(async () => {
754
+ try {
755
+ await rendered.hook.handleDomZIndexReorderCommit([
756
+ { element, zIndex: 2, id: "a", sourceFile: "index.html", key: "a" },
757
+ ]);
758
+ } catch (error) {
759
+ rejection = error;
760
+ }
761
+ });
762
+
763
+ expect(rejection).toBeInstanceOf(Error);
764
+ expect((rejection as Error).message).toBe("Invalid atomic element patch response");
765
+ expect(rendered.recordEdit).not.toHaveBeenCalled();
766
+ expect(rendered.reloadPreview).toHaveBeenCalledTimes(1);
489
767
  expect(
490
768
  fetchMock.mock.calls.filter(([input]) =>
491
- requestUrl(input).includes("/file-mutations/patch-elements-batch/"),
769
+ requestUrl(input).endsWith("/file-mutations/patch-element-batches"),
492
770
  ),
493
771
  ).toHaveLength(1);
494
772
  } finally {