@hyperframes/studio 0.8.114 → 0.8.116

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 (266) hide show
  1. package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
  4. package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
  6. package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
  7. package/dist/chunk-3F47EFCT.js.map +1 -0
  8. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  9. package/dist/chunk-5EKWJZLM.js.map +1 -0
  10. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
  12. package/dist/index.d.ts +146 -40
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +4021 -3591
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +9 -8
  19. package/src/App.tsx +2 -2
  20. package/src/captions/hooks/useCaptionSync.ts +10 -6
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  23. package/src/components/dock/Dock.tsx +5 -7
  24. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  25. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  26. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  27. package/src/components/editor/DomEditOverlay.tsx +5 -1
  28. package/src/components/editor/FileTreeNodes.tsx +13 -10
  29. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  30. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  31. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  32. package/src/components/editor/SnapToolbar.tsx +43 -5
  33. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  34. package/src/components/editor/audioFxTelemetry.ts +2 -3
  35. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  36. package/src/components/editor/cropResize.ts +4 -3
  37. package/src/components/editor/domEditingDom.ts +4 -0
  38. package/src/components/editor/domEditingLayers.ts +1 -5
  39. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  40. package/src/components/editor/domGroupMoveBy.ts +95 -0
  41. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  42. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  43. package/src/components/editor/manualEditingAvailability.ts +8 -0
  44. package/src/components/editor/manualEdits.ts +1 -0
  45. package/src/components/editor/manualEditsParsing.ts +7 -0
  46. package/src/components/editor/manualOffsetDrag.ts +5 -6
  47. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  48. package/src/components/editor/marqueeCommit.ts +24 -8
  49. package/src/components/editor/motionPathCommit.test.ts +29 -1
  50. package/src/components/editor/motionPathCommit.ts +21 -9
  51. package/src/components/editor/motionPathSelection.test.ts +6 -31
  52. package/src/components/editor/probeSourceElement.test.ts +45 -0
  53. package/src/components/editor/probeSourceElement.ts +41 -16
  54. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  55. package/src/components/editor/propertyPanelFont.tsx +3 -2
  56. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  57. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  58. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  59. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  60. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  61. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  62. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  63. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  64. package/src/components/editor/propertyPanelTypes.ts +6 -3
  65. package/src/components/editor/useColorGradingController.ts +2 -1
  66. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  67. package/src/components/editor/useDomEditNudge.ts +2 -0
  68. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  69. package/src/components/editor/useDomEditZOrder.ts +12 -2
  70. package/src/components/editor/useFxCarveNodes.ts +2 -1
  71. package/src/components/editor/useFxLevelling.ts +2 -1
  72. package/src/components/feedback/projectProvenance.ts +2 -1
  73. package/src/components/nle/NLEContext.test.ts +1 -0
  74. package/src/components/nle/NLEContext.tsx +2 -1
  75. package/src/components/nle/NLEPreview.tsx +12 -10
  76. package/src/components/nle/PreviewPoster.tsx +2 -1
  77. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  78. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  79. package/src/components/renders/useFfmpegStatus.ts +2 -1
  80. package/src/components/renders/useRenderQueue.ts +5 -4
  81. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  82. package/src/components/sidebar/AssetsTab.tsx +2 -1
  83. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  84. package/src/components/ui/menuStyle.test.ts +61 -0
  85. package/src/components/ui/menuStyle.ts +10 -0
  86. package/src/contexts/DomEditContext.tsx +5 -1
  87. package/src/contexts/TimelineEditContext.tsx +1 -0
  88. package/src/domEditExports.test.tsx +9 -1
  89. package/src/hooks/domEditCommitRunner.ts +1 -1
  90. package/src/hooks/domSelectionTestHarness.ts +23 -0
  91. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  92. package/src/hooks/domStyleCommit.ts +8 -1
  93. package/src/hooks/draggedGsapPosition.ts +10 -0
  94. package/src/hooks/elementOffsetStager.ts +5 -4
  95. package/src/hooks/gsapDragCommit.test.ts +75 -164
  96. package/src/hooks/gsapDragCommit.ts +4 -125
  97. package/src/hooks/gsapDragPositionCommit.ts +36 -384
  98. package/src/hooks/gsapEditOutcome.ts +22 -2
  99. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  100. package/src/hooks/gsapGestureOutcome.ts +64 -0
  101. package/src/hooks/gsapGroupPreflight.ts +34 -0
  102. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  103. package/src/hooks/gsapMutationClient.ts +4 -3
  104. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  105. package/src/hooks/gsapParsedTween.test.ts +62 -0
  106. package/src/hooks/gsapParsedTween.ts +243 -0
  107. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  108. package/src/hooks/gsapPositionDetection.ts +52 -33
  109. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  110. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  111. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  112. package/src/hooks/gsapResizeIntercept.test.ts +96 -11
  113. package/src/hooks/gsapResizeIntercept.ts +79 -168
  114. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  115. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  116. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  117. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  118. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  119. package/src/hooks/gsapScriptCommitTypes.ts +13 -0
  120. package/src/hooks/gsapShared.ts +7 -0
  121. package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
  122. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  123. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  124. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  125. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  126. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  127. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  128. package/src/hooks/keyframeCacheAstLoad.ts +8 -6
  129. package/src/hooks/newTweenTarget.test.ts +41 -76
  130. package/src/hooks/plainRotation.ts +3 -2
  131. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  132. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  133. package/src/hooks/refreshTimelineRowText.ts +29 -0
  134. package/src/hooks/timelineTimingSync.test.ts +1 -0
  135. package/src/hooks/timelineTimingSync.ts +2 -1
  136. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  137. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  138. package/src/hooks/useAskAgentModal.ts +2 -1
  139. package/src/hooks/useBlockCatalog.ts +2 -1
  140. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  141. package/src/hooks/useCaptionDetection.ts +2 -1
  142. package/src/hooks/useCompositionContentLoader.ts +2 -1
  143. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  144. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  145. package/src/hooks/useDomEditSession.test.tsx +30 -1
  146. package/src/hooks/useDomEditSession.ts +10 -0
  147. package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
  148. package/src/hooks/useDomEditTextCommits.ts +13 -3
  149. package/src/hooks/useDomEditWiring.ts +4 -2
  150. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  151. package/src/hooks/useDomGeometryCommit.ts +8 -4
  152. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  153. package/src/hooks/useDomGeometryCommits.ts +3 -2
  154. package/src/hooks/useDomSelection.test.ts +26 -0
  155. package/src/hooks/useDomSelection.ts +10 -3
  156. package/src/hooks/useDomSelectionTypes.ts +6 -1
  157. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  158. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  159. package/src/hooks/useEditHistoryActions.ts +16 -5
  160. package/src/hooks/useElementLifecycleOps.ts +2 -1
  161. package/src/hooks/useEnableKeyframes.test.ts +34 -3
  162. package/src/hooks/useEnableKeyframes.ts +7 -4
  163. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
  164. package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
  165. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  166. package/src/hooks/useFileManager.ts +14 -10
  167. package/src/hooks/useFileTree.ts +3 -2
  168. package/src/hooks/useFrameCapture.ts +5 -1
  169. package/src/hooks/useFreezeFrame.ts +3 -2
  170. package/src/hooks/useGestureCommit.test.tsx +225 -55
  171. package/src/hooks/useGestureCommit.ts +86 -55
  172. package/src/hooks/useGroupCommits.ts +2 -1
  173. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  174. package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
  175. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  176. package/src/hooks/useGsapAwareEditing.ts +101 -195
  177. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  178. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  179. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  180. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  181. package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
  182. package/src/hooks/useGsapScriptCommits.ts +15 -10
  183. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  184. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  185. package/src/hooks/useGsapTweenCache.ts +18 -12
  186. package/src/hooks/useLintModal.ts +2 -1
  187. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  188. package/src/hooks/usePersistentEditHistory.ts +47 -8
  189. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  190. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  191. package/src/hooks/usePreviewInteraction.ts +2 -0
  192. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  193. package/src/hooks/usePreviewPersistence.ts +13 -11
  194. package/src/hooks/useProjectFileWriter.ts +20 -19
  195. package/src/hooks/useRemoveBackground.ts +2 -1
  196. package/src/hooks/useRenderClipContent.test.ts +18 -0
  197. package/src/hooks/useRenderClipContent.ts +4 -5
  198. package/src/hooks/useSdkSession.ts +2 -1
  199. package/src/hooks/useServerConnection.ts +4 -3
  200. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  201. package/src/hooks/useThumbnailStill.test.ts +100 -0
  202. package/src/hooks/useThumbnailStill.ts +37 -0
  203. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  204. package/src/hooks/useTimelineDeleteOps.ts +22 -27
  205. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  206. package/src/index.ts +4 -0
  207. package/src/player/components/AudioWaveform.tsx +3 -2
  208. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  209. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  210. package/src/player/components/ClipContextMenu.tsx +33 -31
  211. package/src/player/components/CompositionThumbnail.tsx +2 -1
  212. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  213. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  214. package/src/player/components/TextClipContent.test.ts +19 -0
  215. package/src/player/components/TextClipContent.tsx +27 -0
  216. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  217. package/src/player/components/clipMenuNormalize.ts +2 -1
  218. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  219. package/src/player/components/clipMenuToolItems.tsx +13 -9
  220. package/src/player/components/clipPeakMap.ts +2 -1
  221. package/src/player/components/timelineCallbacks.ts +1 -0
  222. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  223. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  224. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  225. package/src/player/components/timelineEditing.ts +3 -1
  226. package/src/player/components/timelineLinkSelection.ts +35 -1
  227. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  228. package/src/player/components/useTimelineClipDrag.ts +6 -7
  229. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  230. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  231. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  232. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  233. package/src/player/lib/thumbnailScheduler.ts +10 -2
  234. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  235. package/src/player/lib/timelineElementHelpers.ts +6 -15
  236. package/src/player/lib/timelineText.ts +40 -0
  237. package/src/player/sceneSwap.ts +2 -1
  238. package/src/player/store/timelineElement.ts +12 -0
  239. package/src/styles/theme.css +3 -0
  240. package/src/telemetry/client.ts +2 -1
  241. package/src/utils/blockInstaller.ts +2 -1
  242. package/src/utils/elementGsap.test.ts +9 -0
  243. package/src/utils/elementGsap.ts +7 -0
  244. package/src/utils/gsapSoftReload.ts +2 -3
  245. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  246. package/src/utils/gsapUndoRestore.ts +3 -6
  247. package/src/utils/keyframeUsage.ts +79 -0
  248. package/src/utils/previewFeatureUsage.test.ts +32 -0
  249. package/src/utils/previewFeatureUsage.ts +46 -0
  250. package/src/utils/previewPrefetch.ts +2 -1
  251. package/src/utils/razorSplitTransaction.ts +12 -8
  252. package/src/utils/softReloadTargets.ts +1 -3
  253. package/src/utils/studioApiFetch.test.ts +75 -0
  254. package/src/utils/studioApiFetch.ts +13 -0
  255. package/src/utils/studioFileHistory.ts +4 -1
  256. package/src/utils/studioHelpers.ts +7 -1
  257. package/src/utils/studioPendingEdits.test.ts +23 -6
  258. package/src/utils/studioPendingEdits.ts +32 -8
  259. package/src/utils/studioTelemetry.test.ts +107 -0
  260. package/src/utils/studioTelemetry.ts +51 -5
  261. package/src/utils/timelineCompositionInsert.ts +3 -2
  262. package/src/webmcp/StudioAgentTools.tsx +4 -3
  263. package/dist/chunk-BIUXP3PG.js.map +0 -1
  264. package/dist/chunk-K47E2ZJA.js.map +0 -1
  265. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  266. /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -0,0 +1,160 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
6
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
+ import type { DomEditGroupPathOffsetCommit } from "../components/editor/domEditOverlayGestures";
8
+ import { usePlayerStore } from "../player/store/playerStore";
9
+ import { trackStudioEditBlocked } from "../utils/studioSaveDiagnostics";
10
+ import { GSAP_EDIT_BLOCK_COPY } from "./gsapEditOutcome";
11
+ import { mountGsapAwareEditing } from "./useGsapAwareEditing.testHelpers";
12
+
13
+ vi.mock("../utils/studioSaveDiagnostics", async (importOriginal) => ({
14
+ ...(await importOriginal<typeof import("../utils/studioSaveDiagnostics")>()),
15
+ trackStudioEditBlocked: vi.fn(),
16
+ trackStudioSaveFailure: vi.fn(),
17
+ }));
18
+
19
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
20
+
21
+ beforeEach(() => {
22
+ usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 1, activeKeyframePct: null });
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.clearAllMocks();
27
+ document.body.innerHTML = "";
28
+ Reflect.deleteProperty(window, "__timelines");
29
+ });
30
+
31
+ const positionTween = (id: string, extra: Partial<GsapAnimation> = {}) =>
32
+ ({
33
+ id: `#${id}-to-0-position`,
34
+ targetSelector: `#${id}`,
35
+ propertyGroup: "position",
36
+ method: "to",
37
+ properties: { x: 100 },
38
+ position: 0,
39
+ resolvedStart: 0,
40
+ duration: 2,
41
+ ease: "none",
42
+ ...extra,
43
+ }) as unknown as GsapAnimation;
44
+
45
+ /** Three members GSAP is visibly moving, one `to` tween each in the file. */
46
+ function mountGroup(animations: GsapAnimation[]) {
47
+ const elements = ["a", "b", "c"].map((id) => {
48
+ const el = Object.assign(document.createElement("div"), { id });
49
+ document.body.append(el);
50
+ return el;
51
+ });
52
+ const live = elements.map((el) => ({
53
+ targets: () => [el],
54
+ vars: { x: 100, duration: 2 },
55
+ duration: () => 2,
56
+ startTime: () => 0,
57
+ }));
58
+ const timelines = { root: { getChildren: () => live, duration: () => 2 } };
59
+ Reflect.set(window, "__timelines", timelines);
60
+ const iframe = {
61
+ contentWindow: { gsap: { getProperty: () => 0, set: vi.fn() }, __timelines: timelines },
62
+ contentDocument: document,
63
+ } as unknown as HTMLIFrameElement;
64
+ const commitMutation = Object.assign(vi.fn().mockResolvedValue(undefined), {
65
+ batch: vi.fn().mockResolvedValue(undefined),
66
+ });
67
+ const showToast = vi.fn();
68
+ const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
69
+ const { editing, root } = mountGsapAwareEditing({
70
+ gsapCommitMutation: commitMutation,
71
+ activeCompPath: "index.html",
72
+ previewIframeRef: { current: iframe },
73
+ showToast,
74
+ makeFetchFallback: () => async () => animations,
75
+ stageElementPositionOffset,
76
+ });
77
+ const groupCommit = (updates: DomEditGroupPathOffsetCommit[]) =>
78
+ editing().handleGsapAwareGroupPathOffsetCommit(updates);
79
+ const updates = elements.map((element) => ({
80
+ selection: {
81
+ element,
82
+ id: element.id,
83
+ selector: `#${element.id}`,
84
+ } as unknown as DomEditSelection,
85
+ next: { x: 30, y: 0 },
86
+ }));
87
+ const written = () => [
88
+ ...commitMutation.mock.calls.map((call) => call[1]),
89
+ ...commitMutation.batch.mock.calls.flatMap(([calls]) =>
90
+ calls.map((c: { mutation: unknown }) => c.mutation),
91
+ ),
92
+ ];
93
+ return { elements, iframe, updates, groupCommit, commitMutation, written, showToast, root };
94
+ }
95
+
96
+ describe("a group drag plans every member before its first write", () => {
97
+ it("refuses the whole group when one member's tween cannot take a keyframe", async () => {
98
+ const h = mountGroup([
99
+ positionTween("a"),
100
+ positionTween("b", { ease: undefined }),
101
+ positionTween("c"),
102
+ ]);
103
+ const styles = h.elements.map((el) => el.getAttribute("style"));
104
+
105
+ await expect(h.groupCommit(h.updates)).rejects.toMatchObject({
106
+ name: "GsapEditBlockedError",
107
+ reason: "keyframes-uneditable",
108
+ detail: "unknown-ease",
109
+ });
110
+
111
+ expect(h.written()).toEqual([]);
112
+ expect(h.commitMutation.batch).not.toHaveBeenCalled();
113
+ const gsap = (h.iframe.contentWindow as unknown as { gsap: { set: unknown } }).gsap;
114
+ expect(gsap.set).not.toHaveBeenCalled();
115
+ expect(h.elements.map((el) => el.getAttribute("style"))).toEqual(styles);
116
+ expect(h.showToast).toHaveBeenCalledTimes(1);
117
+ expect(h.showToast).toHaveBeenCalledWith(GSAP_EDIT_BLOCK_COPY["keyframes-uneditable"], "error");
118
+ expect(trackStudioEditBlocked).toHaveBeenCalledTimes(1);
119
+ expect(trackStudioEditBlocked).toHaveBeenCalledWith(expect.objectContaining({ targetId: "b" }));
120
+ act(() => h.root.unmount());
121
+ });
122
+
123
+ it("refuses members animated in two files, since the batch writes one file", async () => {
124
+ const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
125
+ const updates = h.updates.map((u, i) => ({
126
+ ...u,
127
+ selection: { ...u.selection, sourceFile: i === 2 ? "scenes/intro.html" : "index.html" },
128
+ }));
129
+
130
+ await expect(h.groupCommit(updates)).rejects.toMatchObject({ reason: "mixed-files" });
131
+
132
+ expect(h.written()).toEqual([]);
133
+ expect(trackStudioEditBlocked).toHaveBeenCalledWith(expect.objectContaining({ targetId: "c" }));
134
+ act(() => h.root.unmount());
135
+ });
136
+
137
+ it("writes a group whose members name the open file or leave it implied", async () => {
138
+ const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
139
+ const updates = h.updates.map((u, i) => ({
140
+ ...u,
141
+ selection: { ...u.selection, sourceFile: i === 0 ? "" : "index.html" },
142
+ }));
143
+
144
+ await h.groupCommit(updates);
145
+
146
+ expect(h.written()).toHaveLength(3);
147
+ act(() => h.root.unmount());
148
+ });
149
+
150
+ it("writes every member when each plan holds", async () => {
151
+ const h = mountGroup(["a", "b", "c"].map((id) => positionTween(id)));
152
+
153
+ await h.groupCommit(h.updates);
154
+
155
+ const ids = h.written().map((m) => (m as { animationId?: string }).animationId);
156
+ expect(ids).toEqual(["#a-to-0-position", "#b-to-0-position", "#c-to-0-position"]);
157
+ expect(h.showToast).not.toHaveBeenCalled();
158
+ act(() => h.root.unmount());
159
+ });
160
+ });
@@ -74,7 +74,7 @@ function mountResizeHandler(
74
74
  size: { width: number; height: number },
75
75
  offset?: { x: number; y: number },
76
76
  restore?: () => void,
77
- ) => Promise<void>)
77
+ ) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>)
78
78
  | null = null;
79
79
  let property: ReturnType<typeof useGsapAwareEditing>["commitAnimatedProperty"] | null = null;
80
80
  function Harness() {
@@ -119,25 +119,33 @@ function mountResizeHandler(
119
119
  type AwareEditingParams = Parameters<typeof useGsapAwareEditing>[0];
120
120
 
121
121
  function mountGroupHandler({
122
- gsapCommitMutation,
123
- makeFetchFallback,
122
+ gsapCommitMutation = vi.fn().mockResolvedValue(undefined),
123
+ makeFetchFallback = () => vi.fn().mockResolvedValue([]),
124
124
  trackGsapInteractionFailure = vi.fn(),
125
125
  stageElementPositionOffset = vi.fn(),
126
126
  handleDomRotationCommit = vi.fn(),
127
- }: Pick<AwareEditingParams, "gsapCommitMutation" | "makeFetchFallback"> &
128
- Partial<
129
- Pick<
130
- AwareEditingParams,
131
- "trackGsapInteractionFailure" | "stageElementPositionOffset" | "handleDomRotationCommit"
132
- >
133
- >) {
134
- let groupCommit!: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void>;
127
+ }: Partial<
128
+ Pick<
129
+ AwareEditingParams,
130
+ | "gsapCommitMutation"
131
+ | "makeFetchFallback"
132
+ | "trackGsapInteractionFailure"
133
+ | "stageElementPositionOffset"
134
+ | "handleDomRotationCommit"
135
+ >
136
+ > = {}) {
137
+ let groupCommit!: (
138
+ updates: DomEditGroupPathOffsetCommit[],
139
+ ) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>;
135
140
  let pathOffsetCommit!: (
136
141
  selection: DomEditSelection,
137
142
  next: { x: number; y: number },
138
143
  route?: { plainTranslate?: boolean },
139
- ) => Promise<void>;
140
- let rotationCommit!: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
144
+ ) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>;
145
+ let rotationCommit!: (
146
+ selection: DomEditSelection,
147
+ next: RotationCommit,
148
+ ) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>;
141
149
  function Harness() {
142
150
  const editing = useGsapAwareEditing({
143
151
  domEditSelection: null,
@@ -211,8 +219,6 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
211
219
  const save = vi.fn().mockResolvedValue(undefined);
212
220
  const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
213
221
  const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
214
- gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
215
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
216
222
  stageElementPositionOffset,
217
223
  });
218
224
  const box = {
@@ -231,11 +237,7 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
231
237
  describe("useGsapAwareEditing refuses a group GSAP took over before writing any member", () => {
232
238
  it("writes no member when one CSS-route member has been folded since the press", async () => {
233
239
  const stageElementPositionOffset = vi.fn(() => ({ save: vi.fn(), rollback: vi.fn() }));
234
- const { groupCommit, root } = mountGroupHandler({
235
- gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
236
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
237
- stageElementPositionOffset,
238
- });
240
+ const { groupCommit, root } = mountGroupHandler({ stageElementPositionOffset });
239
241
  const card = {
240
242
  element: document.createElement("div"),
241
243
  id: "card",
@@ -263,7 +265,6 @@ describe("useGsapAwareEditing rotation routing", () => {
263
265
  const gsapCommitMutation = vi.fn();
264
266
  const h = mountGroupHandler({
265
267
  gsapCommitMutation,
266
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
267
268
  handleDomRotationCommit,
268
269
  });
269
270
  const box = { element, id: "box", selector: "#box" } as unknown as DomEditSelection;
@@ -327,7 +328,6 @@ describe("useGsapAwareEditing shared-tween moves", () => {
327
328
  const commitMutation = vi.fn();
328
329
  const { pathOffsetCommit, root } = mountGroupHandler({
329
330
  gsapCommitMutation: commitMutation,
330
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
331
331
  stageElementPositionOffset,
332
332
  });
333
333
  const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
@@ -346,11 +346,7 @@ describe("useGsapAwareEditing shared-tween moves", () => {
346
346
  });
347
347
  const save = vi.fn().mockResolvedValue(undefined);
348
348
  const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
349
- const { groupCommit, root } = mountGroupHandler({
350
- gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
351
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
352
- stageElementPositionOffset,
353
- });
349
+ const { groupCommit, root } = mountGroupHandler({ stageElementPositionOffset });
354
350
  const word = { element: gsapPositioned("span"), hfId: "w0", selector: ".w" };
355
351
  const box = { element: gsapPositioned("div"), id: "box", selector: "#box" };
356
352
  const plain = { element: document.createElement("div"), id: "plain", selector: "#plain" };
@@ -401,6 +397,7 @@ describe("useGsapAwareEditing anchored resize", () => {
401
397
  null,
402
398
  expect.any(Function),
403
399
  expect.any(Function),
400
+ undefined,
404
401
  );
405
402
  act(() => h.root.unmount());
406
403
  });
@@ -589,7 +586,7 @@ describe("useGsapAwareEditing anchored resize", () => {
589
586
  h.selection.element.setAttribute("data-hf-drag-initial-offset-x", "0");
590
587
  h.selection.element.setAttribute("data-hf-drag-initial-offset-y", "0");
591
588
 
592
- let commit!: Promise<void>;
589
+ let commit!: Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>;
593
590
  act(() => {
594
591
  commit = h.resize(h.selection, { width: 300, height: 200 }, { x: -50.2, y: -25.6 });
595
592
  });
@@ -636,7 +633,7 @@ describe("useGsapAwareEditing anchored resize", () => {
636
633
  s: DomEditSelection,
637
634
  m: unknown,
638
635
  o: { coalesceKey?: string; label?: string; softReload?: boolean },
639
- ) => Promise<void>,
636
+ ) => Promise<void | import("../utils/previewFeatureUsage").GeometryCommitResult>,
640
637
  _fetch: unknown,
641
638
  options?: { preflightOnly?: boolean },
642
639
  ) => {
@@ -653,7 +650,6 @@ describe("useGsapAwareEditing anchored resize", () => {
653
650
  );
654
651
  const { groupCommit, root } = mountGroupHandler({
655
652
  gsapCommitMutation: commitMutation,
656
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
657
653
  });
658
654
  const updates = [
659
655
  {
@@ -754,11 +750,7 @@ describe("useGsapAwareEditing anchored resize", () => {
754
750
  mocks.drag.mockImplementation(async (selection) => {
755
751
  throw selection.id === "a" ? failures[0] : failures[1];
756
752
  });
757
- const { groupCommit, root } = mountGroupHandler({
758
- gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
759
- makeFetchFallback: () => vi.fn().mockResolvedValue([]),
760
- trackGsapInteractionFailure,
761
- });
753
+ const { groupCommit, root } = mountGroupHandler({ trackGsapInteractionFailure });
762
754
  const updates = [
763
755
  {
764
756
  selection: { element: gsapPositioned("div"), id: "a", selector: "#a" },
@@ -777,6 +769,7 @@ describe("useGsapAwareEditing anchored resize", () => {
777
769
  updates[0]?.selection,
778
770
  "drag",
779
771
  "Move animated layer (group)",
772
+ true,
780
773
  );
781
774
  mocks.drag.mockReset();
782
775
  if (priorDragImplementation) mocks.drag.mockImplementation(priorDragImplementation);
@@ -0,0 +1,42 @@
1
+ import { vi } from "vitest";
2
+ import { mountReactHarness } from "./domSelectionTestHarness";
3
+ import { useGsapAwareEditing } from "./useGsapAwareEditing";
4
+ import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
5
+
6
+ type Params = Parameters<typeof useGsapAwareEditing>[0];
7
+
8
+ /**
9
+ * Mounts useGsapAwareEditing with no-op callbacks for every param not given, and the real
10
+ * failure telemetry toasting through `showToast`. `editing()` reads the latest render.
11
+ */
12
+ export function mountGsapAwareEditing(
13
+ params: Partial<Omit<Params, "trackGsapInteractionFailure">> & Pick<Params, "showToast">,
14
+ ) {
15
+ let editing!: ReturnType<typeof useGsapAwareEditing>;
16
+ function Harness() {
17
+ editing = useGsapAwareEditing({
18
+ domEditSelection: null,
19
+ selectedGsapAnimations: [],
20
+ gsapCommitMutation: vi.fn().mockResolvedValue(undefined),
21
+ previewIframeRef: { current: null },
22
+ bumpGsapCache: vi.fn(),
23
+ makeFetchFallback: () => vi.fn().mockResolvedValue([]),
24
+ stageElementPositionOffset: vi.fn(),
25
+ handleDomBoxSizeCommit: vi.fn(),
26
+ handleDomRotationCommit: vi.fn(),
27
+ commitPositionPatchToHtml: vi.fn(),
28
+ addGsapAnimation: vi.fn(),
29
+ convertToKeyframes: vi.fn(),
30
+ setArcPath: vi.fn(),
31
+ updateArcSegment: vi.fn(),
32
+ ...params,
33
+ trackGsapInteractionFailure: useGsapInteractionFailureTelemetry(
34
+ "index.html",
35
+ params.showToast,
36
+ ),
37
+ });
38
+ return null;
39
+ }
40
+ const root = mountReactHarness(<Harness />);
41
+ return { editing: () => editing, root };
42
+ }