@hyperframes/studio 0.8.115 → 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 (149) hide show
  1. package/dist/assets/{hyperframes-player-C-9N8nk0.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-BoUM6Xqb.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-CAvZ0AA-.js → index-DYNaXPVf.js} +201 -201
  4. package/dist/assets/{index-CVuorIMv.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
  6. package/dist/chunk-3F47EFCT.js.map +1 -0
  7. package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
  8. package/dist/index.d.ts +104 -22
  9. package/dist/index.html +1 -1
  10. package/dist/index.js +1131 -1044
  11. package/dist/index.js.map +1 -1
  12. package/package.json +8 -8
  13. package/src/captions/hooks/useCaptionSync.ts +10 -6
  14. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  15. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  16. package/src/components/editor/domGroupMoveBy.ts +95 -0
  17. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  18. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  19. package/src/components/editor/manualEditingAvailability.ts +8 -0
  20. package/src/components/editor/manualOffsetDrag.ts +5 -6
  21. package/src/components/editor/motionPathSelection.test.ts +6 -31
  22. package/src/components/editor/probeSourceElement.ts +2 -1
  23. package/src/components/editor/propertyPanelFont.tsx +3 -2
  24. package/src/components/editor/useColorGradingController.ts +2 -1
  25. package/src/components/editor/useFxCarveNodes.ts +2 -1
  26. package/src/components/editor/useFxLevelling.ts +2 -1
  27. package/src/components/feedback/projectProvenance.ts +2 -1
  28. package/src/components/nle/NLEContext.test.ts +1 -0
  29. package/src/components/nle/NLEContext.tsx +2 -1
  30. package/src/components/nle/NLEPreview.tsx +12 -10
  31. package/src/components/nle/PreviewPoster.tsx +2 -1
  32. package/src/components/renders/useFfmpegStatus.ts +2 -1
  33. package/src/components/renders/useRenderQueue.ts +5 -4
  34. package/src/components/sidebar/AssetsTab.tsx +2 -1
  35. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  36. package/src/contexts/DomEditContext.tsx +4 -0
  37. package/src/hooks/domSelectionTestHarness.ts +23 -0
  38. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  39. package/src/hooks/domStyleCommit.ts +8 -1
  40. package/src/hooks/draggedGsapPosition.ts +10 -0
  41. package/src/hooks/gsapDragCommit.test.ts +75 -164
  42. package/src/hooks/gsapDragCommit.ts +2 -105
  43. package/src/hooks/gsapDragPositionCommit.ts +30 -393
  44. package/src/hooks/gsapEditOutcome.ts +22 -2
  45. package/src/hooks/gsapGroupPreflight.ts +34 -0
  46. package/src/hooks/gsapMutationClient.ts +4 -3
  47. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  48. package/src/hooks/gsapParsedTween.test.ts +62 -0
  49. package/src/hooks/gsapParsedTween.ts +243 -0
  50. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  51. package/src/hooks/gsapPositionDetection.ts +52 -33
  52. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  53. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  54. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  55. package/src/hooks/gsapResizeIntercept.test.ts +63 -11
  56. package/src/hooks/gsapResizeIntercept.ts +79 -169
  57. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  58. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  59. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  60. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  61. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  62. package/src/hooks/gsapScriptCommitTypes.ts +8 -0
  63. package/src/hooks/gsapShared.ts +7 -0
  64. package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
  65. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  66. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  67. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  68. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  69. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  70. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  71. package/src/hooks/keyframeCacheAstLoad.ts +2 -1
  72. package/src/hooks/newTweenTarget.test.ts +41 -76
  73. package/src/hooks/timelineTimingSync.test.ts +1 -0
  74. package/src/hooks/timelineTimingSync.ts +2 -1
  75. package/src/hooks/useAskAgentModal.ts +2 -1
  76. package/src/hooks/useBlockCatalog.ts +2 -1
  77. package/src/hooks/useCaptionDetection.ts +2 -1
  78. package/src/hooks/useCompositionContentLoader.ts +2 -1
  79. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  80. package/src/hooks/useDomEditSession.test.tsx +30 -1
  81. package/src/hooks/useDomEditSession.ts +10 -0
  82. package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
  83. package/src/hooks/useDomEditTextCommits.ts +8 -2
  84. package/src/hooks/useDomGeometryCommit.ts +1 -0
  85. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  86. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  87. package/src/hooks/useEditHistoryActions.ts +16 -5
  88. package/src/hooks/useElementLifecycleOps.ts +2 -1
  89. package/src/hooks/useEnableKeyframes.test.ts +32 -1
  90. package/src/hooks/useEnableKeyframes.ts +2 -1
  91. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
  92. package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
  93. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  94. package/src/hooks/useFileManager.ts +14 -10
  95. package/src/hooks/useFileTree.ts +3 -2
  96. package/src/hooks/useFrameCapture.ts +5 -1
  97. package/src/hooks/useFreezeFrame.ts +3 -2
  98. package/src/hooks/useGroupCommits.ts +2 -1
  99. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  100. package/src/hooks/useGsapAwareEditing.test.tsx +17 -29
  101. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  102. package/src/hooks/useGsapAwareEditing.ts +5 -0
  103. package/src/hooks/useGsapAwareGroupMove.ts +16 -16
  104. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  105. package/src/hooks/useGsapScriptCommits.test.tsx +3 -1
  106. package/src/hooks/useGsapScriptCommits.ts +12 -10
  107. package/src/hooks/useLintModal.ts +2 -1
  108. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  109. package/src/hooks/usePersistentEditHistory.ts +47 -8
  110. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  111. package/src/hooks/usePreviewPersistence.ts +13 -11
  112. package/src/hooks/useProjectFileWriter.ts +20 -19
  113. package/src/hooks/useRemoveBackground.ts +2 -1
  114. package/src/hooks/useSdkSession.ts +2 -1
  115. package/src/hooks/useServerConnection.ts +4 -3
  116. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  117. package/src/hooks/useThumbnailStill.test.ts +100 -0
  118. package/src/hooks/useThumbnailStill.ts +37 -0
  119. package/src/hooks/useTimelineDeleteOps.ts +2 -1
  120. package/src/index.ts +4 -0
  121. package/src/player/components/AudioWaveform.tsx +3 -2
  122. package/src/player/components/CompositionThumbnail.tsx +2 -1
  123. package/src/player/components/clipMenuNormalize.ts +2 -1
  124. package/src/player/components/clipPeakMap.ts +2 -1
  125. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  126. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  127. package/src/player/lib/thumbnailScheduler.ts +10 -2
  128. package/src/player/sceneSwap.ts +2 -1
  129. package/src/telemetry/client.ts +2 -1
  130. package/src/utils/blockInstaller.ts +2 -1
  131. package/src/utils/elementGsap.test.ts +9 -0
  132. package/src/utils/elementGsap.ts +7 -0
  133. package/src/utils/gsapSoftReload.ts +2 -3
  134. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  135. package/src/utils/gsapUndoRestore.ts +3 -6
  136. package/src/utils/previewPrefetch.ts +2 -1
  137. package/src/utils/razorSplitTransaction.ts +12 -8
  138. package/src/utils/softReloadTargets.ts +1 -3
  139. package/src/utils/studioApiFetch.test.ts +75 -0
  140. package/src/utils/studioApiFetch.ts +13 -0
  141. package/src/utils/studioFileHistory.ts +4 -1
  142. package/src/utils/studioHelpers.ts +7 -1
  143. package/src/utils/studioPendingEdits.test.ts +23 -6
  144. package/src/utils/studioPendingEdits.ts +32 -8
  145. package/src/utils/studioTelemetry.ts +2 -1
  146. package/src/utils/timelineCompositionInsert.ts +3 -2
  147. package/src/webmcp/StudioAgentTools.tsx +2 -1
  148. package/dist/chunk-TXZMMOMA.js.map +0 -1
  149. /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -0,0 +1,83 @@
1
+ // @vitest-environment happy-dom
2
+ // Real Chrome with the built runtime: its seek, not a plain GSAP seek, decides what a saved edit shows.
3
+ import { readFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { gsap } from "gsap";
6
+ import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
7
+ import { replaceTweenWithKeyframesInScript } from "@hyperframes/parsers/gsap-writer-acorn";
8
+ import puppeteer from "puppeteer-core";
9
+ import { expect, it, vi } from "vitest";
10
+ import { findSystemChrome } from "../../vite.browser";
11
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
12
+ import { usePlayerStore } from "../player/store/playerStore";
13
+ import { planValueEdit } from "./gsapValueAtPlayhead";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ vi.setConfig({ testTimeout: 60_000 });
17
+
18
+ const SRC = `var tl = gsap.timeline({ paused: true });
19
+ tl.from("#target", { x: -60, duration: 2, ease: "none" }, 0);
20
+ tl.fromTo("#target", { x: 0 }, { x: 60, duration: 1, ease: "none" }, 2);
21
+ window.__timelines["main"] = tl;`;
22
+
23
+ /** Plans and writes a drag of `#target` to `x` at `at`, as the move gesture saves it. */
24
+ function writeDrag(x: number, at: number): string {
25
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), {
26
+ id: "target",
27
+ });
28
+ const win = { __timelines: {} as Record<string, gsap.core.Timeline> };
29
+ new Function("gsap", "window", SRC)(gsap, win);
30
+ const timeline = win.__timelines.main!;
31
+ timeline.progress(0.0001, true).seek(at);
32
+ usePlayerStore.setState({ currentTime: at, activeKeyframePct: null });
33
+ const iframe = { contentWindow: { ...win, gsap } } as unknown as HTMLIFrameElement;
34
+ const anim = parseGsapScriptAcorn(SRC).animations[1]!;
35
+ const selection = { id: "target", selector: "#target", element: box } as DomEditSelection;
36
+ const plan = planValueEdit(selection, anim, { x }, iframe);
37
+ timeline.kill();
38
+ if (!plan.ok) throw new Error(`the drag was refused: ${JSON.stringify(plan)}`);
39
+ return replaceTweenWithKeyframesInScript(SRC, anim.id, plan.mutation)!;
40
+ }
41
+
42
+ const page = (script: string) => `<!doctype html><html><body>
43
+ <div id="root" data-composition-id="main" data-start="0" data-duration="4" data-width="1920" data-height="1080">
44
+ <div id="target" class="clip" data-start="0" data-duration="4" data-track-index="1"></div>
45
+ </div>
46
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
47
+ <script>window.__timelines = window.__timelines || {};\n${script}</script>
48
+ </body></html>`;
49
+
50
+ it("a drag on a fromTo's start shows the dropped x there once the runtime seeks", async () => {
51
+ const written = writeDrag(90, 2);
52
+ const executablePath = findSystemChrome();
53
+ if (!executablePath) throw new Error("no Chrome found: set HYPERFRAMES_BROWSER_PATH");
54
+ const browser = await puppeteer.launch({
55
+ executablePath,
56
+ headless: true,
57
+ args: ["--no-sandbox"],
58
+ });
59
+ try {
60
+ const tab = await browser.newPage();
61
+ await tab.setRequestInterception(true);
62
+ // Only the GSAP script is served; anything else is refused, never fetched.
63
+ tab.on("request", (request) =>
64
+ request.url().endsWith("/gsap.min.js")
65
+ ? request.respond({ body: readFileSync(require.resolve("gsap/dist/gsap.min.js"), "utf8") })
66
+ : request.abort("blockedbyclient"),
67
+ );
68
+ await tab.setContent(page(written), { waitUntil: "load" });
69
+ await tab.evaluate(readFileSync(require.resolve("@hyperframes/core/runtime"), "utf8"));
70
+ await tab.waitForFunction(() => "__player" in window);
71
+ const shown = await tab.evaluate(() => {
72
+ const w = window as unknown as {
73
+ __player: { seek(t: number): void };
74
+ gsap: { getProperty(target: string, prop: string): number };
75
+ };
76
+ w.__player.seek(2);
77
+ return w.gsap.getProperty("#target", "x");
78
+ });
79
+ expect(shown).toBeCloseTo(90, 1);
80
+ } finally {
81
+ await browser.close();
82
+ }
83
+ });
@@ -0,0 +1,179 @@
1
+ // @vitest-environment happy-dom
2
+ import { gsap } from "gsap";
3
+ import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
4
+ import { replaceTweenWithKeyframesInScript } from "@hyperframes/parsers/gsap-writer-acorn";
5
+ import { afterEach, expect, it } from "vitest";
6
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
+ import { usePlayerStore } from "../player/store/playerStore";
8
+ import { findParsedTween, parsedImplicitEndValue, parsedTweenEase } from "./gsapParsedTween";
9
+ import { planValueEdit } from "./gsapValueAtPlayhead";
10
+
11
+ /** Runs a composition script as the preview does: a paused timeline, bound, then seeked to `at`. */
12
+ function play(script: string, at: number) {
13
+ const win = { __timelines: {} as Record<string, gsap.core.Timeline> };
14
+ new Function("gsap", "window", script)(gsap, win);
15
+ const timeline = win.__timelines.t!;
16
+ timeline.progress(0.0001, true).seek(at);
17
+ return { timeline, iframe: { contentWindow: { ...win, gsap } } as unknown as HTMLIFrameElement };
18
+ }
19
+
20
+ /** Drags `#x` to `x` at `at`, writes the plan into the script, and replays the written file there. */
21
+ function dragAndReplay(script: string, x: number, at: number) {
22
+ const box = document.body.appendChild(document.createElement("div"));
23
+ box.id = "x";
24
+ const { timeline, iframe } = play(script, at);
25
+ usePlayerStore.setState({ currentTime: at, activeKeyframePct: null });
26
+ const anim = parseGsapScriptAcorn(script).animations[0]!;
27
+ const selection = { id: "x", selector: "#x", element: box } as DomEditSelection;
28
+ const plan = planValueEdit(selection, anim, { x }, iframe);
29
+ timeline.kill();
30
+ if (!plan.ok) return { plan };
31
+ const written = replaceTweenWithKeyframesInScript(script, anim.id, plan.mutation)!;
32
+ const replay = play(written, at);
33
+ const shown = gsap.getProperty(box, "x");
34
+ replay.timeline.kill();
35
+ return { plan, written, shown };
36
+ }
37
+
38
+ const script = (vars: string) =>
39
+ `var tl = gsap.timeline({ paused: true });\ntl.to("#x", { ${vars} }, 0);\nwindow.__timelines["t"] = tl;`;
40
+
41
+ afterEach(() => {
42
+ document.body.replaceChildren();
43
+ usePlayerStore.setState({ currentTime: 0, activeKeyframePct: null });
44
+ });
45
+
46
+ it("edits a delayed tween with no authored ease, landing the value at the playhead", () => {
47
+ const { plan, shown } = dragAndReplay(script("duration: 1, delay: 0.5, x: 100"), 200, 2);
48
+ expect(plan.ok).toBe(true);
49
+ expect(shown).toBe(200);
50
+ });
51
+
52
+ it("writes a delayed linear tween so GSAP shows the new value at the playhead, not later", () => {
53
+ const { shown } = dragAndReplay(script("duration: 1, delay: 0.5, x: 100, ease: 'none'"), 200, 2);
54
+ expect(shown).toBe(200);
55
+ });
56
+
57
+ it("keeps GSAP's default ease, by name, for a tween that authors none", () => {
58
+ const { plan } = dragAndReplay(script("duration: 1, x: 100"), 60, 0.5);
59
+ expect(plan.ok && plan.mutation.easeEach).toBe("power1.out");
60
+ });
61
+
62
+ it("reads a tween the playhead has not reached without redrawing a sibling's live value", () => {
63
+ const [x, y] = ["x", "y"].map((id) =>
64
+ Object.assign(document.body.appendChild(document.createElement("div")), { id }),
65
+ );
66
+ const src = `var tl = gsap.timeline({ paused: true });
67
+ tl.to("#y", { x: 100, duration: 1, ease: "none" }, 0);
68
+ tl.to("#x", { x: 300, duration: 1 }, 2);
69
+ window.__timelines["t"] = tl;`;
70
+ const { timeline, iframe } = play(src, 0.5);
71
+ gsap.set(y!, { x: 77 });
72
+
73
+ const tween = findParsedTween(iframe, x!, parseGsapScriptAcorn(src).animations[1]!);
74
+
75
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
76
+ expect(gsap.getProperty(y!, "x")).toBe(77);
77
+ expect(timeline.time()).toBe(0.5);
78
+ timeline.kill();
79
+ });
80
+
81
+ it("keeps a sibling's live attribute while reading an unplayed tween beside the runtime's filler", () => {
82
+ const [x, y] = ["x", "y"].map((id) =>
83
+ Object.assign(document.body.appendChild(document.createElement("div")), { id }),
84
+ );
85
+ y!.setAttribute("data-value", "0");
86
+ const src = `var tl = gsap.timeline({ paused: true });
87
+ tl.to({}, { duration: 4, data: "hf-runtime-filler" }, 0);
88
+ tl.to("#y", { attr: { "data-value": 100 }, duration: 1, ease: "none" }, 0);
89
+ tl.to("#x", { x: 300, duration: 1 }, 2);
90
+ window.__timelines["t"] = tl;`;
91
+ const { timeline, iframe } = play(src, 0.5);
92
+ y!.setAttribute("data-value", "77");
93
+
94
+ const tween = findParsedTween(iframe, x!, parseGsapScriptAcorn(src).animations.at(-1)!);
95
+
96
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
97
+ expect(y!.getAttribute("data-value")).toBe("77");
98
+ timeline.kill();
99
+ });
100
+
101
+ it("reads a later tween's start from the earlier tween on the same layer, as playback does", () => {
102
+ const x = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
103
+ const src = `var tl = gsap.timeline({ paused: true });
104
+ tl.to("#x", { x: 50, duration: 4, ease: "none" }, 0);
105
+ tl.to("#x", { x: 100, duration: 1, ease: "none", overwrite: "auto" }, 2);
106
+ window.__timelines["t"] = tl;`;
107
+ const { timeline, iframe } = play(src, 0.5);
108
+
109
+ const tween = findParsedTween(iframe, x, parseGsapScriptAcorn(src).animations[1]!);
110
+
111
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(25);
112
+ timeline.seek(2.5);
113
+ expect(gsap.getProperty(x, "x")).toBe(62.5);
114
+ timeline.kill();
115
+ });
116
+
117
+ it("saves a dragged keyframe tween with its authored start, not the drag's live value", () => {
118
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
119
+ const src = script(
120
+ "keyframes: { '0%': { opacity: 0 }, '100%': { opacity: 1, x: 300 } }, duration: 1, ease: 'none'",
121
+ ).replace("}, 0);", "}, 2);");
122
+ const { timeline, iframe } = play(src, 0.5);
123
+ usePlayerStore.setState({ currentTime: 0.5, activeKeyframePct: null });
124
+ gsap.set(box, { x: 77 });
125
+ const anim = parseGsapScriptAcorn(src).animations[0]!;
126
+ const selection = { id: "x", selector: "#x", element: box } as DomEditSelection;
127
+
128
+ const plan = planValueEdit(selection, anim, { x: 77 }, iframe);
129
+ timeline.kill();
130
+
131
+ expect(plan.ok).toBe(true);
132
+ if (!plan.ok) return;
133
+ const written = replaceTweenWithKeyframesInScript(src, anim.id, plan.mutation)!;
134
+ const at = (time: number) => {
135
+ const replay = play(written, time);
136
+ const x = gsap.getProperty(box, "x");
137
+ replay.timeline.kill();
138
+ return x;
139
+ };
140
+ expect(at(0.5)).toBe(77);
141
+ expect(at(2)).toBe(0);
142
+ expect(at(3)).toBe(300);
143
+ });
144
+
145
+ it("keeps a layer's other transform values while reading an unplayed tween", () => {
146
+ const box = Object.assign(document.body.appendChild(document.createElement("div")), { id: "x" });
147
+ const src = script(
148
+ "keyframes: { '0%': { opacity: 0 }, '100%': { opacity: 1, x: 300 } }, duration: 1",
149
+ ).replace("}, 0);", "}, 2);");
150
+ const { timeline, iframe } = play(src, 0.5);
151
+ gsap.set(box, {
152
+ x: 77,
153
+ skewX: 23,
154
+ rotationX: 19,
155
+ xPercent: 12,
156
+ transformOrigin: "20px 30px 40px",
157
+ });
158
+
159
+ const tween = findParsedTween(iframe, box, parseGsapScriptAcorn(src).animations[0]!);
160
+ timeline.kill();
161
+
162
+ expect(parsedImplicitEndValue(tween)("x", "start")).toBe(0);
163
+ const values = ["x", "skewX", "rotationX", "xPercent", "transformOrigin"];
164
+ expect(values.map((p) => gsap.getProperty(box, p))).toEqual([77, 23, 19, 12, "20px 30px 40px"]);
165
+ });
166
+
167
+ it("names only an ease GSAP built in, and refuses a custom function rather than guess", () => {
168
+ const custom = { vars: { ease: (p: number) => p * p } };
169
+ const iframe = { contentWindow: { gsap } } as unknown as HTMLIFrameElement;
170
+ expect(parsedTweenEase(iframe, { vars: { ease: gsap.parseEase("expo.in") } })).toBe("expo.in");
171
+ expect(parsedTweenEase(iframe, custom)).toBeNull();
172
+ });
173
+
174
+ it("writes an edit into a looping tween and keeps its repeat and yoyo, as main did", () => {
175
+ const vars = "duration: 2, x: 100, ease: 'none', repeat: 1, yoyo: true";
176
+ const { plan, written } = dragAndReplay(script(vars), 40, 1);
177
+ expect(plan.ok).toBe(true);
178
+ expect(written).toMatch(/repeat: 1[\s\S]*yoyo: true/);
179
+ });