@hyperframes/studio 0.8.24 → 0.8.25
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-CTQCs_KG.js → hyperframes-player-CTP_00ix.js} +1 -1
- package/dist/assets/{index-okdE01ky.js → index-Bjb4YqS_.js} +1 -1
- package/dist/assets/{index-Bq3M0sjr.js → index-DSk_ALFr.js} +218 -218
- package/dist/assets/{index-9WJFcXTP.js → index-Di-8Jxqc.js} +1 -1
- package/dist/assets/index-DnRfAiK2.css +1 -0
- package/dist/{chunk-6BT6DTB4.js → chunk-A7S5SGNA.js} +2 -2
- package/dist/chunk-A7S5SGNA.js.map +1 -0
- package/dist/{domEditingLayers-URA7BLWE.js → domEditingLayers-JMMVOGTO.js} +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.html +2 -2
- package/dist/index.js +6206 -5011
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/editor/TopologyLens.test.tsx +294 -0
- package/src/components/editor/TopologyLens.tsx +206 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayGeometryBatch.test.ts +18 -0
- package/src/components/editor/domEditingLayers.test.ts +9 -0
- package/src/components/editor/domEditingLayers.ts +8 -8
- package/src/components/editor/topologyLensGeometry.test.ts +44 -0
- package/src/components/editor/topologyLensGeometry.ts +119 -0
- package/src/components/editor/topologyLensState.test.ts +116 -0
- package/src/components/editor/topologyLensState.ts +72 -0
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +126 -0
- package/src/components/nle/PreviewOverlays.tsx +24 -18
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +16 -0
- package/src/components/sidebar/CompositionsTab.tsx +10 -3
- package/src/components/ui/HyperframesLoader.tsx +3 -48
- package/src/components/ui/HyperframesMark.tsx +53 -0
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/hooks/domEditCommitRunner.test.ts +59 -0
- package/src/hooks/domEditCommitRunner.ts +55 -20
- package/src/hooks/domEditCommitTypes.ts +8 -1
- package/src/hooks/domEditTextCommitPlan.ts +47 -0
- package/src/hooks/useDomEditAttributeCommits.ts +4 -1
- package/src/hooks/useDomEditCommits.test.tsx +207 -8
- package/src/hooks/useDomEditCommits.ts +54 -14
- package/src/hooks/useDomEditPositionPatchCommit.test.tsx +3 -4
- package/src/hooks/useDomEditPositionPatchCommit.ts +27 -29
- package/src/hooks/useDomEditSession.ts +6 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +86 -2
- package/src/hooks/useDomEditTextCommits.ts +64 -86
- package/src/hooks/useDomEditWiring.ts +1 -1
- package/src/hooks/useDomSelection.ts +2 -0
- package/src/hooks/useDomSelectionTypes.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +71 -4
- package/src/hooks/useExternalFileChangeCoordinator.ts +18 -9
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +13 -1
- package/src/hooks/useGsapAnimationFetchFallback.ts +7 -3
- package/src/hooks/useGsapAnimationOps.test.tsx +101 -0
- package/src/hooks/useGsapAnimationOps.ts +4 -4
- package/src/hooks/useGsapAwareEditing.test.tsx +30 -2
- package/src/hooks/useGsapAwareEditing.ts +31 -10
- package/src/hooks/useGsapSelectionHandlers.test.tsx +3 -1
- package/src/hooks/useGsapSelectionHandlers.ts +21 -11
- package/src/hooks/useRenderClipContent.test.ts +27 -0
- package/src/hooks/useRenderClipContent.ts +5 -0
- package/src/hooks/useStudioExternalFileChanges.ts +5 -0
- package/src/player/components/CompositionThumbnail.test.ts +66 -1
- package/src/player/components/CompositionThumbnail.tsx +6 -0
- package/src/player/store/playerStore.test.ts +24 -0
- package/src/player/store/thumbnailSlice.ts +6 -0
- package/src/styles/studio.css +179 -0
- package/src/utils/domEditSaveQueue.test.ts +5 -3
- package/src/utils/domEditSaveQueue.ts +6 -1
- package/src/webmcp/StudioAgentTools.test.ts +70 -0
- package/src/webmcp/StudioAgentTools.tsx +89 -21
- package/src/webmcp/handles.test.ts +172 -15
- package/src/webmcp/handles.ts +152 -35
- package/src/webmcp/tools/animationTools.test.ts +241 -60
- package/src/webmcp/tools/animationTools.ts +191 -104
- package/src/webmcp/tools/contentTools.test.ts +103 -41
- package/src/webmcp/tools/contentTools.ts +149 -105
- package/src/webmcp/tools/inspectTools.test.ts +47 -5
- package/src/webmcp/tools/inspectTools.ts +59 -19
- package/src/webmcp/tools/lookTools.test.ts +185 -38
- package/src/webmcp/tools/lookTools.ts +134 -21
- package/src/webmcp/tools/selectionTools.test.ts +105 -9
- package/src/webmcp/tools/selectionTools.ts +34 -19
- package/src/webmcp/tools/targetedWriteTools.test.ts +125 -0
- package/src/webmcp/tools/transformTools.test.ts +140 -54
- package/src/webmcp/tools/transformTools.ts +215 -56
- package/src/webmcp/useStudioAgentTools.test.tsx +310 -10
- package/src/webmcp/useStudioAgentTools.ts +27 -15
- package/src/webmcp/webmcpTestUtils.ts +37 -0
- package/src/webmcp/writeCoordinator.test.ts +375 -0
- package/src/webmcp/writeCoordinator.ts +473 -0
- package/dist/assets/index-yGhfxxoL.css +0 -1
- package/dist/chunk-6BT6DTB4.js.map +0 -1
- /package/dist/{domEditingLayers-URA7BLWE.js.map → domEditingLayers-JMMVOGTO.js.map} +0 -0
|
@@ -10,46 +10,89 @@ import {
|
|
|
10
10
|
type StudioAddKeyframeResult,
|
|
11
11
|
type StudioUpdateAnimationResult,
|
|
12
12
|
} from "./animationTools";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
expectFailure,
|
|
15
|
+
expectOk,
|
|
16
|
+
previewElement,
|
|
17
|
+
selectionFor,
|
|
18
|
+
sourceHandle,
|
|
19
|
+
targetedWriteDeps,
|
|
20
|
+
} from "../webmcpTestUtils";
|
|
21
|
+
|
|
22
|
+
const animationInput = (input: Record<string, unknown>) => ({
|
|
23
|
+
handle: sourceHandle("headline"),
|
|
24
|
+
...input,
|
|
25
|
+
});
|
|
14
26
|
|
|
15
27
|
function animationDeps(overrides: Partial<AnimationToolDeps> = {}): AnimationToolDeps {
|
|
16
28
|
const element = previewElement('<h1 id="headline">Ship it</h1>', "headline");
|
|
29
|
+
const selection = selectionFor(element);
|
|
17
30
|
return {
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
...targetedWriteDeps(selection),
|
|
32
|
+
getAnimationsForSelection: async () => [{ id: "anim-1" }, { id: "anim-gone" }, { id: "a" }],
|
|
20
33
|
readPlayhead: () => ({ currentTime: 2.4, duration: 10, isPlaying: false }),
|
|
21
|
-
addAnimation: () =>
|
|
34
|
+
addAnimation: async () => true,
|
|
22
35
|
updateAnimation: async () => true,
|
|
23
36
|
addKeyframe: async () => undefined,
|
|
24
|
-
deleteAnimation: () =>
|
|
37
|
+
deleteAnimation: async () => true,
|
|
25
38
|
...overrides,
|
|
26
39
|
};
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
describe("studioAddAnimation", () => {
|
|
43
|
+
it("does not report success before persistence and preview sync settle", async () => {
|
|
44
|
+
let release!: (landed: boolean) => void;
|
|
45
|
+
let actorStarted!: () => void;
|
|
46
|
+
const pending = new Promise<boolean>((resolve) => {
|
|
47
|
+
release = resolve;
|
|
48
|
+
});
|
|
49
|
+
const started = new Promise<void>((resolve) => {
|
|
50
|
+
actorStarted = resolve;
|
|
51
|
+
});
|
|
52
|
+
let settled = false;
|
|
53
|
+
|
|
54
|
+
const resultPromise = studioAddAnimation(
|
|
55
|
+
animationDeps({
|
|
56
|
+
addAnimation: () => {
|
|
57
|
+
actorStarted();
|
|
58
|
+
return pending;
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
animationInput({ method: "from" }),
|
|
62
|
+
).then((result) => {
|
|
63
|
+
settled = true;
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await started;
|
|
68
|
+
await Promise.resolve();
|
|
69
|
+
expect(settled).toBe(false);
|
|
70
|
+
|
|
71
|
+
release(true);
|
|
72
|
+
expect((await resultPromise).ok).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
30
75
|
it("reports where the playhead actually was, not a position the caller chose", async () => {
|
|
31
76
|
// The handler reads the playhead itself and ignores any position argument,
|
|
32
77
|
// so echoing one back would report a number that had no effect.
|
|
33
|
-
const addAnimation = vi.fn();
|
|
78
|
+
const addAnimation = vi.fn(async () => true);
|
|
34
79
|
|
|
35
80
|
const result = await studioAddAnimation(
|
|
36
81
|
animationDeps({
|
|
37
82
|
addAnimation,
|
|
38
83
|
readPlayhead: () => ({ currentTime: 7.25, duration: 10, isPlaying: false }),
|
|
39
84
|
}),
|
|
40
|
-
{ method: "from" },
|
|
85
|
+
animationInput({ method: "from" }),
|
|
41
86
|
);
|
|
42
87
|
|
|
43
88
|
const ok = expectOk<StudioAddAnimationResult>(result);
|
|
44
89
|
expect(ok.insertedAtSeconds).toBe(7.25);
|
|
45
90
|
expect(ok.method).toBe("from");
|
|
46
|
-
expect(addAnimation).toHaveBeenCalledWith("from");
|
|
91
|
+
expect(addAnimation).toHaveBeenCalledWith(expect.anything(), "from");
|
|
47
92
|
});
|
|
48
93
|
|
|
49
|
-
it("marks the result as dispatched
|
|
50
|
-
|
|
51
|
-
// is no honest success signal to report.
|
|
52
|
-
const result = await studioAddAnimation(animationDeps(), { method: "to" });
|
|
94
|
+
it("marks the result as dispatched after the underlying write settles", async () => {
|
|
95
|
+
const result = await studioAddAnimation(animationDeps(), animationInput({ method: "to" }));
|
|
53
96
|
|
|
54
97
|
expect(expectOk<StudioAddAnimationResult>(result).dispatched).toBe(true);
|
|
55
98
|
});
|
|
@@ -58,7 +101,10 @@ describe("studioAddAnimation", () => {
|
|
|
58
101
|
const addAnimation = vi.fn();
|
|
59
102
|
|
|
60
103
|
const result = expectFailure(
|
|
61
|
-
await studioAddAnimation(
|
|
104
|
+
await studioAddAnimation(
|
|
105
|
+
animationDeps({ addAnimation }),
|
|
106
|
+
animationInput({ method: "wiggle" }),
|
|
107
|
+
),
|
|
62
108
|
);
|
|
63
109
|
|
|
64
110
|
expect(result.kind).toBe("invalid");
|
|
@@ -71,45 +117,67 @@ describe("studioAddAnimation", () => {
|
|
|
71
117
|
const paused = expectFailure(
|
|
72
118
|
await studioAddAnimation(
|
|
73
119
|
animationDeps({ getWriteBlockedReason: () => "Auto-save is paused", addAnimation }),
|
|
74
|
-
{ method: "to" },
|
|
120
|
+
animationInput({ method: "to" }),
|
|
75
121
|
),
|
|
76
122
|
);
|
|
77
123
|
const unselected = expectFailure(
|
|
78
|
-
await studioAddAnimation(
|
|
79
|
-
|
|
80
|
-
|
|
124
|
+
await studioAddAnimation(
|
|
125
|
+
animationDeps({ buildSelection: async () => null, addAnimation }),
|
|
126
|
+
animationInput({ method: "to" }),
|
|
127
|
+
),
|
|
81
128
|
);
|
|
82
129
|
|
|
83
130
|
expect(paused.kind).toBe("blocked");
|
|
84
|
-
expect(unselected.kind).toBe("
|
|
131
|
+
expect(unselected.kind).toBe("blocked");
|
|
85
132
|
expect(addAnimation).not.toHaveBeenCalled();
|
|
86
133
|
});
|
|
87
134
|
});
|
|
88
135
|
|
|
89
136
|
describe("studioUpdateAnimation", () => {
|
|
90
|
-
it("
|
|
137
|
+
it("dispatches the write after the handler reports acceptance", async () => {
|
|
91
138
|
const updateAnimation = vi.fn(async () => true);
|
|
92
139
|
|
|
93
|
-
const result = await studioUpdateAnimation(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
const result = await studioUpdateAnimation(
|
|
141
|
+
animationDeps({ updateAnimation }),
|
|
142
|
+
animationInput({
|
|
143
|
+
animationId: "anim-1",
|
|
144
|
+
ease: "power2.out",
|
|
145
|
+
duration: 1.5,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
98
148
|
|
|
99
149
|
const ok = expectOk<StudioUpdateAnimationResult>(result);
|
|
150
|
+
expect(ok.changed).toBe(false);
|
|
100
151
|
expect(ok.updated).toEqual({ duration: 1.5, ease: "power2.out" });
|
|
101
|
-
expect(updateAnimation).toHaveBeenCalledWith("anim-1", {
|
|
152
|
+
expect(updateAnimation).toHaveBeenCalledWith(expect.anything(), "anim-1", {
|
|
102
153
|
duration: 1.5,
|
|
103
154
|
ease: "power2.out",
|
|
104
155
|
});
|
|
105
156
|
});
|
|
106
157
|
|
|
158
|
+
it("refuses an animation id owned by another target before calling the actor", async () => {
|
|
159
|
+
const updateAnimation = vi.fn(async () => true);
|
|
160
|
+
const result = await studioUpdateAnimation(
|
|
161
|
+
animationDeps({
|
|
162
|
+
getAnimationsForSelection: async () => [{ id: "other-animation" }],
|
|
163
|
+
updateAnimation,
|
|
164
|
+
}),
|
|
165
|
+
animationInput({ animationId: "anim-1", duration: 2 }),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(result).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
|
|
169
|
+
expect(updateAnimation).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
107
172
|
it("reports a false return as a real failure", async () => {
|
|
108
173
|
const result = expectFailure(
|
|
109
|
-
await studioUpdateAnimation(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
174
|
+
await studioUpdateAnimation(
|
|
175
|
+
animationDeps({ updateAnimation: async () => false }),
|
|
176
|
+
animationInput({
|
|
177
|
+
animationId: "anim-gone",
|
|
178
|
+
ease: "none",
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
113
181
|
);
|
|
114
182
|
|
|
115
183
|
expect(result.kind).toBe("failed");
|
|
@@ -123,24 +191,26 @@ describe("studioUpdateAnimation", () => {
|
|
|
123
191
|
|
|
124
192
|
const result = expectFailure(
|
|
125
193
|
await studioUpdateAnimation(
|
|
126
|
-
animationDeps({
|
|
127
|
-
{ animationId: "anim-1", ease: "none" },
|
|
194
|
+
animationDeps({ buildSelection: async () => null, updateAnimation }),
|
|
195
|
+
animationInput({ animationId: "anim-1", ease: "none" }),
|
|
128
196
|
),
|
|
129
197
|
);
|
|
130
198
|
|
|
131
|
-
expect(result.kind).toBe("
|
|
132
|
-
expect(result.reason).toMatch(/
|
|
199
|
+
expect(result.kind).toBe("blocked");
|
|
200
|
+
expect(result.reason).toMatch(/cannot edit/);
|
|
133
201
|
expect(updateAnimation).not.toHaveBeenCalled();
|
|
134
202
|
});
|
|
135
203
|
|
|
136
204
|
it("requires at least one field, and rejects a negative duration", async () => {
|
|
137
205
|
const deps = animationDeps();
|
|
138
206
|
|
|
139
|
-
expect(expectFailure(await studioUpdateAnimation(deps, { animationId: "a" })).reason).toMatch(
|
|
140
|
-
/at least one/,
|
|
141
|
-
);
|
|
142
207
|
expect(
|
|
143
|
-
expectFailure(await studioUpdateAnimation(deps, { animationId: "a"
|
|
208
|
+
expectFailure(await studioUpdateAnimation(deps, animationInput({ animationId: "a" }))).reason,
|
|
209
|
+
).toMatch(/at least one/);
|
|
210
|
+
expect(
|
|
211
|
+
expectFailure(
|
|
212
|
+
await studioUpdateAnimation(deps, animationInput({ animationId: "a", duration: -1 })),
|
|
213
|
+
).reason,
|
|
144
214
|
).toMatch(/negative/);
|
|
145
215
|
});
|
|
146
216
|
|
|
@@ -148,32 +218,53 @@ describe("studioUpdateAnimation", () => {
|
|
|
148
218
|
const updateAnimation = vi.fn();
|
|
149
219
|
|
|
150
220
|
const result = expectFailure(
|
|
151
|
-
await studioUpdateAnimation(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
221
|
+
await studioUpdateAnimation(
|
|
222
|
+
animationDeps({ updateAnimation }),
|
|
223
|
+
animationInput({
|
|
224
|
+
animationId: " ",
|
|
225
|
+
ease: "none",
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
155
228
|
);
|
|
156
229
|
|
|
157
230
|
expect(result.kind).toBe("invalid");
|
|
158
231
|
expect(updateAnimation).not.toHaveBeenCalled();
|
|
159
232
|
});
|
|
233
|
+
|
|
234
|
+
it("rejects raw JavaScript ease expressions before dispatch", async () => {
|
|
235
|
+
const updateAnimation = vi.fn();
|
|
236
|
+
|
|
237
|
+
const result = await studioUpdateAnimation(
|
|
238
|
+
animationDeps({ updateAnimation }),
|
|
239
|
+
animationInput({ animationId: "anim-1", ease: "__raw:(()=>alert(1))()" }),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(result).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
|
|
243
|
+
expect(updateAnimation).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
160
245
|
});
|
|
161
246
|
|
|
162
247
|
describe("studioAddKeyframe", () => {
|
|
163
248
|
it("passes every property through in one commit", async () => {
|
|
164
249
|
const addKeyframe = vi.fn(async () => undefined);
|
|
165
250
|
|
|
166
|
-
const result = await studioAddKeyframe(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
251
|
+
const result = await studioAddKeyframe(
|
|
252
|
+
animationDeps({ addKeyframe }),
|
|
253
|
+
animationInput({
|
|
254
|
+
animationId: "anim-1",
|
|
255
|
+
percent: 50,
|
|
256
|
+
properties: { y: -50, opacity: 0 },
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
171
259
|
|
|
172
260
|
const ok = expectOk<StudioAddKeyframeResult>(result);
|
|
173
261
|
expect(ok.properties).toEqual({ y: -50, opacity: 0 });
|
|
174
262
|
// One call, so one undo entry, rather than one per property.
|
|
175
263
|
expect(addKeyframe).toHaveBeenCalledTimes(1);
|
|
176
|
-
expect(addKeyframe).toHaveBeenCalledWith("anim-1", 50, {
|
|
264
|
+
expect(addKeyframe).toHaveBeenCalledWith(expect.anything(), "anim-1", 50, {
|
|
265
|
+
y: -50,
|
|
266
|
+
opacity: 0,
|
|
267
|
+
});
|
|
177
268
|
});
|
|
178
269
|
|
|
179
270
|
it("validates percent itself, because the platform does not", async () => {
|
|
@@ -184,7 +275,10 @@ describe("studioAddKeyframe", () => {
|
|
|
184
275
|
|
|
185
276
|
for (const percent of [-1, 101, Number.NaN, "50"]) {
|
|
186
277
|
const result = expectFailure(
|
|
187
|
-
await studioAddKeyframe(
|
|
278
|
+
await studioAddKeyframe(
|
|
279
|
+
deps,
|
|
280
|
+
animationInput({ animationId: "a", percent, properties: { y: 1 } }),
|
|
281
|
+
),
|
|
188
282
|
);
|
|
189
283
|
expect(result.kind).toBe("invalid");
|
|
190
284
|
}
|
|
@@ -197,35 +291,107 @@ describe("studioAddKeyframe", () => {
|
|
|
197
291
|
|
|
198
292
|
for (const properties of [{}, { y: null }, [], "y:1"]) {
|
|
199
293
|
const result = expectFailure(
|
|
200
|
-
await studioAddKeyframe(
|
|
294
|
+
await studioAddKeyframe(
|
|
295
|
+
deps,
|
|
296
|
+
animationInput({ animationId: "a", percent: 50, properties }),
|
|
297
|
+
),
|
|
201
298
|
);
|
|
202
299
|
expect(result.kind).toBe("invalid");
|
|
203
300
|
}
|
|
204
301
|
expect(addKeyframe).not.toHaveBeenCalled();
|
|
205
302
|
});
|
|
206
303
|
|
|
304
|
+
it("rejects raw JavaScript keyframe values before dispatch", async () => {
|
|
305
|
+
const addKeyframe = vi.fn();
|
|
306
|
+
|
|
307
|
+
const result = await studioAddKeyframe(
|
|
308
|
+
animationDeps({ addKeyframe }),
|
|
309
|
+
animationInput({
|
|
310
|
+
animationId: "anim-1",
|
|
311
|
+
percent: 50,
|
|
312
|
+
properties: { x: "__raw:(()=>alert(1))()" },
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(result).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
|
|
317
|
+
expect(addKeyframe).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
207
320
|
it("accepts 0 and 100 as the ends of the tween", async () => {
|
|
208
321
|
for (const percent of [0, 100]) {
|
|
209
|
-
const result = await studioAddKeyframe(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
322
|
+
const result = await studioAddKeyframe(
|
|
323
|
+
animationDeps(),
|
|
324
|
+
animationInput({
|
|
325
|
+
animationId: "a",
|
|
326
|
+
percent,
|
|
327
|
+
properties: { y: 1 },
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
214
330
|
expect(expectOk<StudioAddKeyframeResult>(result).percent).toBe(percent);
|
|
215
331
|
}
|
|
216
332
|
});
|
|
333
|
+
|
|
334
|
+
it("refuses an animation id owned by another target before calling the actor", async () => {
|
|
335
|
+
const addKeyframe = vi.fn(async () => undefined);
|
|
336
|
+
|
|
337
|
+
const result = await studioAddKeyframe(
|
|
338
|
+
animationDeps({
|
|
339
|
+
getAnimationsForSelection: async () => [{ id: "other-animation" }],
|
|
340
|
+
addKeyframe,
|
|
341
|
+
}),
|
|
342
|
+
animationInput({ animationId: "anim-1", percent: 50, properties: { y: 1 } }),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(result).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
|
|
346
|
+
expect(addKeyframe).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
217
348
|
});
|
|
218
349
|
|
|
219
350
|
describe("studioDeleteAnimation", () => {
|
|
220
|
-
it("
|
|
221
|
-
|
|
351
|
+
it("does not report success before persistence and preview sync settle", async () => {
|
|
352
|
+
let release!: (landed: boolean) => void;
|
|
353
|
+
let actorStarted!: () => void;
|
|
354
|
+
const pending = new Promise<boolean>((resolve) => {
|
|
355
|
+
release = resolve;
|
|
356
|
+
});
|
|
357
|
+
const started = new Promise<void>((resolve) => {
|
|
358
|
+
actorStarted = resolve;
|
|
359
|
+
});
|
|
360
|
+
let settled = false;
|
|
222
361
|
|
|
223
|
-
const
|
|
224
|
-
|
|
362
|
+
const resultPromise = studioDeleteAnimation(
|
|
363
|
+
animationDeps({
|
|
364
|
+
deleteAnimation: () => {
|
|
365
|
+
actorStarted();
|
|
366
|
+
return pending;
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
animationInput({ animationId: "anim-1" }),
|
|
370
|
+
).then((result) => {
|
|
371
|
+
settled = true;
|
|
372
|
+
return result;
|
|
225
373
|
});
|
|
226
374
|
|
|
375
|
+
await started;
|
|
376
|
+
await Promise.resolve();
|
|
377
|
+
expect(settled).toBe(false);
|
|
378
|
+
|
|
379
|
+
release(true);
|
|
380
|
+
expect((await resultPromise).ok).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("dispatches the delete and says so", async () => {
|
|
384
|
+
const deleteAnimation = vi.fn(async () => true);
|
|
385
|
+
|
|
386
|
+
const result = await studioDeleteAnimation(
|
|
387
|
+
animationDeps({ deleteAnimation }),
|
|
388
|
+
animationInput({
|
|
389
|
+
animationId: "anim-1",
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
227
393
|
expect(result.ok).toBe(true);
|
|
228
|
-
expect(deleteAnimation).toHaveBeenCalledWith("anim-1");
|
|
394
|
+
expect(deleteAnimation).toHaveBeenCalledWith(expect.anything(), "anim-1");
|
|
229
395
|
});
|
|
230
396
|
|
|
231
397
|
it("refuses while a write is blocked", async () => {
|
|
@@ -234,11 +400,26 @@ describe("studioDeleteAnimation", () => {
|
|
|
234
400
|
const result = expectFailure(
|
|
235
401
|
await studioDeleteAnimation(
|
|
236
402
|
animationDeps({ getWriteBlockedReason: () => "Auto-save is paused", deleteAnimation }),
|
|
237
|
-
{ animationId: "anim-1" },
|
|
403
|
+
animationInput({ animationId: "anim-1" }),
|
|
238
404
|
),
|
|
239
405
|
);
|
|
240
406
|
|
|
241
407
|
expect(result.kind).toBe("blocked");
|
|
242
408
|
expect(deleteAnimation).not.toHaveBeenCalled();
|
|
243
409
|
});
|
|
410
|
+
|
|
411
|
+
it("refuses an animation id owned by another target before calling the actor", async () => {
|
|
412
|
+
const deleteAnimation = vi.fn();
|
|
413
|
+
|
|
414
|
+
const result = await studioDeleteAnimation(
|
|
415
|
+
animationDeps({
|
|
416
|
+
getAnimationsForSelection: async () => [{ id: "other-animation" }],
|
|
417
|
+
deleteAnimation,
|
|
418
|
+
}),
|
|
419
|
+
animationInput({ animationId: "anim-1" }),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(result).toMatchObject({ ok: false, stage: "refused", kind: "invalid" });
|
|
423
|
+
expect(deleteAnimation).not.toHaveBeenCalled();
|
|
424
|
+
});
|
|
244
425
|
});
|