@hyperframes/studio 0.7.71 → 0.7.73
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-C1X90psg.js → hyperframes-player-DGlzhf_s.js} +1 -1
- package/dist/assets/index-B37rWXo4.css +1 -0
- package/dist/assets/{index-DzxDHPR1.js → index-CbVTOJ-E.js} +1 -1
- package/dist/assets/{index-nY4lLBqM.js → index-CcA5WVJv.js} +1 -1
- package/dist/assets/index-DXcLI2wu.js +428 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +2 -2
- package/dist/index.js +7762 -4261
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +10 -4
- package/src/components/editor/AnimationCard.test.tsx +100 -0
- package/src/components/editor/AnimationCard.tsx +5 -2
- package/src/components/editor/EaseCurveSection.test.tsx +348 -0
- package/src/components/editor/EaseCurveSection.tsx +444 -237
- package/src/components/editor/EaseParamFields.test.tsx +188 -0
- package/src/components/editor/EaseParamFields.tsx +246 -0
- package/src/components/editor/PropertyPanel.test.tsx +9 -7
- package/src/components/editor/PropertyPanelFlat.tsx +75 -88
- package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
- package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
- package/src/components/editor/easeCurveSvg.tsx +65 -0
- package/src/components/editor/easePresetLibrary.test.ts +114 -0
- package/src/components/editor/easePresetLibrary.ts +49 -0
- package/src/components/editor/gsapAnimationConstants.ts +15 -13
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
- package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
- package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
- package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
- package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
- package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
- package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
- package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
- package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +239 -41
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +260 -300
- package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
- package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
- package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
- package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
- package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
- package/src/components/editor/propertyPanelTypes.ts +13 -0
- package/src/components/editor/useColorGradingController.test.ts +331 -10
- package/src/components/editor/useColorGradingController.ts +57 -90
- package/src/components/editor/useColorGradingPreviews.ts +350 -0
- package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
- package/src/components/editor/useColorGradingScopes.ts +62 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/contexts/PanelLayoutContext.tsx +3 -6
- package/src/hooks/useBlockCatalog.ts +31 -13
- package/src/hooks/useBlockHandlers.ts +22 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
- package/src/hooks/useGsapScriptCommits.ts +14 -1
- package/src/hooks/usePanelLayout.test.ts +68 -1
- package/src/hooks/usePanelLayout.ts +72 -34
- package/src/icons/SystemIcons.tsx +4 -0
- package/src/player/components/PlayerControls.tsx +68 -229
- package/src/player/components/ShortcutsPanel.tsx +2 -4
- package/src/player/components/SpeedMenu.tsx +1 -2
- package/src/telemetry/events.test.ts +9 -0
- package/src/telemetry/events.ts +5 -0
- package/src/utils/blockInstaller.test.ts +67 -0
- package/src/utils/blockInstaller.ts +162 -109
- package/src/utils/studioUiPreferences.test.ts +5 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/dist/assets/index-CSCh2Vrp.js +0 -428
- package/dist/assets/index-Ceyz8Qt2.css +0 -1
- package/src/player/components/PlayerControls.test.ts +0 -20
- package/src/player/components/useSeekBarDrag.ts +0 -169
|
@@ -13,9 +13,9 @@ function brightPopGrading() {
|
|
|
13
13
|
return next;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
const next = normalizeHfColorGrading({ preset: "
|
|
18
|
-
if (!next) throw new Error("expected
|
|
16
|
+
function cleanStudioGrading() {
|
|
17
|
+
const next = normalizeHfColorGrading({ preset: "clean-studio", intensity: 1 });
|
|
18
|
+
if (!next) throw new Error("expected clean-studio preset to normalize");
|
|
19
19
|
return next;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -56,19 +56,30 @@ function makeElement(overrides: Partial<DomEditSelection> = {}): DomEditSelectio
|
|
|
56
56
|
} as DomEditSelection;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
type ApplyScope = (
|
|
60
|
+
scope: "source-file" | "project",
|
|
61
|
+
value: string | null,
|
|
62
|
+
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
63
|
+
|
|
59
64
|
function HookHost({
|
|
60
65
|
onState,
|
|
61
66
|
onSetAttributeLive,
|
|
62
67
|
element,
|
|
68
|
+
previewIframeRef,
|
|
69
|
+
onApplyScope,
|
|
63
70
|
}: {
|
|
64
71
|
onState: (state: ReturnType<typeof useColorGradingController>) => void;
|
|
65
72
|
onSetAttributeLive: (attr: string, value: string | null) => void;
|
|
66
73
|
element: DomEditSelection;
|
|
74
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
75
|
+
onApplyScope?: ApplyScope;
|
|
67
76
|
}) {
|
|
68
77
|
const state = useColorGradingController({
|
|
69
78
|
projectId: "proj",
|
|
70
79
|
element,
|
|
80
|
+
previewIframeRef,
|
|
71
81
|
onSetAttributeLive,
|
|
82
|
+
onApplyScope,
|
|
72
83
|
});
|
|
73
84
|
onState(state);
|
|
74
85
|
return null;
|
|
@@ -77,6 +88,8 @@ function HookHost({
|
|
|
77
88
|
function renderHook(
|
|
78
89
|
onSetAttributeLive: (attr: string, value: string | null) => void,
|
|
79
90
|
initialElement: DomEditSelection = makeElement(),
|
|
91
|
+
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
|
|
92
|
+
onApplyScope?: ApplyScope,
|
|
80
93
|
) {
|
|
81
94
|
const host = document.createElement("div");
|
|
82
95
|
document.body.append(host);
|
|
@@ -89,6 +102,8 @@ function renderHook(
|
|
|
89
102
|
onState: (s: ReturnType<typeof useColorGradingController>) => (latest = s),
|
|
90
103
|
onSetAttributeLive,
|
|
91
104
|
element,
|
|
105
|
+
previewIframeRef,
|
|
106
|
+
onApplyScope,
|
|
92
107
|
}),
|
|
93
108
|
);
|
|
94
109
|
});
|
|
@@ -107,6 +122,58 @@ function renderHook(
|
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
type PreviewWindow = Window & {
|
|
126
|
+
__hf?: {
|
|
127
|
+
colorGrading?: {
|
|
128
|
+
renderPreviews?: ReturnType<typeof vi.fn>;
|
|
129
|
+
startPreviewPlayback?: ReturnType<typeof vi.fn>;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
__player?: { play: ReturnType<typeof vi.fn> };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function createPreviewFrame() {
|
|
136
|
+
const iframe = document.body.appendChild(document.createElement("iframe"));
|
|
137
|
+
const contentWindow = iframe.contentWindow as PreviewWindow | null;
|
|
138
|
+
if (!contentWindow) throw new Error("expected iframe contentWindow");
|
|
139
|
+
return { contentWindow, iframe };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function installPreviewRenderer(contentWindow: PreviewWindow) {
|
|
143
|
+
const renderPreviews = vi
|
|
144
|
+
.fn()
|
|
145
|
+
.mockImplementation(async (_target: unknown, candidates: Array<{ id: string }>) => ({
|
|
146
|
+
width: 160,
|
|
147
|
+
height: 90,
|
|
148
|
+
images: candidates.map(({ id }) => ({
|
|
149
|
+
id,
|
|
150
|
+
dataUrl: `data:image/png;base64,${id}`,
|
|
151
|
+
})),
|
|
152
|
+
}));
|
|
153
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
154
|
+
return renderPreviews;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function flushPreviewRequest() {
|
|
158
|
+
act(() => vi.advanceTimersByTime(0));
|
|
159
|
+
await act(async () => {
|
|
160
|
+
await Promise.resolve();
|
|
161
|
+
await Promise.resolve();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function colorGradingMessages(calls: ReadonlyArray<ReadonlyArray<unknown>>) {
|
|
166
|
+
return calls
|
|
167
|
+
.map(([message]) => message)
|
|
168
|
+
.filter(
|
|
169
|
+
(message): message is { action: string; grading: unknown } =>
|
|
170
|
+
typeof message === "object" &&
|
|
171
|
+
message !== null &&
|
|
172
|
+
"action" in message &&
|
|
173
|
+
message.action === "set-color-grading",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
110
177
|
describe("useColorGradingController", () => {
|
|
111
178
|
it("starts with the neutral (inactive) grading and idle compare state", () => {
|
|
112
179
|
const { root, getState } = renderHook(vi.fn());
|
|
@@ -115,6 +182,168 @@ describe("useColorGradingController", () => {
|
|
|
115
182
|
act(() => root.unmount());
|
|
116
183
|
});
|
|
117
184
|
|
|
185
|
+
it("requests one exact preset batch from the selected media runtime", async () => {
|
|
186
|
+
vi.useFakeTimers();
|
|
187
|
+
const devicePixelRatio = vi.spyOn(window, "devicePixelRatio", "get").mockReturnValue(2);
|
|
188
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
189
|
+
const renderPreviews = installPreviewRenderer(contentWindow);
|
|
190
|
+
const initialElement = makeElement({
|
|
191
|
+
dataAttributes: {
|
|
192
|
+
"color-grading": JSON.stringify({
|
|
193
|
+
wheels: { shadows: { hue: 210, amount: 0.2 } },
|
|
194
|
+
effects: { pixelate: 0.4 },
|
|
195
|
+
palette: ["#112233", "#ffffff"],
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const { root, getState } = renderHook(vi.fn(), initialElement, { current: iframe });
|
|
200
|
+
|
|
201
|
+
act(() => getState().requestPresetPreviews());
|
|
202
|
+
await flushPreviewRequest();
|
|
203
|
+
|
|
204
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect(renderPreviews.mock.calls[0]?.[1]).toHaveLength(18);
|
|
206
|
+
expect(renderPreviews.mock.calls[0]?.[1]).toContainEqual({
|
|
207
|
+
id: "bright-pop",
|
|
208
|
+
grading: expect.objectContaining({
|
|
209
|
+
wheels: expect.objectContaining({
|
|
210
|
+
shadows: expect.objectContaining({ amount: 0 }),
|
|
211
|
+
}),
|
|
212
|
+
effects: expect.objectContaining({ pixelate: 0.4 }),
|
|
213
|
+
palette: ["#112233", "#ffffff"],
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
expect(renderPreviews.mock.calls[0]?.[2]).toEqual({ maxDimension: 320 });
|
|
217
|
+
expect(getState().presetPreviews).toEqual({
|
|
218
|
+
status: "ready",
|
|
219
|
+
images: expect.objectContaining({ "bright-pop": "data:image/png;base64,bright-pop" }),
|
|
220
|
+
width: 160,
|
|
221
|
+
height: 90,
|
|
222
|
+
});
|
|
223
|
+
act(() => root.unmount());
|
|
224
|
+
devicePixelRatio.mockRestore();
|
|
225
|
+
vi.useRealTimers();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("retains partial preset previews and exposes retry after the batch times out", async () => {
|
|
229
|
+
vi.useFakeTimers();
|
|
230
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
231
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
232
|
+
width: 160,
|
|
233
|
+
height: 90,
|
|
234
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,bright" }],
|
|
235
|
+
});
|
|
236
|
+
contentWindow.__hf = { colorGrading: { renderPreviews } };
|
|
237
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
238
|
+
|
|
239
|
+
act(() => getState().requestPresetPreviews());
|
|
240
|
+
await flushPreviewRequest();
|
|
241
|
+
expect(getState().presetPreviews).toMatchObject({
|
|
242
|
+
status: "loading",
|
|
243
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await act(async () => {
|
|
247
|
+
await vi.advanceTimersByTimeAsync(1700);
|
|
248
|
+
});
|
|
249
|
+
expect(getState().presetPreviews).toMatchObject({
|
|
250
|
+
status: "unavailable",
|
|
251
|
+
images: { "bright-pop": "data:image/png;base64,bright" },
|
|
252
|
+
});
|
|
253
|
+
expect(renderPreviews.mock.calls.length).toBeGreaterThan(1);
|
|
254
|
+
act(() => root.unmount());
|
|
255
|
+
vi.useRealTimers();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("persists a disabled secondary even though it does not render pixels", () => {
|
|
259
|
+
vi.useFakeTimers();
|
|
260
|
+
const onSetAttributeLive = vi.fn();
|
|
261
|
+
const grading = normalizeHfColorGrading({
|
|
262
|
+
secondaries: [
|
|
263
|
+
{
|
|
264
|
+
enabled: false,
|
|
265
|
+
key: { hue: { center: 215, range: 25 } },
|
|
266
|
+
correction: { saturation: 0.15 },
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
if (!grading) throw new Error("expected secondary grading");
|
|
271
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
272
|
+
|
|
273
|
+
act(() => getState().commitColorGrading(grading));
|
|
274
|
+
act(() => vi.advanceTimersByTime(400));
|
|
275
|
+
|
|
276
|
+
expect(onSetAttributeLive.mock.calls[0]?.[0]).toBe("color-grading");
|
|
277
|
+
expect(onSetAttributeLive.mock.calls[0]?.[1]).toContain('"enabled":false');
|
|
278
|
+
act(() => root.unmount());
|
|
279
|
+
vi.useRealTimers();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("copies a disabled authored secondary to a broader scope", async () => {
|
|
283
|
+
const onApplyScope = vi.fn<ApplyScope>().mockResolvedValue({
|
|
284
|
+
changedFiles: 1,
|
|
285
|
+
changedElements: 1,
|
|
286
|
+
});
|
|
287
|
+
const grading = {
|
|
288
|
+
secondaries: [
|
|
289
|
+
{
|
|
290
|
+
enabled: false,
|
|
291
|
+
key: { hue: { center: 215, range: 25 } },
|
|
292
|
+
correction: { saturation: 0.15 },
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
const { root, getState } = renderHook(
|
|
297
|
+
vi.fn(),
|
|
298
|
+
makeElement({ dataAttributes: { "color-grading": JSON.stringify(grading) } }),
|
|
299
|
+
undefined,
|
|
300
|
+
onApplyScope,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
await act(async () => getState().applyToScope());
|
|
304
|
+
|
|
305
|
+
expect(onApplyScope).toHaveBeenCalledWith(
|
|
306
|
+
"source-file",
|
|
307
|
+
expect.stringContaining('"enabled":false'),
|
|
308
|
+
);
|
|
309
|
+
act(() => root.unmount());
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("requests exact effect families and retains earlier family images", async () => {
|
|
313
|
+
vi.useFakeTimers();
|
|
314
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
315
|
+
const renderPreviews = installPreviewRenderer(contentWindow);
|
|
316
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
317
|
+
|
|
318
|
+
act(() => getState().requestEffectPreviews(["blur", "pixelate", "bloom"]));
|
|
319
|
+
await flushPreviewRequest();
|
|
320
|
+
|
|
321
|
+
expect(renderPreviews).toHaveBeenCalledTimes(1);
|
|
322
|
+
expect(renderPreviews.mock.calls[0]?.[1].map(({ id }: { id: string }) => id)).toEqual([
|
|
323
|
+
"blur",
|
|
324
|
+
"pixelate",
|
|
325
|
+
"bloom",
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
act(() => getState().requestEffectPreviews(["kuwahara"]));
|
|
329
|
+
await flushPreviewRequest();
|
|
330
|
+
|
|
331
|
+
expect(renderPreviews).toHaveBeenCalledTimes(2);
|
|
332
|
+
expect(getState().effectPreviews).toEqual({
|
|
333
|
+
status: "ready",
|
|
334
|
+
images: {
|
|
335
|
+
blur: "data:image/png;base64,blur",
|
|
336
|
+
pixelate: "data:image/png;base64,pixelate",
|
|
337
|
+
bloom: "data:image/png;base64,bloom",
|
|
338
|
+
kuwahara: "data:image/png;base64,kuwahara",
|
|
339
|
+
},
|
|
340
|
+
width: 160,
|
|
341
|
+
height: 90,
|
|
342
|
+
});
|
|
343
|
+
act(() => root.unmount());
|
|
344
|
+
vi.useRealTimers();
|
|
345
|
+
});
|
|
346
|
+
|
|
118
347
|
it("commitColorGrading updates grading state synchronously and schedules a debounced persist", async () => {
|
|
119
348
|
vi.useFakeTimers();
|
|
120
349
|
const onSetAttributeLive = vi.fn();
|
|
@@ -135,6 +364,86 @@ describe("useColorGradingController", () => {
|
|
|
135
364
|
vi.useRealTimers();
|
|
136
365
|
});
|
|
137
366
|
|
|
367
|
+
it("previews through the runtime only and restores the committed grade without persisting", () => {
|
|
368
|
+
vi.useFakeTimers();
|
|
369
|
+
const onSetAttributeLive = vi.fn();
|
|
370
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
371
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
372
|
+
const { root, getState } = renderHook(onSetAttributeLive, makeElement(), {
|
|
373
|
+
current: iframe,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
act(() => getState().previewColorGrading(brightPopGrading()));
|
|
377
|
+
act(() => getState().previewColorGrading(null));
|
|
378
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
379
|
+
expect(gradingMessages).toHaveLength(2);
|
|
380
|
+
expect(gradingMessages[0]?.grading).toMatchObject({ preset: "bright-pop" });
|
|
381
|
+
expect(gradingMessages[1]?.grading).toBeNull();
|
|
382
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
383
|
+
act(() => vi.advanceTimersByTime(500));
|
|
384
|
+
expect(onSetAttributeLive).not.toHaveBeenCalled();
|
|
385
|
+
|
|
386
|
+
act(() => root.unmount());
|
|
387
|
+
vi.useRealTimers();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("animates only the selected video card without starting the project player", async () => {
|
|
391
|
+
vi.useFakeTimers();
|
|
392
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
393
|
+
const stopPlayback = vi.fn();
|
|
394
|
+
const renderPreviews = vi.fn().mockResolvedValue({
|
|
395
|
+
width: 320,
|
|
396
|
+
height: 180,
|
|
397
|
+
images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,animated" }],
|
|
398
|
+
});
|
|
399
|
+
const startPreviewPlayback = vi.fn(() => stopPlayback);
|
|
400
|
+
contentWindow.__hf = { colorGrading: { renderPreviews, startPreviewPlayback } };
|
|
401
|
+
contentWindow.__player = { play: vi.fn() };
|
|
402
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
403
|
+
|
|
404
|
+
act(() =>
|
|
405
|
+
getState().previewColorGrading(brightPopGrading(), {
|
|
406
|
+
animatedPreview: { kind: "presets", id: "bright-pop" },
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
await act(async () => {
|
|
410
|
+
await Promise.resolve();
|
|
411
|
+
await Promise.resolve();
|
|
412
|
+
});
|
|
413
|
+
expect(startPreviewPlayback).toHaveBeenCalledTimes(1);
|
|
414
|
+
expect(renderPreviews).toHaveBeenCalledWith(
|
|
415
|
+
expect.anything(),
|
|
416
|
+
[{ id: "bright-pop", grading: expect.objectContaining({ preset: "bright-pop" }) }],
|
|
417
|
+
{ maxDimension: 160, useMediaTime: true },
|
|
418
|
+
);
|
|
419
|
+
expect(contentWindow.__player.play).not.toHaveBeenCalled();
|
|
420
|
+
expect(getState().presetPreviews.images["bright-pop"]).toBe("data:image/png;base64,animated");
|
|
421
|
+
|
|
422
|
+
act(() => getState().previewColorGrading(null));
|
|
423
|
+
expect(stopPlayback).toHaveBeenCalledTimes(1);
|
|
424
|
+
|
|
425
|
+
act(() => root.unmount());
|
|
426
|
+
vi.useRealTimers();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("restores a just-committed look even before React renders the new state", () => {
|
|
430
|
+
vi.useFakeTimers();
|
|
431
|
+
const { contentWindow, iframe } = createPreviewFrame();
|
|
432
|
+
const postMessage = vi.spyOn(contentWindow, "postMessage");
|
|
433
|
+
const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
|
|
434
|
+
const brightPop = brightPopGrading();
|
|
435
|
+
|
|
436
|
+
act(() => {
|
|
437
|
+
getState().commitColorGrading(brightPop);
|
|
438
|
+
getState().previewColorGrading(null);
|
|
439
|
+
});
|
|
440
|
+
const gradingMessages = colorGradingMessages(postMessage.mock.calls);
|
|
441
|
+
expect(gradingMessages.at(-1)?.grading).toMatchObject({ preset: "bright-pop" });
|
|
442
|
+
|
|
443
|
+
act(() => root.unmount());
|
|
444
|
+
vi.useRealTimers();
|
|
445
|
+
});
|
|
446
|
+
|
|
138
447
|
it("reverts to the last confirmed-good grading via the real onSettled(false) signal (matches runDomEditCommit, which never rejects)", async () => {
|
|
139
448
|
// The actual Studio commit runner (runDomEditCommit) catches persist
|
|
140
449
|
// failures internally and always resolves — it reports outcome only
|
|
@@ -253,7 +562,7 @@ describe("useColorGradingController", () => {
|
|
|
253
562
|
});
|
|
254
563
|
},
|
|
255
564
|
)
|
|
256
|
-
// Edit B (
|
|
565
|
+
// Edit B (clean-studio): settles immediately and successfully.
|
|
257
566
|
.mockImplementationOnce(
|
|
258
567
|
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
259
568
|
onSettled?.(true);
|
|
@@ -272,13 +581,13 @@ describe("useColorGradingController", () => {
|
|
|
272
581
|
|
|
273
582
|
// B commits on the SAME element before A's persist has settled.
|
|
274
583
|
act(() => {
|
|
275
|
-
getState().commitColorGrading(
|
|
584
|
+
getState().commitColorGrading(cleanStudioGrading());
|
|
276
585
|
});
|
|
277
586
|
act(() => {
|
|
278
587
|
vi.advanceTimersByTime(400);
|
|
279
588
|
});
|
|
280
589
|
expect(onSetAttributeLive).toHaveBeenCalledTimes(2); // B's persist has already settled (mock resolves sync)
|
|
281
|
-
expect(getState().grading.preset).toBe("
|
|
590
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
282
591
|
|
|
283
592
|
// NOW A's stale persist finally settles as a FAILURE — must not revert
|
|
284
593
|
// `grading` (which now correctly shows B's newer edit) back to the
|
|
@@ -292,20 +601,32 @@ describe("useColorGradingController", () => {
|
|
|
292
601
|
await Promise.resolve();
|
|
293
602
|
await Promise.resolve();
|
|
294
603
|
});
|
|
295
|
-
expect(getState().grading.preset).toBe("
|
|
604
|
+
expect(getState().grading.preset).toBe("clean-studio");
|
|
296
605
|
act(() => root.unmount());
|
|
297
606
|
vi.useRealTimers();
|
|
298
607
|
});
|
|
299
608
|
|
|
300
|
-
it("resetGrading
|
|
609
|
+
it("resetGrading resets Grade fields without clearing LUT, Effects, or Palette", () => {
|
|
301
610
|
const { root, getState } = renderHook(vi.fn());
|
|
611
|
+
const grading = normalizeHfColorGrading({
|
|
612
|
+
preset: "bright-pop",
|
|
613
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
|
|
614
|
+
effects: { pixelate: 0.5 },
|
|
615
|
+
palette: ["#112233", "#ffffff"],
|
|
616
|
+
});
|
|
617
|
+
if (!grading) throw new Error("expected grading to normalize");
|
|
302
618
|
act(() => {
|
|
303
|
-
getState().commitColorGrading(
|
|
619
|
+
getState().commitColorGrading(grading);
|
|
304
620
|
});
|
|
305
621
|
act(() => {
|
|
306
622
|
getState().resetGrading();
|
|
307
623
|
});
|
|
308
|
-
expect(getState().grading
|
|
624
|
+
expect(getState().grading).toMatchObject({
|
|
625
|
+
preset: "neutral",
|
|
626
|
+
lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
|
|
627
|
+
effects: { pixelate: 0.5 },
|
|
628
|
+
palette: ["#112233", "#ffffff"],
|
|
629
|
+
});
|
|
309
630
|
act(() => root.unmount());
|
|
310
631
|
});
|
|
311
632
|
|