@hyperframes/studio 0.8.75 → 0.8.77
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-BSDTJ9EF.js +464 -0
- package/dist/assets/{index-BlUYw1MB.js → index-2GgHjQq-.js} +163 -163
- package/dist/assets/{index-0w6iIlwZ.css → index-BWJqxeaH.css} +1 -1
- package/dist/assets/{index-BiFKcNjs.js → index-CsFCRdMo.js} +1 -1
- package/dist/assets/{index-HjexPsny.js → index-DcxWQ7Za.js} +1 -1
- package/dist/{chunk-7BP32G62.js → chunk-5VANA2TG.js} +12 -1
- package/dist/chunk-5VANA2TG.js.map +1 -0
- package/dist/{domEditingLayers-UKMVYXT6.js → domEditingLayers-IGPLTI5E.js} +4 -2
- package/dist/index.d.ts +34 -69
- package/dist/index.html +2 -2
- package/dist/index.js +5287 -5277
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanels.types.ts +0 -2
- package/src/components/editor/DomEditOverlay.test.ts +5 -0
- package/src/components/editor/LayersPanel.tsx +3 -14
- package/src/components/editor/PreviewOverlayProvider.test.tsx +6 -1
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingElement.ts +13 -0
- package/src/components/editor/domEditingLayers.test.ts +27 -0
- package/src/components/editor/domEditingLayers.ts +9 -0
- package/src/components/editor/gsapLivePreview.test.ts +63 -0
- package/src/components/editor/gsapLivePreview.ts +2 -14
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +57 -35
- package/src/components/editor/overlayFrameLoop.test.ts +16 -0
- package/src/components/editor/overlayFrameLoop.ts +11 -0
- package/src/components/editor/propertyPanelHelpers.ts +4 -6
- package/src/components/editor/usePreviewCompositionRect.test.tsx +6 -1
- package/src/components/nle/NLEPreview.test.ts +202 -22
- package/src/components/nle/NLEPreview.tsx +65 -46
- package/src/components/nle/PreviewPoster.tsx +99 -0
- package/src/components/nle/PreviewZoomOverlay.tsx +111 -0
- package/src/components/nle/previewZoom.test.ts +42 -0
- package/src/components/nle/previewZoom.ts +33 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +30 -49
- package/src/components/panels/VariablesPanel.tsx +0 -2
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +5 -1
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +14 -0
- package/src/components/sidebar/CompositionsTab.test.ts +4 -6
- package/src/components/sidebar/CompositionsTab.tsx +28 -21
- package/src/components/studioColorGradingScope.ts +0 -3
- package/src/hooks/domEditDeleteMembers.ts +0 -2
- package/src/hooks/gsapScriptCommitTypes.ts +0 -2
- package/src/hooks/keyframeCacheAstLoad.test.ts +31 -1
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/timelineAudioGroupCreate.ts +0 -1
- package/src/hooks/timelineEditingHelpers.ts +0 -3
- package/src/hooks/timelineTimingSync.ts +0 -1
- package/src/hooks/timelineTrackVisibility.ts +0 -1
- package/src/hooks/useBlockHandlers.ts +0 -2
- package/src/hooks/useClipboard.test.ts +69 -10
- package/src/hooks/useClipboard.ts +28 -19
- package/src/hooks/useDomEditCommits.test.tsx +0 -1
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useDomEditWiring.ts +1 -3
- package/src/hooks/useEditHistoryActions.test.tsx +31 -14
- package/src/hooks/useEditHistoryActions.ts +12 -7
- package/src/hooks/useEditorSave.ts +0 -3
- package/src/hooks/useElementLifecycleOps.ts +0 -1
- package/src/hooks/useEnableKeyframes.test.ts +9 -1
- package/src/hooks/useFileManager.ts +0 -2
- package/src/hooks/useGestureRecording.test.tsx +1 -1
- package/src/hooks/useGestureRecording.ts +6 -10
- package/src/hooks/useGroupCommits.ts +0 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -1
- package/src/hooks/useGsapTweenCache.element.test.tsx +39 -0
- package/src/hooks/useGsapTweenCache.ts +5 -13
- package/src/hooks/useLintModal.test.tsx +34 -1
- package/src/hooks/useLintModal.ts +32 -30
- package/src/hooks/usePersistentEditHistory.test.ts +221 -315
- package/src/hooks/usePersistentEditHistory.ts +155 -342
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +2 -0
- package/src/hooks/usePreviewPersistence.ts +0 -2
- package/src/hooks/useProjectCompositionVariables.ts +1 -3
- package/src/hooks/useRazorSplit.history.test.tsx +46 -67
- package/src/hooks/useRazorSplit.test.ts +17 -47
- package/src/hooks/useRenderClipContent.test.ts +29 -0
- package/src/hooks/useRenderClipContent.ts +6 -3
- package/src/hooks/useSdkSession.lifecycle.test.tsx +34 -4
- package/src/hooks/useSdkSession.ts +8 -2
- package/src/hooks/useSlideshowPersist.ts +0 -2
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useTimelineAssetDropOps.ts +0 -1
- package/src/hooks/useTimelineDeleteOps.ts +0 -1
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/hooks/useTimelineEditingTypes.ts +0 -2
- package/src/hostMount.test.tsx +53 -22
- package/src/main.tsx +3 -0
- package/src/player/components/CompositionThumbnail.tsx +14 -0
- package/src/player/components/Player.test.ts +49 -0
- package/src/player/components/Player.tsx +21 -7
- package/src/player/components/timelineClipDragCommit.test.ts +7 -39
- package/src/player/hooks/timelineDomBinding.test.ts +50 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +25 -1
- package/src/player/hooks/timelineSyncHydration.ts +3 -3
- package/src/player/hooks/useShadowPreviewReload.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.play.test.ts +120 -0
- package/src/player/hooks/useTimelinePlayer.ts +1 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/automationStoreSync.test.ts +24 -0
- package/src/player/lib/automationStoreSync.ts +3 -9
- package/src/player/lib/playbackScrub.ts +1 -1
- package/src/player/lib/timelineDOM.test.ts +28 -0
- package/src/player/lib/timelineDOM.ts +4 -2
- package/src/player/lib/timelineElementHelpers.ts +78 -9
- package/src/player/lib/timelineIframeHelpers.test.ts +49 -5
- package/src/player/lib/timelineIframeHelpers.ts +13 -12
- package/src/player/store/playerStore.test.ts +35 -1
- package/src/player/store/playerStore.ts +29 -0
- package/src/player/store/readinessSlice.ts +34 -6
- package/src/utils/blockInstaller.ts +0 -3
- package/src/utils/previewPrefetch.test.ts +22 -0
- package/src/utils/previewPrefetch.ts +14 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/razorSplitTransaction.ts +1 -1
- package/src/utils/sdkEditTransaction.ts +0 -3
- package/src/utils/studioFileHistory.test.ts +0 -9
- package/src/utils/studioFileHistory.ts +1 -5
- package/src/utils/studioUiPreferences.test.ts +9 -3
- package/src/utils/studioUiPreferences.ts +0 -20
- package/src/utils/timelineCompositionInsert.ts +0 -1
- package/dist/assets/hyperframes-player-WbBVbWuO.js +0 -464
- package/dist/chunk-7BP32G62.js.map +0 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +0 -60
- package/src/utils/editHistory.test.ts +0 -534
- package/src/utils/editHistory.ts +0 -236
- package/src/utils/editHistoryStorage.test.ts +0 -37
- package/src/utils/editHistoryStorage.ts +0 -99
- /package/dist/{domEditingLayers-UKMVYXT6.js.map → domEditingLayers-IGPLTI5E.js.map} +0 -0
|
@@ -1,534 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildEditHistoryEntry,
|
|
4
|
-
canApplyEditHistoryEntry,
|
|
5
|
-
createEmptyEditHistory,
|
|
6
|
-
hashEditHistoryContent,
|
|
7
|
-
pushEditHistoryEntry,
|
|
8
|
-
redoEditHistory,
|
|
9
|
-
undoEditHistory,
|
|
10
|
-
} from "./editHistory";
|
|
11
|
-
|
|
12
|
-
describe("edit history", () => {
|
|
13
|
-
it("pushes changed file snapshots onto undo and clears redo", () => {
|
|
14
|
-
const state = createEmptyEditHistory();
|
|
15
|
-
const entry = buildEditHistoryEntry({
|
|
16
|
-
projectId: "project-1",
|
|
17
|
-
label: "Move layer",
|
|
18
|
-
files: {
|
|
19
|
-
"index.html": {
|
|
20
|
-
before: '<div style="left: 0px"></div>',
|
|
21
|
-
after: '<div style="left: 20px"></div>',
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
now: 100,
|
|
25
|
-
id: "entry-1",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const withUndo = pushEditHistoryEntry(state, entry);
|
|
29
|
-
const redoEntry = buildEditHistoryEntry({
|
|
30
|
-
projectId: "project-1",
|
|
31
|
-
label: "Redoable edit",
|
|
32
|
-
files: {
|
|
33
|
-
"index.html": {
|
|
34
|
-
before: '<div style="left: 20px"></div>',
|
|
35
|
-
after: '<div style="left: 40px"></div>',
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
now: 200,
|
|
39
|
-
id: "redo-entry",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const next = pushEditHistoryEntry(
|
|
43
|
-
{
|
|
44
|
-
...withUndo,
|
|
45
|
-
redo: [redoEntry],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
...entry,
|
|
49
|
-
id: "entry-2",
|
|
50
|
-
label: "Resize layer",
|
|
51
|
-
createdAt: 300,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(withUndo.undo).toHaveLength(1);
|
|
56
|
-
expect(withUndo.redo).toHaveLength(0);
|
|
57
|
-
expect(next.undo.map((item) => item.label)).toEqual(["Move layer", "Resize layer"]);
|
|
58
|
-
expect(next.redo).toHaveLength(0);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("undo returns before contents and moves entry to redo", () => {
|
|
62
|
-
const entry = buildEditHistoryEntry({
|
|
63
|
-
projectId: "project-1",
|
|
64
|
-
label: "Move layer",
|
|
65
|
-
files: {
|
|
66
|
-
"index.html": { before: "before", after: "after" },
|
|
67
|
-
},
|
|
68
|
-
now: 100,
|
|
69
|
-
id: "entry-1",
|
|
70
|
-
});
|
|
71
|
-
const state = pushEditHistoryEntry(createEmptyEditHistory(), entry);
|
|
72
|
-
|
|
73
|
-
const result = undoEditHistory(state, { "index.html": hashEditHistoryContent("after") }, 200);
|
|
74
|
-
|
|
75
|
-
expect(result.ok).toBe(true);
|
|
76
|
-
expect(result.filesToWrite).toEqual({ "index.html": "before" });
|
|
77
|
-
expect(result.state.undo).toHaveLength(0);
|
|
78
|
-
expect(result.state.redo.map((item) => item.id)).toEqual(["entry-1"]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("redo returns after contents and moves entry to undo", () => {
|
|
82
|
-
const entry = buildEditHistoryEntry({
|
|
83
|
-
projectId: "project-1",
|
|
84
|
-
label: "Move layer",
|
|
85
|
-
files: {
|
|
86
|
-
"index.html": { before: "before", after: "after" },
|
|
87
|
-
},
|
|
88
|
-
now: 100,
|
|
89
|
-
id: "entry-1",
|
|
90
|
-
});
|
|
91
|
-
const undone = undoEditHistory(
|
|
92
|
-
pushEditHistoryEntry(createEmptyEditHistory(), entry),
|
|
93
|
-
{ "index.html": hashEditHistoryContent("after") },
|
|
94
|
-
200,
|
|
95
|
-
).state;
|
|
96
|
-
|
|
97
|
-
const result = redoEditHistory(undone, { "index.html": hashEditHistoryContent("before") }, 300);
|
|
98
|
-
|
|
99
|
-
expect(result.ok).toBe(true);
|
|
100
|
-
expect(result.filesToWrite).toEqual({ "index.html": "after" });
|
|
101
|
-
expect(result.state.undo.map((item) => item.id)).toEqual(["entry-1"]);
|
|
102
|
-
expect(result.state.redo).toHaveLength(0);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("blocks undo when current content hash does not match the recorded after hash", () => {
|
|
106
|
-
const entry = buildEditHistoryEntry({
|
|
107
|
-
projectId: "project-1",
|
|
108
|
-
label: "Move layer",
|
|
109
|
-
files: {
|
|
110
|
-
"index.html": { before: "before", after: "after" },
|
|
111
|
-
},
|
|
112
|
-
now: 100,
|
|
113
|
-
id: "entry-1",
|
|
114
|
-
});
|
|
115
|
-
const state = pushEditHistoryEntry(createEmptyEditHistory(), entry);
|
|
116
|
-
|
|
117
|
-
const result = undoEditHistory(
|
|
118
|
-
state,
|
|
119
|
-
{ "index.html": hashEditHistoryContent("external") },
|
|
120
|
-
200,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
expect(result.ok).toBe(false);
|
|
124
|
-
expect(result.reason).toBe("content-mismatch");
|
|
125
|
-
expect(result.state).toBe(state);
|
|
126
|
-
expect(result.filesToWrite).toEqual({});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("can validate all files in a multi-file entry before applying", () => {
|
|
130
|
-
const entry = buildEditHistoryEntry({
|
|
131
|
-
projectId: "project-1",
|
|
132
|
-
label: "Update files",
|
|
133
|
-
files: {
|
|
134
|
-
"index.html": { before: "a", after: "b" },
|
|
135
|
-
"compositions/title.html": { before: "c", after: "d" },
|
|
136
|
-
},
|
|
137
|
-
now: 100,
|
|
138
|
-
id: "entry-1",
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
expect(
|
|
142
|
-
canApplyEditHistoryEntry(entry, "undo", {
|
|
143
|
-
"index.html": hashEditHistoryContent("b"),
|
|
144
|
-
"compositions/title.html": hashEditHistoryContent("d"),
|
|
145
|
-
}),
|
|
146
|
-
).toEqual({ ok: true });
|
|
147
|
-
expect(
|
|
148
|
-
canApplyEditHistoryEntry(entry, "undo", {
|
|
149
|
-
"index.html": hashEditHistoryContent("b"),
|
|
150
|
-
"compositions/title.html": hashEditHistoryContent("external"),
|
|
151
|
-
}),
|
|
152
|
-
).toEqual({ ok: false, reason: "content-mismatch", path: "compositions/title.html" });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("prunes oldest undo entries when the limit is exceeded", () => {
|
|
156
|
-
let state = createEmptyEditHistory({ maxEntries: 2 });
|
|
157
|
-
for (let index = 1; index <= 3; index += 1) {
|
|
158
|
-
state = pushEditHistoryEntry(
|
|
159
|
-
state,
|
|
160
|
-
buildEditHistoryEntry({
|
|
161
|
-
projectId: "project-1",
|
|
162
|
-
label: `Edit ${index}`,
|
|
163
|
-
files: {
|
|
164
|
-
"index.html": { before: `${index - 1}`, after: `${index}` },
|
|
165
|
-
},
|
|
166
|
-
now: index,
|
|
167
|
-
id: `entry-${index}`,
|
|
168
|
-
}),
|
|
169
|
-
{ maxEntries: 2 },
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
expect(state.undo.map((entry) => entry.id)).toEqual(["entry-2", "entry-3"]);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("coalesces source editor edits for the same file inside the coalesce window", () => {
|
|
177
|
-
const first = buildEditHistoryEntry({
|
|
178
|
-
projectId: "project-1",
|
|
179
|
-
label: "Edit source",
|
|
180
|
-
kind: "source",
|
|
181
|
-
coalesceKey: "source:index.html",
|
|
182
|
-
files: {
|
|
183
|
-
"index.html": { before: "a", after: "b" },
|
|
184
|
-
},
|
|
185
|
-
now: 100,
|
|
186
|
-
id: "entry-1",
|
|
187
|
-
});
|
|
188
|
-
const second = buildEditHistoryEntry({
|
|
189
|
-
projectId: "project-1",
|
|
190
|
-
label: "Edit source",
|
|
191
|
-
kind: "source",
|
|
192
|
-
coalesceKey: "source:index.html",
|
|
193
|
-
files: {
|
|
194
|
-
"index.html": { before: "b", after: "c" },
|
|
195
|
-
},
|
|
196
|
-
now: 300,
|
|
197
|
-
id: "entry-2",
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const state = pushEditHistoryEntry(
|
|
201
|
-
pushEditHistoryEntry(createEmptyEditHistory(), first),
|
|
202
|
-
second,
|
|
203
|
-
{ coalesceMs: 1000 },
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
expect(state.undo).toHaveLength(1);
|
|
207
|
-
expect(state.undo[0].id).toBe("entry-2");
|
|
208
|
-
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
209
|
-
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("merges a lane-change move with its z-reorder past the default window via entry coalesceMs", () => {
|
|
213
|
-
// The z entry records only after the move persist's round-trip — often >300ms.
|
|
214
|
-
// Both sides pass coalesceMs: 5000 with the shared gesture key so the pair
|
|
215
|
-
// still folds into ONE undo step.
|
|
216
|
-
const move = buildEditHistoryEntry({
|
|
217
|
-
projectId: "project-1",
|
|
218
|
-
label: "Move timeline clips",
|
|
219
|
-
kind: "timeline",
|
|
220
|
-
coalesceKey: "clip-lane-move:1",
|
|
221
|
-
coalesceMs: 5000,
|
|
222
|
-
files: { "index.html": { before: "a", after: "b" } },
|
|
223
|
-
now: 100,
|
|
224
|
-
id: "move-entry",
|
|
225
|
-
});
|
|
226
|
-
const zReorder = buildEditHistoryEntry({
|
|
227
|
-
projectId: "project-1",
|
|
228
|
-
label: "Reorder layers",
|
|
229
|
-
kind: "manual",
|
|
230
|
-
coalesceKey: "clip-lane-move:1",
|
|
231
|
-
coalesceMs: 5000,
|
|
232
|
-
files: { "index.html": { before: "b", after: "c" } },
|
|
233
|
-
now: 500,
|
|
234
|
-
id: "z-entry",
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const state = pushEditHistoryEntry(
|
|
238
|
-
pushEditHistoryEntry(createEmptyEditHistory(), move),
|
|
239
|
-
zReorder,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(state.undo).toHaveLength(1);
|
|
243
|
-
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
244
|
-
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("folds a slow GSAP follow-up into the timing edit via a per-entry coalesceMs override", () => {
|
|
248
|
-
const timing = buildEditHistoryEntry({
|
|
249
|
-
projectId: "project-1",
|
|
250
|
-
label: "Resize timeline clip",
|
|
251
|
-
kind: "timeline",
|
|
252
|
-
coalesceKey: "timeline-resize:clip",
|
|
253
|
-
files: { "index.html": { before: "orig", after: "timing" } },
|
|
254
|
-
now: 0,
|
|
255
|
-
id: "timing",
|
|
256
|
-
});
|
|
257
|
-
// The server GSAP rewrite lands ~2s later, past the 300ms default window, but the
|
|
258
|
-
// follow-up carries a large coalesceMs so undo still collapses to a single step.
|
|
259
|
-
const gsap = buildEditHistoryEntry({
|
|
260
|
-
projectId: "project-1",
|
|
261
|
-
label: "Resize timeline clip",
|
|
262
|
-
kind: "timeline",
|
|
263
|
-
coalesceKey: "timeline-resize:clip",
|
|
264
|
-
coalesceMs: 10_000,
|
|
265
|
-
files: { "index.html": { before: "timing", after: "timing+gsap" } },
|
|
266
|
-
now: 2000,
|
|
267
|
-
id: "gsap",
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
const state = pushEditHistoryEntry(
|
|
271
|
-
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
272
|
-
gsap,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
expect(state.undo).toHaveLength(1);
|
|
276
|
-
expect(state.undo[0].files["index.html"].before).toBe("orig");
|
|
277
|
-
expect(state.undo[0].files["index.html"].after).toBe("timing+gsap");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("keeps timing-only files when a multi-file GSAP follow-up touches one path", () => {
|
|
281
|
-
const timing = buildEditHistoryEntry({
|
|
282
|
-
projectId: "project-1",
|
|
283
|
-
label: "Move timeline clips",
|
|
284
|
-
kind: "timeline",
|
|
285
|
-
coalesceKey: "timeline-group-move:a,b",
|
|
286
|
-
files: {
|
|
287
|
-
"index.html": { before: "index-before", after: "index-timing" },
|
|
288
|
-
"scene.html": { before: "scene-before", after: "scene-timing" },
|
|
289
|
-
},
|
|
290
|
-
now: 0,
|
|
291
|
-
id: "timing",
|
|
292
|
-
});
|
|
293
|
-
const gsap = buildEditHistoryEntry({
|
|
294
|
-
projectId: "project-1",
|
|
295
|
-
label: "Move timeline clips",
|
|
296
|
-
kind: "timeline",
|
|
297
|
-
coalesceKey: "timeline-group-move:a,b",
|
|
298
|
-
coalesceMs: 10_000,
|
|
299
|
-
files: {
|
|
300
|
-
"index.html": { before: "index-timing", after: "index-timing+gsap" },
|
|
301
|
-
},
|
|
302
|
-
now: 2000,
|
|
303
|
-
id: "gsap",
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const state = pushEditHistoryEntry(
|
|
307
|
-
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
308
|
-
gsap,
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
expect(state.undo).toHaveLength(1);
|
|
312
|
-
expect(Object.keys(state.undo[0].files)).toEqual(["index.html", "scene.html"]);
|
|
313
|
-
expect(state.undo[0].files).toMatchObject({
|
|
314
|
-
"index.html": { before: "index-before", after: "index-timing+gsap" },
|
|
315
|
-
"scene.html": { before: "scene-before", after: "scene-timing" },
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("keeps a discontinuous GSAP follow-up separate and preserves foreign bytes on undo", () => {
|
|
320
|
-
const timing = buildEditHistoryEntry({
|
|
321
|
-
projectId: "project-1",
|
|
322
|
-
label: "Move timeline clip",
|
|
323
|
-
kind: "timeline",
|
|
324
|
-
coalesceKey: "timeline-move:clip",
|
|
325
|
-
files: { "index.html": { before: "A", after: "B" } },
|
|
326
|
-
now: 0,
|
|
327
|
-
id: "timing",
|
|
328
|
-
});
|
|
329
|
-
const gsapAfterForeignWrite = buildEditHistoryEntry({
|
|
330
|
-
projectId: "project-1",
|
|
331
|
-
label: "Move timeline clip",
|
|
332
|
-
kind: "timeline",
|
|
333
|
-
coalesceKey: "timeline-move:clip",
|
|
334
|
-
coalesceMs: 10_000,
|
|
335
|
-
files: { "index.html": { before: "F", after: "G" } },
|
|
336
|
-
now: 2000,
|
|
337
|
-
id: "gsap",
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const state = pushEditHistoryEntry(
|
|
341
|
-
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
342
|
-
gsapAfterForeignWrite,
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
expect(state.undo.map((entry) => entry.id)).toEqual(["timing", "gsap"]);
|
|
346
|
-
const undoGsap = undoEditHistory(state, { "index.html": hashEditHistoryContent("G") }, 3000);
|
|
347
|
-
expect(undoGsap.ok).toBe(true);
|
|
348
|
-
expect(undoGsap.filesToWrite).toEqual({ "index.html": "F" });
|
|
349
|
-
|
|
350
|
-
const undoTiming = undoEditHistory(
|
|
351
|
-
undoGsap.state,
|
|
352
|
-
{ "index.html": hashEditHistoryContent("F") },
|
|
353
|
-
4000,
|
|
354
|
-
);
|
|
355
|
-
expect(undoTiming).toMatchObject({
|
|
356
|
-
ok: false,
|
|
357
|
-
reason: "content-mismatch",
|
|
358
|
-
filesToWrite: {},
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("does not merge a slow follow-up without the coalesceMs override", () => {
|
|
363
|
-
const timing = buildEditHistoryEntry({
|
|
364
|
-
projectId: "project-1",
|
|
365
|
-
label: "Resize timeline clip",
|
|
366
|
-
kind: "timeline",
|
|
367
|
-
coalesceKey: "timeline-resize:clip",
|
|
368
|
-
files: { "index.html": { before: "orig", after: "timing" } },
|
|
369
|
-
now: 0,
|
|
370
|
-
id: "timing",
|
|
371
|
-
});
|
|
372
|
-
const late = buildEditHistoryEntry({
|
|
373
|
-
projectId: "project-1",
|
|
374
|
-
label: "Resize timeline clip",
|
|
375
|
-
kind: "timeline",
|
|
376
|
-
coalesceKey: "timeline-resize:clip",
|
|
377
|
-
files: { "index.html": { before: "timing", after: "timing+gsap" } },
|
|
378
|
-
now: 2000,
|
|
379
|
-
id: "late",
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const state = pushEditHistoryEntry(
|
|
383
|
-
pushEditHistoryEntry(createEmptyEditHistory(), timing),
|
|
384
|
-
late,
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
expect(state.undo).toHaveLength(2);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("coalesces entries with the same coalesceKey within the window (prop: format)", () => {
|
|
391
|
-
const first = buildEditHistoryEntry({
|
|
392
|
-
projectId: "project-1",
|
|
393
|
-
label: "Edit title color",
|
|
394
|
-
kind: "source",
|
|
395
|
-
coalesceKey: "prop:title.color",
|
|
396
|
-
files: {
|
|
397
|
-
"index.html": { before: "a", after: "b" },
|
|
398
|
-
},
|
|
399
|
-
now: 100,
|
|
400
|
-
id: "entry-1",
|
|
401
|
-
});
|
|
402
|
-
const second = buildEditHistoryEntry({
|
|
403
|
-
projectId: "project-1",
|
|
404
|
-
label: "Edit title color",
|
|
405
|
-
kind: "source",
|
|
406
|
-
coalesceKey: "prop:title.color",
|
|
407
|
-
files: {
|
|
408
|
-
"index.html": { before: "b", after: "c" },
|
|
409
|
-
},
|
|
410
|
-
now: 200,
|
|
411
|
-
id: "entry-2",
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
const state = pushEditHistoryEntry(
|
|
415
|
-
pushEditHistoryEntry(createEmptyEditHistory(), first),
|
|
416
|
-
second,
|
|
417
|
-
{ coalesceMs: 1000 },
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
expect(state.undo).toHaveLength(1);
|
|
421
|
-
expect(state.undo[0].id).toBe("entry-2");
|
|
422
|
-
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
423
|
-
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it("does not coalesce entries with different coalesceKeys (cross-prop separation)", () => {
|
|
427
|
-
const titleEdit = buildEditHistoryEntry({
|
|
428
|
-
projectId: "project-1",
|
|
429
|
-
label: "Edit title color",
|
|
430
|
-
kind: "source",
|
|
431
|
-
coalesceKey: "prop:title.color",
|
|
432
|
-
files: {
|
|
433
|
-
"index.html": { before: "a", after: "b" },
|
|
434
|
-
},
|
|
435
|
-
now: 100,
|
|
436
|
-
id: "entry-title",
|
|
437
|
-
});
|
|
438
|
-
const bodyEdit = buildEditHistoryEntry({
|
|
439
|
-
projectId: "project-1",
|
|
440
|
-
label: "Edit body color",
|
|
441
|
-
kind: "source",
|
|
442
|
-
coalesceKey: "prop:body.color",
|
|
443
|
-
files: {
|
|
444
|
-
"index.html": { before: "b", after: "c" },
|
|
445
|
-
},
|
|
446
|
-
now: 200,
|
|
447
|
-
id: "entry-body",
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const state = pushEditHistoryEntry(
|
|
451
|
-
pushEditHistoryEntry(createEmptyEditHistory(), titleEdit),
|
|
452
|
-
bodyEdit,
|
|
453
|
-
{ coalesceMs: 1000 },
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
expect(state.undo.map((e) => e.id)).toEqual(["entry-title", "entry-body"]);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it("does not coalesce source editor edits outside the coalesce window", () => {
|
|
460
|
-
const first = buildEditHistoryEntry({
|
|
461
|
-
projectId: "project-1",
|
|
462
|
-
label: "Edit source",
|
|
463
|
-
kind: "source",
|
|
464
|
-
coalesceKey: "source:index.html",
|
|
465
|
-
files: {
|
|
466
|
-
"index.html": { before: "a", after: "b" },
|
|
467
|
-
},
|
|
468
|
-
now: 100,
|
|
469
|
-
id: "entry-1",
|
|
470
|
-
});
|
|
471
|
-
const second = buildEditHistoryEntry({
|
|
472
|
-
projectId: "project-1",
|
|
473
|
-
label: "Edit source",
|
|
474
|
-
kind: "source",
|
|
475
|
-
coalesceKey: "source:index.html",
|
|
476
|
-
files: {
|
|
477
|
-
"index.html": { before: "b", after: "c" },
|
|
478
|
-
},
|
|
479
|
-
now: 5000,
|
|
480
|
-
id: "entry-2",
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const state = pushEditHistoryEntry(
|
|
484
|
-
pushEditHistoryEntry(createEmptyEditHistory(), first),
|
|
485
|
-
second,
|
|
486
|
-
{ coalesceMs: 1000 },
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
expect(state.undo.map((entry) => entry.id)).toEqual(["entry-1", "entry-2"]);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("coalesces entries exactly at the coalesce boundary (delta === coalesceMs is inclusive)", () => {
|
|
493
|
-
const first = buildEditHistoryEntry({
|
|
494
|
-
projectId: "project-1",
|
|
495
|
-
label: "Edit source",
|
|
496
|
-
kind: "source",
|
|
497
|
-
coalesceKey: "source:index.html",
|
|
498
|
-
files: {
|
|
499
|
-
"index.html": { before: "a", after: "b" },
|
|
500
|
-
},
|
|
501
|
-
now: 100,
|
|
502
|
-
id: "entry-1",
|
|
503
|
-
});
|
|
504
|
-
const second = buildEditHistoryEntry({
|
|
505
|
-
projectId: "project-1",
|
|
506
|
-
label: "Edit source",
|
|
507
|
-
kind: "source",
|
|
508
|
-
coalesceKey: "source:index.html",
|
|
509
|
-
files: {
|
|
510
|
-
"index.html": { before: "b", after: "c" },
|
|
511
|
-
},
|
|
512
|
-
now: 1100, // exactly coalesceMs=1000ms after first
|
|
513
|
-
id: "entry-2",
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
const state = pushEditHistoryEntry(
|
|
517
|
-
pushEditHistoryEntry(createEmptyEditHistory(), first),
|
|
518
|
-
second,
|
|
519
|
-
{ coalesceMs: 1000 },
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// Boundary is <=: delta of exactly 1000ms coalesces into one entry.
|
|
523
|
-
expect(state.undo).toHaveLength(1);
|
|
524
|
-
expect(state.undo[0].id).toBe("entry-2");
|
|
525
|
-
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
526
|
-
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it.todo("gesture-start/commit collapses intermediate drag steps into one undo entry");
|
|
530
|
-
|
|
531
|
-
it.todo(
|
|
532
|
-
"origin:applyPatches edits are excluded from undo stack to prevent undo loops (requires SDK session)",
|
|
533
|
-
);
|
|
534
|
-
});
|