@hyperframes/studio 0.6.109 → 0.6.111
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-BRjQ5a5_.js +418 -0
- package/dist/assets/index-B7S86vK1.js +370 -0
- package/dist/assets/index-DP8pPIk2.css +1 -0
- package/dist/assets/{index-SlMJloLR.js → index-_IV-vm9l.js} +1 -1
- package/dist/assets/{index-CFCe0Xvn.js → index-x0c2-zQN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/App.tsx +133 -141
- package/src/components/StudioHeader.tsx +44 -0
- package/src/components/StudioOverlays.tsx +70 -0
- package/src/components/editor/SourceEditor.tsx +19 -16
- package/src/components/editor/gsapAnimationConstants.ts +24 -0
- package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
- package/src/components/editor/manualEditingAvailability.ts +25 -7
- package/src/components/storyboard/FramePoster.tsx +56 -0
- package/src/components/storyboard/StoryboardDirection.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +262 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +88 -0
- package/src/components/storyboard/StoryboardGrid.tsx +33 -0
- package/src/components/storyboard/StoryboardLoaded.tsx +136 -0
- package/src/components/storyboard/StoryboardScriptPanel.tsx +26 -0
- package/src/components/storyboard/StoryboardSourceEditor.tsx +231 -0
- package/src/components/storyboard/StoryboardStatusLegend.tsx +21 -0
- package/src/components/storyboard/StoryboardView.tsx +78 -0
- package/src/components/storyboard/frameStatus.ts +36 -0
- package/src/contexts/ViewModeContext.tsx +98 -0
- package/src/hooks/gsapScriptCommitTypes.ts +4 -7
- package/src/hooks/sdkSelfWriteRegistry.test.ts +67 -0
- package/src/hooks/sdkSelfWriteRegistry.ts +77 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -0
- package/src/hooks/useAppHotkeys.ts +20 -0
- package/src/hooks/useDomEditCommits.ts +43 -14
- package/src/hooks/useDomEditSession.test.ts +41 -0
- package/src/hooks/useDomEditSession.ts +38 -6
- package/src/hooks/useDomGeometryCommits.ts +5 -9
- package/src/hooks/useElementLifecycleOps.ts +30 -0
- package/src/hooks/useGsapAnimationOps.ts +78 -51
- package/src/hooks/useGsapKeyframeOps.ts +127 -43
- package/src/hooks/useGsapPropertyDebounce.test.ts +70 -0
- package/src/hooks/useGsapPropertyDebounce.ts +201 -23
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +85 -0
- package/src/hooks/useGsapScriptCommits.ts +95 -40
- package/src/hooks/useGsapTweenCache.ts +0 -27
- package/src/hooks/useRazorSplit.ts +4 -10
- package/src/hooks/useSdkSession.test.ts +12 -0
- package/src/hooks/useSdkSession.ts +91 -25
- package/src/hooks/useStoryboard.ts +80 -0
- package/src/hooks/useTimelineEditing.ts +163 -68
- package/src/utils/gsapSoftReload.ts +14 -0
- package/src/utils/sdkCutover.gate.test.ts +61 -0
- package/src/utils/sdkCutover.test.ts +839 -0
- package/src/utils/sdkCutover.ts +465 -0
- package/src/utils/sdkOpMapping.ts +43 -0
- package/src/utils/sdkResolverShadow.test.ts +366 -0
- package/src/utils/sdkResolverShadow.ts +313 -0
- package/src/utils/timelineElementSplit.test.ts +61 -1
- package/src/utils/timelineElementSplit.ts +18 -0
- package/dist/assets/hyperframes-player-67pq7USK.js +0 -418
- package/dist/assets/index-BVqybwMG.css +0 -1
- package/dist/assets/index-ho_f4axK.js +0 -296
- package/src/utils/sdkShadow.test.ts +0 -606
- package/src/utils/sdkShadow.ts +0 -517
- package/src/utils/sdkShadowGsapFidelity.ts +0 -296
- package/src/utils/sdkShadowGsapKeyframe.test.ts +0 -265
- package/src/utils/sdkShadowGsapKeyframe.ts +0 -257
- package/src/utils/sdkShadowNumeric.ts +0 -11
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
// Pre-existing-complex timeline hook (DOM patch + GSAP position shift/scale +
|
|
2
|
-
// playback-start resolution)
|
|
3
|
-
// the move/resize .then() chains, which nudges several callbacks over the CC
|
|
4
|
-
// threshold. The added branches are telemetry-only.
|
|
2
|
+
// playback-start resolution).
|
|
5
3
|
// fallow-ignore-file complexity
|
|
6
4
|
import { useCallback, useRef } from "react";
|
|
7
|
-
import type { Composition } from "@hyperframes/sdk";
|
|
8
|
-
import { runShadowDelete, runShadowTiming } from "../utils/sdkShadow";
|
|
9
5
|
import type { TimelineElement } from "../player";
|
|
10
6
|
import { usePlayerStore } from "../player";
|
|
11
7
|
import { useRazorSplit } from "./useRazorSplit";
|
|
@@ -37,6 +33,8 @@ import {
|
|
|
37
33
|
scaleGsapPositions,
|
|
38
34
|
} from "./timelineEditingHelpers";
|
|
39
35
|
import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
|
|
36
|
+
import { sdkTimingPersist } from "../utils/sdkCutover";
|
|
37
|
+
import type { Composition } from "@hyperframes/sdk";
|
|
40
38
|
|
|
41
39
|
// ── Types ──
|
|
42
40
|
|
|
@@ -60,8 +58,10 @@ interface UseTimelineEditingOptions {
|
|
|
60
58
|
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
61
59
|
uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
|
|
62
60
|
isRecordingRef?: React.RefObject<boolean>;
|
|
63
|
-
/** Stage 7
|
|
61
|
+
/** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
|
|
64
62
|
sdkSession?: Composition | null;
|
|
63
|
+
/** Resync the SDK session after a server-authoritative timeline write. */
|
|
64
|
+
forceReloadSdkSession?: () => void;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ── Hook ──
|
|
@@ -80,6 +80,7 @@ export function useTimelineEditing({
|
|
|
80
80
|
uploadProjectFiles,
|
|
81
81
|
isRecordingRef,
|
|
82
82
|
sdkSession,
|
|
83
|
+
forceReloadSdkSession,
|
|
83
84
|
}: UseTimelineEditingOptions) {
|
|
84
85
|
const projectIdRef = useRef(projectId);
|
|
85
86
|
projectIdRef.current = projectId;
|
|
@@ -92,6 +93,7 @@ export function useTimelineEditing({
|
|
|
92
93
|
element: TimelineElement,
|
|
93
94
|
label: string,
|
|
94
95
|
buildPatches: PersistTimelineEditInput["buildPatches"],
|
|
96
|
+
coalesceKey?: string,
|
|
95
97
|
): Promise<void> => {
|
|
96
98
|
if (isRecordingRef?.current) {
|
|
97
99
|
showToast("Cannot edit timeline while recording", "error");
|
|
@@ -99,19 +101,25 @@ export function useTimelineEditing({
|
|
|
99
101
|
}
|
|
100
102
|
const pid = projectIdRef.current;
|
|
101
103
|
if (!pid) return Promise.resolve();
|
|
102
|
-
const queued = editQueueRef.current
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
const queued = editQueueRef.current
|
|
105
|
+
.then(() =>
|
|
106
|
+
persistTimelineEdit({
|
|
107
|
+
projectId: pid,
|
|
108
|
+
element,
|
|
109
|
+
activeCompPath,
|
|
110
|
+
label,
|
|
111
|
+
buildPatches,
|
|
112
|
+
writeProjectFile,
|
|
113
|
+
recordEdit,
|
|
114
|
+
domEditSaveTimestampRef,
|
|
115
|
+
pendingTimelineEditPathRef,
|
|
116
|
+
coalesceKey,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
.then(() => {
|
|
120
|
+
// Server wrote the file; resync the stale in-memory SDK doc.
|
|
121
|
+
forceReloadSdkSession?.();
|
|
122
|
+
});
|
|
115
123
|
editQueueRef.current = queued.catch((error) => {
|
|
116
124
|
console.error(`[Timeline] Failed to persist: ${label}`, error);
|
|
117
125
|
});
|
|
@@ -125,18 +133,20 @@ export function useTimelineEditing({
|
|
|
125
133
|
pendingTimelineEditPathRef,
|
|
126
134
|
showToast,
|
|
127
135
|
isRecordingRef,
|
|
136
|
+
forceReloadSdkSession,
|
|
128
137
|
],
|
|
129
138
|
);
|
|
130
139
|
|
|
140
|
+
// fallow-ignore-next-line complexity
|
|
131
141
|
const handleTimelineElementMove = useCallback(
|
|
142
|
+
// fallow-ignore-next-line complexity
|
|
132
143
|
(element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
|
|
133
144
|
patchIframeDomTiming(previewIframeRef.current, element, [
|
|
134
145
|
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
135
146
|
["data-track-index", String(updates.track)],
|
|
136
147
|
]);
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
return enqueueEdit(element, "Move timeline clip", (original, target) => {
|
|
148
|
+
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
|
149
|
+
const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
|
|
140
150
|
let patched = applyPatchByTarget(original, target, {
|
|
141
151
|
type: "attribute",
|
|
142
152
|
property: "start",
|
|
@@ -147,24 +157,63 @@ export function useTimelineEditing({
|
|
|
147
157
|
property: "track-index",
|
|
148
158
|
value: String(updates.track),
|
|
149
159
|
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
};
|
|
161
|
+
// Server-path fallback (no SDK session): persist the attr patch, then
|
|
162
|
+
// shift GSAP tween positions on the server and reload the preview — the
|
|
163
|
+
// SDK path folds both into setTiming, but the fallback must do them
|
|
164
|
+
// explicitly or the clip moves while its GSAP tweens stay put + the
|
|
165
|
+
// preview never refreshes. coalesceKey mirrors the SDK branch so undo
|
|
166
|
+
// granularity is identical on either path.
|
|
167
|
+
const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
|
|
168
|
+
const moveFallback = () =>
|
|
169
|
+
enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() => {
|
|
170
|
+
const pid = projectIdRef.current;
|
|
171
|
+
const delta = updates.start - element.start;
|
|
172
|
+
if (delta !== 0 && element.domId && pid) {
|
|
173
|
+
return shiftGsapPositions(pid, targetPath, element.domId, delta)
|
|
174
|
+
.then(() => reloadPreview())
|
|
175
|
+
.catch((err) => console.error("[Timeline] Failed to shift GSAP positions", err));
|
|
176
|
+
}
|
|
177
|
+
return reloadPreview();
|
|
178
|
+
});
|
|
179
|
+
if (sdkSession && element.hfId) {
|
|
180
|
+
return sdkTimingPersist(
|
|
181
|
+
element.hfId,
|
|
182
|
+
targetPath,
|
|
183
|
+
{ start: updates.start, trackIndex: updates.track },
|
|
184
|
+
sdkSession,
|
|
185
|
+
{
|
|
186
|
+
editHistory: { recordEdit },
|
|
187
|
+
writeProjectFile,
|
|
188
|
+
reloadPreview,
|
|
189
|
+
domEditSaveTimestampRef,
|
|
190
|
+
compositionPath: activeCompPath,
|
|
191
|
+
// Capture on-disk bytes as the undo `before` so undoing a timing move
|
|
192
|
+
// restores the file verbatim, not a normalized full-DOM re-emit.
|
|
193
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
194
|
+
},
|
|
195
|
+
{ label: "Move timeline clip", coalesceKey },
|
|
196
|
+
).then((handled) => {
|
|
197
|
+
if (!handled) return moveFallback();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return moveFallback();
|
|
163
201
|
},
|
|
164
|
-
[
|
|
202
|
+
[
|
|
203
|
+
previewIframeRef,
|
|
204
|
+
enqueueEdit,
|
|
205
|
+
activeCompPath,
|
|
206
|
+
sdkSession,
|
|
207
|
+
recordEdit,
|
|
208
|
+
writeProjectFile,
|
|
209
|
+
reloadPreview,
|
|
210
|
+
domEditSaveTimestampRef,
|
|
211
|
+
],
|
|
165
212
|
);
|
|
166
213
|
|
|
214
|
+
// fallow-ignore-next-line complexity
|
|
167
215
|
const handleTimelineElementResize = useCallback(
|
|
216
|
+
// fallow-ignore-next-line complexity
|
|
168
217
|
(
|
|
169
218
|
element: TimelineElement,
|
|
170
219
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
@@ -173,6 +222,9 @@ export function useTimelineEditing({
|
|
|
173
222
|
["data-start", formatTimelineAttributeNumber(updates.start)],
|
|
174
223
|
["data-duration", formatTimelineAttributeNumber(updates.duration)],
|
|
175
224
|
];
|
|
225
|
+
// Patch the live playback-start/media-start attr too, or a resize that
|
|
226
|
+
// trims the playback start leaves the preview showing the old in-point
|
|
227
|
+
// until the next reload (the persisted patch handles it via pbs below).
|
|
176
228
|
if (updates.playbackStart != null) {
|
|
177
229
|
const liveAttr =
|
|
178
230
|
element.playbackStartAttr === "playback-start"
|
|
@@ -181,10 +233,8 @@ export function useTimelineEditing({
|
|
|
181
233
|
liveAttrs.push([liveAttr, formatTimelineAttributeNumber(updates.playbackStart)]);
|
|
182
234
|
}
|
|
183
235
|
patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
updates.start !== element.start || updates.duration !== element.duration;
|
|
187
|
-
return enqueueEdit(element, "Resize timeline clip", (original, target) => {
|
|
236
|
+
const targetPath = element.sourceFile || activeCompPath || "index.html";
|
|
237
|
+
const buildResizePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
|
|
188
238
|
const pbs = resolveResizePlaybackStart(original, target, element, updates);
|
|
189
239
|
let patched = applyPatchByTarget(original, target, {
|
|
190
240
|
type: "attribute",
|
|
@@ -204,34 +254,77 @@ export function useTimelineEditing({
|
|
|
204
254
|
});
|
|
205
255
|
}
|
|
206
256
|
return patched;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
257
|
+
};
|
|
258
|
+
// SDK path: skip when a playback-start adjustment is needed (setTiming has no pbs field).
|
|
259
|
+
// The second clause fires because trimming the start of a clip that has a
|
|
260
|
+
// playback-start attribute implicitly shifts that in-point — which the SDK
|
|
261
|
+
// setTiming op can't express — so those resizes must take the server path.
|
|
262
|
+
const hasPbsAdjustment =
|
|
263
|
+
updates.playbackStart != null ||
|
|
264
|
+
(updates.start !== element.start && element.playbackStart != null);
|
|
265
|
+
// Server-path fallback: after persisting the attr patch, scale GSAP tween
|
|
266
|
+
// positions/durations on the server and reload the preview. The SDK path
|
|
267
|
+
// folds both into setTiming; the fallback must do them explicitly or the
|
|
268
|
+
// clip resizes while its GSAP tweens keep their old timing + the preview
|
|
269
|
+
// never refreshes. coalesceKey mirrors the SDK branch for undo parity.
|
|
270
|
+
const coalesceKey = `timeline-resize:${element.hfId ?? element.id}`;
|
|
271
|
+
const timingChanged =
|
|
272
|
+
updates.start !== element.start || updates.duration !== element.duration;
|
|
273
|
+
const resizeFallback = () =>
|
|
274
|
+
enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(() => {
|
|
275
|
+
const pid = projectIdRef.current;
|
|
276
|
+
if (timingChanged && element.domId && pid) {
|
|
277
|
+
return scaleGsapPositions(
|
|
278
|
+
pid,
|
|
279
|
+
targetPath,
|
|
280
|
+
element.domId,
|
|
281
|
+
element.start,
|
|
282
|
+
element.duration,
|
|
283
|
+
updates.start,
|
|
284
|
+
updates.duration,
|
|
285
|
+
)
|
|
286
|
+
.then(() => reloadPreview())
|
|
287
|
+
.catch((err) => console.error("[Timeline] Failed to scale GSAP positions", err));
|
|
288
|
+
}
|
|
289
|
+
return reloadPreview();
|
|
290
|
+
});
|
|
291
|
+
if (sdkSession && element.hfId && !hasPbsAdjustment) {
|
|
292
|
+
return sdkTimingPersist(
|
|
293
|
+
element.hfId,
|
|
294
|
+
targetPath,
|
|
295
|
+
{ start: updates.start, duration: updates.duration },
|
|
296
|
+
sdkSession,
|
|
297
|
+
{
|
|
298
|
+
editHistory: { recordEdit },
|
|
299
|
+
writeProjectFile,
|
|
300
|
+
reloadPreview,
|
|
301
|
+
domEditSaveTimestampRef,
|
|
302
|
+
compositionPath: activeCompPath,
|
|
303
|
+
// Capture on-disk bytes as the undo `before` so undoing a timing
|
|
304
|
+
// resize restores the file verbatim, not a normalized full-DOM re-emit.
|
|
305
|
+
readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
|
|
306
|
+
},
|
|
307
|
+
{ label: "Resize timeline clip", coalesceKey },
|
|
308
|
+
).then((handled) => {
|
|
309
|
+
if (!handled) return resizeFallback();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return resizeFallback();
|
|
229
313
|
},
|
|
230
|
-
[
|
|
314
|
+
[
|
|
315
|
+
previewIframeRef,
|
|
316
|
+
enqueueEdit,
|
|
317
|
+
activeCompPath,
|
|
318
|
+
sdkSession,
|
|
319
|
+
recordEdit,
|
|
320
|
+
writeProjectFile,
|
|
321
|
+
reloadPreview,
|
|
322
|
+
domEditSaveTimestampRef,
|
|
323
|
+
],
|
|
231
324
|
);
|
|
232
325
|
|
|
326
|
+
// fallow-ignore-next-line complexity
|
|
233
327
|
const handleTimelineElementDelete = useCallback(
|
|
234
|
-
// Pre-existing handler complexity, unchanged by this PR.
|
|
235
328
|
// fallow-ignore-next-line complexity
|
|
236
329
|
async (element: TimelineElement) => {
|
|
237
330
|
if (isRecordingRef?.current) {
|
|
@@ -287,8 +380,8 @@ export function useTimelineEditing({
|
|
|
287
380
|
timelineElements.filter((te) => (te.key ?? te.id) !== (element.key ?? element.id)),
|
|
288
381
|
);
|
|
289
382
|
usePlayerStore.getState().setSelectedElementId(null);
|
|
383
|
+
forceReloadSdkSession?.();
|
|
290
384
|
reloadPreview();
|
|
291
|
-
if (sdkSession) runShadowDelete(sdkSession, element.hfId);
|
|
292
385
|
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
|
293
386
|
} catch (error) {
|
|
294
387
|
const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
|
|
@@ -304,12 +397,12 @@ export function useTimelineEditing({
|
|
|
304
397
|
domEditSaveTimestampRef,
|
|
305
398
|
reloadPreview,
|
|
306
399
|
isRecordingRef,
|
|
307
|
-
|
|
400
|
+
forceReloadSdkSession,
|
|
308
401
|
],
|
|
309
402
|
);
|
|
310
403
|
|
|
404
|
+
// fallow-ignore-next-line complexity
|
|
311
405
|
const handleTimelineAssetDrop = useCallback(
|
|
312
|
-
// Pre-existing handler complexity, unchanged by this PR.
|
|
313
406
|
// fallow-ignore-next-line complexity
|
|
314
407
|
async (
|
|
315
408
|
assetPath: string,
|
|
@@ -373,6 +466,7 @@ export function useTimelineEditing({
|
|
|
373
466
|
recordEdit,
|
|
374
467
|
});
|
|
375
468
|
|
|
469
|
+
forceReloadSdkSession?.();
|
|
376
470
|
reloadPreview();
|
|
377
471
|
} catch (error) {
|
|
378
472
|
const message =
|
|
@@ -389,11 +483,12 @@ export function useTimelineEditing({
|
|
|
389
483
|
domEditSaveTimestampRef,
|
|
390
484
|
reloadPreview,
|
|
391
485
|
isRecordingRef,
|
|
486
|
+
forceReloadSdkSession,
|
|
392
487
|
],
|
|
393
488
|
);
|
|
394
489
|
|
|
490
|
+
// fallow-ignore-next-line complexity
|
|
395
491
|
const handleTimelineFileDrop = useCallback(
|
|
396
|
-
// Pre-existing handler complexity, unchanged by this PR.
|
|
397
492
|
// fallow-ignore-next-line complexity
|
|
398
493
|
async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
|
|
399
494
|
if (isRecordingRef?.current) {
|
|
@@ -31,6 +31,19 @@ function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
|
31
31
|
return results;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Extract the GSAP timeline script text from a serialized HTML document, for
|
|
36
|
+
* feeding into applySoftReload. Returns null when zero or multiple GSAP scripts
|
|
37
|
+
* are present (ambiguous — caller should fall back to a full reload), matching
|
|
38
|
+
* applySoftReload's own single-script requirement.
|
|
39
|
+
*/
|
|
40
|
+
export function extractGsapScriptText(html: string): string | null {
|
|
41
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
42
|
+
const scripts = findGsapScriptElements(doc);
|
|
43
|
+
if (scripts.length !== 1) return null;
|
|
44
|
+
return scripts[0].textContent || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
/** Check that the new script repopulated __timelines with at least one entry. */
|
|
35
48
|
function verifyTimelinesPopulated(win: IframeWindow): boolean {
|
|
36
49
|
const tlKeys = win.__timelines
|
|
@@ -73,6 +86,7 @@ export function applySoftReload(iframe: HTMLIFrameElement | null, scriptText: st
|
|
|
73
86
|
// full iframe reload that destroys the very WebGL context we're preserving.
|
|
74
87
|
let deferredToAsync = false;
|
|
75
88
|
|
|
89
|
+
// fallow-ignore-next-line complexity
|
|
76
90
|
const doReload = () => {
|
|
77
91
|
const timelines = win.__timelines;
|
|
78
92
|
const allTargets: Element[] = [];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Dark-launch contract: with STUDIO_SDK_CUTOVER_ENABLED=false, EVERY cutover
|
|
4
|
+
// persist chokepoint must return false so the caller takes the legacy server
|
|
5
|
+
// path — even when a valid SDK session exists (one always does, for
|
|
6
|
+
// shadow/selection). This is the contract the prod flag-flip rests on; a future
|
|
7
|
+
// refactor of the gate guards that silently re-enables cutover on flag-off
|
|
8
|
+
// turns these red. (sdkCutover.test.ts mocks the flag TRUE; this is its sibling.)
|
|
9
|
+
vi.mock("../components/editor/manualEditingAvailability", () => ({
|
|
10
|
+
STUDIO_SDK_CUTOVER_ENABLED: false,
|
|
11
|
+
STUDIO_SDK_RESOLVER_SHADOW_ENABLED: false,
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("./studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
14
|
+
|
|
15
|
+
import { sdkTimingPersist, sdkGsapTweenPersist, sdkDeletePersist } from "./sdkCutover";
|
|
16
|
+
|
|
17
|
+
const makeSession = () =>
|
|
18
|
+
({
|
|
19
|
+
getElement: () => ({ inlineStyles: {} }),
|
|
20
|
+
serialize: () => "<html></html>",
|
|
21
|
+
batch: (fn: () => void) => fn(),
|
|
22
|
+
setTiming: vi.fn(),
|
|
23
|
+
dispatch: vi.fn(),
|
|
24
|
+
}) as never;
|
|
25
|
+
|
|
26
|
+
const makeDeps = () =>
|
|
27
|
+
({
|
|
28
|
+
editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
|
|
29
|
+
writeProjectFile: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
reloadPreview: vi.fn(),
|
|
31
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
32
|
+
}) as never;
|
|
33
|
+
|
|
34
|
+
describe("dark-launch gate — STUDIO_SDK_CUTOVER_ENABLED=false ⇒ persist returns false", () => {
|
|
35
|
+
it("sdkTimingPersist falls back without writing", async () => {
|
|
36
|
+
const deps = makeDeps();
|
|
37
|
+
expect(await sdkTimingPersist("hf-a", "/c.html", { start: 1 }, makeSession(), deps)).toBe(
|
|
38
|
+
false,
|
|
39
|
+
);
|
|
40
|
+
expect(
|
|
41
|
+
(deps as unknown as { writeProjectFile: ReturnType<typeof vi.fn> }).writeProjectFile,
|
|
42
|
+
).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("sdkGsapTweenPersist (shared GSAP-op chokepoint) falls back", async () => {
|
|
46
|
+
expect(
|
|
47
|
+
await sdkGsapTweenPersist(
|
|
48
|
+
"/c.html",
|
|
49
|
+
{ kind: "remove", animationId: "a" },
|
|
50
|
+
makeSession(),
|
|
51
|
+
makeDeps(),
|
|
52
|
+
),
|
|
53
|
+
).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("sdkDeletePersist falls back", async () => {
|
|
57
|
+
expect(
|
|
58
|
+
await sdkDeletePersist("hf-a", "<html></html>", "/c.html", makeSession(), makeDeps()),
|
|
59
|
+
).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|