@hyperframes/studio 0.7.58 → 0.7.60
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-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
|
|
7
|
+
import { useColorGradingController } from "./useColorGradingController";
|
|
8
|
+
import type { DomEditSelection } from "./domEditing";
|
|
9
|
+
|
|
10
|
+
function freshPopGrading() {
|
|
11
|
+
const next = normalizeHfColorGrading({ preset: "fresh-pop", intensity: 1 });
|
|
12
|
+
if (!next) throw new Error("expected fresh-pop preset to normalize");
|
|
13
|
+
return next;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function naturalLiftGrading() {
|
|
17
|
+
const next = normalizeHfColorGrading({ preset: "natural-lift", intensity: 1 });
|
|
18
|
+
if (!next) throw new Error("expected natural-lift preset to normalize");
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
document.body.innerHTML = "";
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeElement(overrides: Partial<DomEditSelection> = {}): DomEditSelection {
|
|
29
|
+
return {
|
|
30
|
+
element: document.createElement("video"),
|
|
31
|
+
id: "s1-bg",
|
|
32
|
+
selector: "#s1-bg",
|
|
33
|
+
label: "S1 Background",
|
|
34
|
+
tagName: "video",
|
|
35
|
+
sourceFile: "index.html",
|
|
36
|
+
compositionPath: "index.html",
|
|
37
|
+
isCompositionHost: false,
|
|
38
|
+
isInsideLockedComposition: false,
|
|
39
|
+
boundingBox: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
40
|
+
textContent: "",
|
|
41
|
+
dataAttributes: {},
|
|
42
|
+
inlineStyles: {},
|
|
43
|
+
computedStyles: {},
|
|
44
|
+
textFields: [],
|
|
45
|
+
capabilities: {
|
|
46
|
+
canSelect: true,
|
|
47
|
+
canEditStyles: true,
|
|
48
|
+
canCrop: true,
|
|
49
|
+
canMove: true,
|
|
50
|
+
canResize: true,
|
|
51
|
+
canApplyManualOffset: true,
|
|
52
|
+
canApplyManualSize: true,
|
|
53
|
+
canApplyManualRotation: true,
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
} as DomEditSelection;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function HookHost({
|
|
60
|
+
onState,
|
|
61
|
+
onSetAttributeLive,
|
|
62
|
+
element,
|
|
63
|
+
}: {
|
|
64
|
+
onState: (state: ReturnType<typeof useColorGradingController>) => void;
|
|
65
|
+
onSetAttributeLive: (attr: string, value: string | null) => void;
|
|
66
|
+
element: DomEditSelection;
|
|
67
|
+
}) {
|
|
68
|
+
const state = useColorGradingController({
|
|
69
|
+
projectId: "proj",
|
|
70
|
+
element,
|
|
71
|
+
onSetAttributeLive,
|
|
72
|
+
});
|
|
73
|
+
onState(state);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderHook(
|
|
78
|
+
onSetAttributeLive: (attr: string, value: string | null) => void,
|
|
79
|
+
initialElement: DomEditSelection = makeElement(),
|
|
80
|
+
) {
|
|
81
|
+
const host = document.createElement("div");
|
|
82
|
+
document.body.append(host);
|
|
83
|
+
const root = createRoot(host);
|
|
84
|
+
let latest: ReturnType<typeof useColorGradingController> | undefined;
|
|
85
|
+
const renderWith = (element: DomEditSelection) => {
|
|
86
|
+
act(() => {
|
|
87
|
+
root.render(
|
|
88
|
+
React.createElement(HookHost, {
|
|
89
|
+
onState: (s: ReturnType<typeof useColorGradingController>) => (latest = s),
|
|
90
|
+
onSetAttributeLive,
|
|
91
|
+
element,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
renderWith(initialElement);
|
|
97
|
+
return {
|
|
98
|
+
root,
|
|
99
|
+
rerenderWithElement: renderWith,
|
|
100
|
+
// A method, not a getter — `const { state } = renderHook(...)` would
|
|
101
|
+
// destructure a getter into a one-time snapshot, silently going stale
|
|
102
|
+
// after the first state change. Call `.getState()` fresh every time.
|
|
103
|
+
getState(): ReturnType<typeof useColorGradingController> {
|
|
104
|
+
if (!latest) throw new Error("hook did not render");
|
|
105
|
+
return latest;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("useColorGradingController", () => {
|
|
111
|
+
it("starts with the neutral (inactive) grading and idle compare state", () => {
|
|
112
|
+
const { root, getState } = renderHook(vi.fn());
|
|
113
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
114
|
+
expect(getState().compareEnabled).toBe(false);
|
|
115
|
+
act(() => root.unmount());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("commitColorGrading updates grading state synchronously and schedules a debounced persist", async () => {
|
|
119
|
+
vi.useFakeTimers();
|
|
120
|
+
const onSetAttributeLive = vi.fn();
|
|
121
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
122
|
+
act(() => {
|
|
123
|
+
getState().commitColorGrading(freshPopGrading());
|
|
124
|
+
});
|
|
125
|
+
expect(getState().grading.preset).toBe("fresh-pop");
|
|
126
|
+
expect(onSetAttributeLive).not.toHaveBeenCalled();
|
|
127
|
+
act(() => {
|
|
128
|
+
vi.advanceTimersByTime(400);
|
|
129
|
+
});
|
|
130
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
131
|
+
const [attr, value] = onSetAttributeLive.mock.calls[0] as [string, string];
|
|
132
|
+
expect(attr).toBe("color-grading");
|
|
133
|
+
expect(value).toContain("fresh-pop");
|
|
134
|
+
act(() => root.unmount());
|
|
135
|
+
vi.useRealTimers();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("reverts to the last confirmed-good grading via the real onSettled(false) signal (matches runDomEditCommit, which never rejects)", async () => {
|
|
139
|
+
// The actual Studio commit runner (runDomEditCommit) catches persist
|
|
140
|
+
// failures internally and always resolves — it reports outcome only
|
|
141
|
+
// through the onSettled callback passed as the 3rd argument. A mock
|
|
142
|
+
// that only rejects would validate a path the real callback never takes.
|
|
143
|
+
vi.useFakeTimers();
|
|
144
|
+
const onSetAttributeLive = vi.fn(
|
|
145
|
+
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
146
|
+
onSettled?.(false);
|
|
147
|
+
return Promise.resolve();
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
151
|
+
act(() => {
|
|
152
|
+
getState().commitColorGrading(freshPopGrading());
|
|
153
|
+
});
|
|
154
|
+
expect(getState().grading.preset).toBe("fresh-pop");
|
|
155
|
+
act(() => {
|
|
156
|
+
vi.advanceTimersByTime(400);
|
|
157
|
+
});
|
|
158
|
+
await act(async () => {
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
await Promise.resolve();
|
|
161
|
+
});
|
|
162
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
163
|
+
expect(getState().runtimeStatus.state).toBe("unavailable");
|
|
164
|
+
act(() => root.unmount());
|
|
165
|
+
vi.useRealTimers();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("reverts to the last confirmed-good grading when a persist rejects (fallback for a non-onSettled implementation)", async () => {
|
|
169
|
+
vi.useFakeTimers();
|
|
170
|
+
const onSetAttributeLive = vi.fn().mockRejectedValue(new Error("disk full"));
|
|
171
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
172
|
+
act(() => {
|
|
173
|
+
getState().commitColorGrading(freshPopGrading());
|
|
174
|
+
});
|
|
175
|
+
expect(getState().grading.preset).toBe("fresh-pop");
|
|
176
|
+
act(() => {
|
|
177
|
+
vi.advanceTimersByTime(400);
|
|
178
|
+
});
|
|
179
|
+
// The rejection settles on a microtask, not a timer — flush it.
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await Promise.resolve();
|
|
182
|
+
await Promise.resolve();
|
|
183
|
+
});
|
|
184
|
+
// Reverted to "neutral" (the last confirmed-good value, from before this
|
|
185
|
+
// commit) instead of permanently showing "fresh-pop" as if it had saved.
|
|
186
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
187
|
+
expect(getState().runtimeStatus.state).toBe("unavailable");
|
|
188
|
+
act(() => root.unmount());
|
|
189
|
+
vi.useRealTimers();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("a stale in-flight persist result does not touch state after selection has moved on to a THIRD element", async () => {
|
|
193
|
+
vi.useFakeTimers();
|
|
194
|
+
let resolveA: (() => void) | undefined;
|
|
195
|
+
let capturedOnSettledA: ((ok: boolean) => void) | undefined;
|
|
196
|
+
const onSetAttributeLive = vi.fn(
|
|
197
|
+
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
198
|
+
capturedOnSettledA = onSettled;
|
|
199
|
+
return new Promise<void>((resolve) => {
|
|
200
|
+
resolveA = resolve;
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
const { root, getState, rerenderWithElement } = renderHook(
|
|
205
|
+
onSetAttributeLive,
|
|
206
|
+
makeElement({ id: "s1-bg" }),
|
|
207
|
+
);
|
|
208
|
+
act(() => {
|
|
209
|
+
getState().commitColorGrading(freshPopGrading());
|
|
210
|
+
});
|
|
211
|
+
// Let the debounce fire while still on s1-bg — the persist call is now
|
|
212
|
+
// genuinely in flight (its promise won't settle until resolveA() below).
|
|
213
|
+
act(() => {
|
|
214
|
+
vi.advanceTimersByTime(400);
|
|
215
|
+
});
|
|
216
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
217
|
+
|
|
218
|
+
// Selection moves twice more while s1-bg's persist is still pending.
|
|
219
|
+
rerenderWithElement(makeElement({ id: "s2-bg" }));
|
|
220
|
+
rerenderWithElement(makeElement({ id: "s3-bg" }));
|
|
221
|
+
expect(getState().grading.preset).toBe("neutral"); // s3-bg's own fresh state
|
|
222
|
+
|
|
223
|
+
// NOW the stale s1-bg persist finally settles as a failure.
|
|
224
|
+
act(() => {
|
|
225
|
+
capturedOnSettledA?.(false);
|
|
226
|
+
resolveA?.();
|
|
227
|
+
});
|
|
228
|
+
await act(async () => {
|
|
229
|
+
await Promise.resolve();
|
|
230
|
+
await Promise.resolve();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// s3-bg's state must be untouched by a result that belongs to s1-bg.
|
|
234
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
235
|
+
expect(getState().runtimeStatus.state).not.toBe("unavailable");
|
|
236
|
+
act(() => root.unmount());
|
|
237
|
+
vi.useRealTimers();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("a stale in-flight persist for edit A does not clobber edit B's state — SAME element, no selection change", async () => {
|
|
241
|
+
vi.useFakeTimers();
|
|
242
|
+
let resolveA: (() => void) | undefined;
|
|
243
|
+
let capturedOnSettledA: ((ok: boolean) => void) | undefined;
|
|
244
|
+
const onSetAttributeLive = vi
|
|
245
|
+
.fn()
|
|
246
|
+
// Edit A (fresh-pop): captures its onSettled and never resolves until
|
|
247
|
+
// resolveA() is called below — simulates a slow persist.
|
|
248
|
+
.mockImplementationOnce(
|
|
249
|
+
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
250
|
+
capturedOnSettledA = onSettled;
|
|
251
|
+
return new Promise<void>((resolve) => {
|
|
252
|
+
resolveA = resolve;
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
// Edit B (natural-lift): settles immediately and successfully.
|
|
257
|
+
.mockImplementationOnce(
|
|
258
|
+
(_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
|
|
259
|
+
onSettled?.(true);
|
|
260
|
+
return Promise.resolve();
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
const { root, getState } = renderHook(onSetAttributeLive);
|
|
264
|
+
|
|
265
|
+
act(() => {
|
|
266
|
+
getState().commitColorGrading(freshPopGrading());
|
|
267
|
+
});
|
|
268
|
+
act(() => {
|
|
269
|
+
vi.advanceTimersByTime(400);
|
|
270
|
+
});
|
|
271
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(1); // A's persist now in flight
|
|
272
|
+
|
|
273
|
+
// B commits on the SAME element before A's persist has settled.
|
|
274
|
+
act(() => {
|
|
275
|
+
getState().commitColorGrading(naturalLiftGrading());
|
|
276
|
+
});
|
|
277
|
+
act(() => {
|
|
278
|
+
vi.advanceTimersByTime(400);
|
|
279
|
+
});
|
|
280
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(2); // B's persist has already settled (mock resolves sync)
|
|
281
|
+
expect(getState().grading.preset).toBe("natural-lift");
|
|
282
|
+
|
|
283
|
+
// NOW A's stale persist finally settles as a FAILURE — must not revert
|
|
284
|
+
// `grading` (which now correctly shows B's newer edit) back to the
|
|
285
|
+
// pre-A baseline ("neutral"), and must not stamp confirmedGradingRef
|
|
286
|
+
// with A's now-superseded attempt on success either.
|
|
287
|
+
act(() => {
|
|
288
|
+
capturedOnSettledA?.(false);
|
|
289
|
+
resolveA?.();
|
|
290
|
+
});
|
|
291
|
+
await act(async () => {
|
|
292
|
+
await Promise.resolve();
|
|
293
|
+
await Promise.resolve();
|
|
294
|
+
});
|
|
295
|
+
expect(getState().grading.preset).toBe("natural-lift");
|
|
296
|
+
act(() => root.unmount());
|
|
297
|
+
vi.useRealTimers();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("resetGrading returns to the neutral preset", () => {
|
|
301
|
+
const { root, getState } = renderHook(vi.fn());
|
|
302
|
+
act(() => {
|
|
303
|
+
getState().commitColorGrading(freshPopGrading());
|
|
304
|
+
});
|
|
305
|
+
act(() => {
|
|
306
|
+
getState().resetGrading();
|
|
307
|
+
});
|
|
308
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
309
|
+
act(() => root.unmount());
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("resets grading/compare state when selection changes to a different element", () => {
|
|
313
|
+
const { root, getState, rerenderWithElement } = renderHook(
|
|
314
|
+
vi.fn(),
|
|
315
|
+
makeElement({ id: "s1-bg" }),
|
|
316
|
+
);
|
|
317
|
+
act(() => {
|
|
318
|
+
getState().commitColorGrading(freshPopGrading());
|
|
319
|
+
});
|
|
320
|
+
expect(getState().grading.preset).toBe("fresh-pop");
|
|
321
|
+
// A different element, with no persisted grading of its own — without a
|
|
322
|
+
// reset, this hook (unlike the legacy component it was extracted from,
|
|
323
|
+
// which remounts via a `key={selectionIdentityKey}`) would keep showing
|
|
324
|
+
// the previous element's grading.
|
|
325
|
+
rerenderWithElement(makeElement({ id: "s2-bg" }));
|
|
326
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
327
|
+
act(() => root.unmount());
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("also resets when the same local id/selector recurs in a different source file", () => {
|
|
331
|
+
// Same id, same selector, same selectorIndex — only sourceFile differs.
|
|
332
|
+
// Without sourceFile in the identity key, this would collide with the
|
|
333
|
+
// first element (e.g. host composition vs. an inlined sub-composition,
|
|
334
|
+
// or two unrelated sub-comps that happen to share a local id).
|
|
335
|
+
const { root, getState, rerenderWithElement } = renderHook(
|
|
336
|
+
vi.fn(),
|
|
337
|
+
makeElement({ id: "bg", sourceFile: "index.html" }),
|
|
338
|
+
);
|
|
339
|
+
act(() => {
|
|
340
|
+
getState().commitColorGrading(freshPopGrading());
|
|
341
|
+
});
|
|
342
|
+
expect(getState().grading.preset).toBe("fresh-pop");
|
|
343
|
+
rerenderWithElement(makeElement({ id: "bg", sourceFile: "sub-comp.html" }));
|
|
344
|
+
expect(getState().grading.preset).toBe("neutral");
|
|
345
|
+
act(() => root.unmount());
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("flushes — rather than discards — a pending persist for the previous element when selection changes before it fires", () => {
|
|
349
|
+
vi.useFakeTimers();
|
|
350
|
+
const onSetAttributeLive = vi.fn();
|
|
351
|
+
const { root, getState, rerenderWithElement } = renderHook(
|
|
352
|
+
onSetAttributeLive,
|
|
353
|
+
makeElement({ id: "s1-bg" }),
|
|
354
|
+
);
|
|
355
|
+
act(() => {
|
|
356
|
+
getState().commitColorGrading(freshPopGrading());
|
|
357
|
+
});
|
|
358
|
+
// Switch selection before the 350ms debounce fires — the in-flight edit
|
|
359
|
+
// must be written immediately (targeting the OUTGOING element's own
|
|
360
|
+
// commit callback), not silently dropped just because a debounce timer
|
|
361
|
+
// hadn't elapsed yet.
|
|
362
|
+
act(() => {
|
|
363
|
+
vi.advanceTimersByTime(200);
|
|
364
|
+
});
|
|
365
|
+
rerenderWithElement(makeElement({ id: "s2-bg" }));
|
|
366
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
367
|
+
const [attr, value] = onSetAttributeLive.mock.calls[0] as [string, string];
|
|
368
|
+
expect(attr).toBe("color-grading");
|
|
369
|
+
expect(value).toContain("fresh-pop");
|
|
370
|
+
// And it must not ALSO fire again once the (now-cleared) original timer
|
|
371
|
+
// window would have elapsed.
|
|
372
|
+
act(() => {
|
|
373
|
+
vi.advanceTimersByTime(400);
|
|
374
|
+
});
|
|
375
|
+
expect(onSetAttributeLive).toHaveBeenCalledTimes(1);
|
|
376
|
+
act(() => root.unmount());
|
|
377
|
+
vi.useRealTimers();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("does not permanently cache a non-OK media/metadata response — the next mount retries", async () => {
|
|
381
|
+
const videoWithSrc = () => {
|
|
382
|
+
const el = document.createElement("video");
|
|
383
|
+
el.setAttribute("src", "clip.mp4");
|
|
384
|
+
return el;
|
|
385
|
+
};
|
|
386
|
+
const fetchMock = vi
|
|
387
|
+
.fn()
|
|
388
|
+
.mockResolvedValueOnce({ ok: false } as Response)
|
|
389
|
+
.mockResolvedValueOnce({
|
|
390
|
+
ok: true,
|
|
391
|
+
json: () =>
|
|
392
|
+
Promise.resolve({ metadata: { kind: "video", color: { dynamicRange: "hdr" } } }),
|
|
393
|
+
} as unknown as Response);
|
|
394
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
395
|
+
|
|
396
|
+
const first = renderHook(vi.fn(), makeElement({ id: "retry-asset", element: videoWithSrc() }));
|
|
397
|
+
await act(async () => {
|
|
398
|
+
await Promise.resolve();
|
|
399
|
+
});
|
|
400
|
+
expect(first.getState().mediaMetadata).toBeNull();
|
|
401
|
+
act(() => first.root.unmount());
|
|
402
|
+
|
|
403
|
+
// A second, independent mount for the SAME asset path — if the failed
|
|
404
|
+
// response had been cached, this would never re-fetch and mediaMetadata
|
|
405
|
+
// would stay null forever.
|
|
406
|
+
const second = renderHook(
|
|
407
|
+
vi.fn(),
|
|
408
|
+
makeElement({ id: "retry-asset-2", element: videoWithSrc() }),
|
|
409
|
+
);
|
|
410
|
+
await act(async () => {
|
|
411
|
+
await Promise.resolve();
|
|
412
|
+
});
|
|
413
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
414
|
+
expect(second.getState().mediaMetadata?.color.dynamicRange).toBe("hdr");
|
|
415
|
+
act(() => second.root.unmount());
|
|
416
|
+
vi.unstubAllGlobals();
|
|
417
|
+
});
|
|
418
|
+
});
|