@hyperframes/studio 0.7.39 → 0.7.40
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/{index-312a3Ceu.js → index-89kPtC4s.js} +1 -1
- package/dist/assets/{index-B9YvRJz1.js → index-B2Utv-2b.js} +195 -195
- package/dist/assets/index-BpM6cnfP.css +1 -0
- package/dist/assets/{index-CQHiecE7.js → index-CjX8Ljc8.js} +1 -1
- package/dist/{chunk-RCLGSZ7C.js → chunk-BA66NM4L.js} +1 -1
- package/dist/{chunk-RCLGSZ7C.js.map → chunk-BA66NM4L.js.map} +1 -1
- package/dist/{domEditingLayers-S6YOLVRG.js → domEditingLayers-H7LDFIJ7.js} +2 -2
- package/dist/index.d.ts +34 -29
- package/dist/index.html +2 -2
- package/dist/index.js +2749 -2075
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -1
- package/src/components/StudioPreviewArea.tsx +6 -0
- package/src/components/StudioRightPanel.tsx +3 -0
- package/src/components/editor/DomEditOverlay.tsx +33 -47
- package/src/components/editor/PropertyPanel.test.ts +25 -0
- package/src/components/editor/PropertyPanel.tsx +33 -7
- package/src/components/editor/domEditingElement.ts +8 -1
- package/src/components/editor/manualEditsDom.ts +1 -18
- package/src/components/editor/manualEditsStyleHelpers.ts +18 -0
- package/src/components/editor/offCanvasIndicatorGeometry.ts +74 -0
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +154 -0
- package/src/components/editor/offCanvasIndicatorRefresh.ts +99 -0
- package/src/components/editor/propertyPanelHelpers.ts +11 -0
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/hooks/gsapDragCommit.test.ts +56 -0
- package/src/hooks/gsapDragCommit.ts +41 -109
- package/src/hooks/gsapDragPositionCommit.ts +3 -0
- package/src/hooks/gsapDragStaticSetHelpers.ts +109 -0
- package/src/hooks/gsapRuntimeBridge.ts +23 -2
- package/src/hooks/gsapRuntimeKeyframes.test.ts +34 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +13 -1
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +1 -0
- package/src/hooks/timelineEditingHelpers.ts +17 -5
- package/src/hooks/timelineTrackVisibility.test.ts +202 -0
- package/src/hooks/timelineTrackVisibility.ts +371 -0
- package/src/hooks/useRenderClipContent.test.ts +68 -2
- package/src/hooks/useRenderClipContent.ts +5 -6
- package/src/hooks/useTimelineEditing.ts +35 -30
- package/src/hooks/useTimelineEditingTypes.ts +30 -0
- package/src/icons/SystemIcons.tsx +12 -4
- package/src/player/components/AudioWaveform.tsx +11 -8
- package/src/player/components/CompositionThumbnail.tsx +14 -11
- package/src/player/components/PlayheadIndicator.tsx +21 -11
- package/src/player/components/Timeline.test.ts +105 -5
- package/src/player/components/Timeline.tsx +37 -47
- package/src/player/components/TimelineCanvas.tsx +275 -256
- package/src/player/components/TimelineClip.test.tsx +105 -0
- package/src/player/components/TimelineClip.tsx +44 -57
- package/src/player/components/TimelineRuler.tsx +17 -23
- package/src/player/components/TimelineShortcutHint.tsx +26 -0
- package/src/player/components/TimelineTypes.ts +18 -0
- package/src/player/components/VideoThumbnail.tsx +16 -13
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineIcons.tsx +2 -44
- package/src/player/components/timelineLayout.ts +18 -22
- package/src/player/components/timelineMotionStyles.test.ts +101 -0
- package/src/player/components/timelineTheme.test.ts +21 -4
- package/src/player/components/timelineTheme.ts +15 -15
- package/src/player/components/useTimelineActiveClips.test.ts +93 -0
- package/src/player/components/useTimelineActiveClips.ts +125 -0
- package/src/player/components/useTimelinePlayhead.ts +6 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +110 -3
- package/src/player/hooks/useExpandedTimelineElements.ts +106 -19
- package/src/player/lib/time.test.ts +1 -0
- package/src/player/lib/timelineDOM.test.ts +48 -1
- package/src/player/lib/timelineDOM.ts +7 -0
- package/src/player/lib/timelineIframeHelpers.ts +1 -0
- package/src/player/store/playerStore.ts +5 -1
- package/src/styles/studio.css +108 -0
- package/src/utils/sourcePatcher.test.ts +11 -0
- package/src/utils/sourcePatcher.ts +6 -4
- package/dist/assets/index-0P10SwC_.css +0 -1
- /package/dist/{domEditingLayers-S6YOLVRG.js.map → domEditingLayers-H7LDFIJ7.js.map} +0 -0
|
@@ -176,6 +176,40 @@ describe("hasNonHoldTweenForElement — strict live-tween existence (drag stale-
|
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
describe("hasNonHoldTweenForElement — channel-scoped (position vs sibling rotation)", () => {
|
|
180
|
+
const el = { id: "puck-c" };
|
|
181
|
+
const rotationTween = {
|
|
182
|
+
targets: () => [el],
|
|
183
|
+
vars: { keyframes: { "0%": { rotation: 0 }, "100%": { rotation: 90 } }, duration: 1 },
|
|
184
|
+
duration: () => 1,
|
|
185
|
+
startTime: () => 0,
|
|
186
|
+
};
|
|
187
|
+
const positionTween = {
|
|
188
|
+
targets: () => [el],
|
|
189
|
+
vars: { keyframes: { "0%": { x: 0, y: 0 }, "100%": { x: 100, y: 40 } }, duration: 1 },
|
|
190
|
+
duration: () => 1,
|
|
191
|
+
startTime: () => 0,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
it("unfiltered: a rotation-only keyframed tween counts as non-hold", () => {
|
|
195
|
+
expect(hasNonHoldTweenForElement(fakeIframe(el, [rotationTween]), "#puck-c")).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("position-scoped: a sibling rotation-only tween is NOT a live position tween", () => {
|
|
199
|
+
// The primary fix: a keyframed rotation must not route a static position hold
|
|
200
|
+
// into the keyframe branch. Scoping to position channels returns false here.
|
|
201
|
+
expect(
|
|
202
|
+
hasNonHoldTweenForElement(fakeIframe(el, [rotationTween]), "#puck-c", undefined, ["x", "y"]),
|
|
203
|
+
).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("position-scoped: a real keyframed position tween still counts", () => {
|
|
207
|
+
expect(
|
|
208
|
+
hasNonHoldTweenForElement(fakeIframe(el, [positionTween]), "#puck-c", undefined, ["x", "y"]),
|
|
209
|
+
).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
179
213
|
describe("arcPathFromMotionPathValue", () => {
|
|
180
214
|
it("builds arc config from object form { path, curviness }", () => {
|
|
181
215
|
const arc = arcPathFromMotionPathValue({
|
|
@@ -237,6 +237,7 @@ export function resolveRuntimeTween(
|
|
|
237
237
|
kind: "keyframe" | "set",
|
|
238
238
|
compositionId?: string,
|
|
239
239
|
channels?: string[],
|
|
240
|
+
// fallow-ignore-next-line code-duplication
|
|
240
241
|
): ResolvedRuntimeTween | null {
|
|
241
242
|
const timelines = timelinesOf(iframe);
|
|
242
243
|
if (!timelines) return null;
|
|
@@ -261,6 +262,7 @@ export function resolveRuntimeTween(
|
|
|
261
262
|
const wantChannels = channels && channels.length > 0 ? channels : null;
|
|
262
263
|
|
|
263
264
|
let first: ResolvedRuntimeTween | null = null;
|
|
265
|
+
// fallow-ignore-next-line code-duplication
|
|
264
266
|
let channelMatch: ResolvedRuntimeTween | null = null;
|
|
265
267
|
for (const tlId of tlIds) {
|
|
266
268
|
const timeline = timelines[tlId];
|
|
@@ -316,6 +318,7 @@ export function readRuntimeKeyframes(
|
|
|
316
318
|
selector: string,
|
|
317
319
|
compositionId?: string,
|
|
318
320
|
requireChannels?: string[],
|
|
321
|
+
// fallow-ignore-next-line code-duplication
|
|
319
322
|
): ReadTween | null {
|
|
320
323
|
const timelines = timelinesOf(iframe);
|
|
321
324
|
if (!timelines) return null;
|
|
@@ -344,11 +347,13 @@ export function readRuntimeKeyframes(
|
|
|
344
347
|
// (e.g. two non-overlapping gesture recordings → two separate `to()`s). The
|
|
345
348
|
// overlay must draw the segment under the PLAYHEAD, not blindly the first one
|
|
346
349
|
// — otherwise recording a second gesture leaves the path stuck on the first.
|
|
350
|
+
// fallow-ignore-next-line code-duplication
|
|
347
351
|
let firstRead: ReadTween | null = null;
|
|
348
352
|
for (const tlId of tlIds) {
|
|
349
353
|
const timeline = timelines[tlId];
|
|
350
354
|
if (!timeline?.getChildren) continue;
|
|
351
355
|
const now = typeof timeline.time === "function" ? timeline.time() : null;
|
|
356
|
+
// fallow-ignore-next-line code-duplication
|
|
352
357
|
for (const tween of timeline.getChildren(true)) {
|
|
353
358
|
if (!tween.vars || !matchesElement(tween, targetEl)) continue;
|
|
354
359
|
const dur = typeof tween.duration === "function" ? tween.duration() : 0;
|
|
@@ -377,12 +382,17 @@ export function readRuntimeKeyframes(
|
|
|
377
382
|
* The drag's stale-parse guard needs this exact distinction — after a delete-all
|
|
378
383
|
* only a hold may remain, and resurrecting the deleted tween from the stale parse
|
|
379
384
|
* must be avoided.
|
|
385
|
+
* When `channels` is provided, only tweens carrying one of those keyframe
|
|
386
|
+
* properties count as non-hold motion (e.g. position channels), so a sibling
|
|
387
|
+
* rotation/scale tween doesn't make a static position hold enter the keyframe
|
|
388
|
+
* branch.
|
|
380
389
|
*/
|
|
381
390
|
// fallow-ignore-next-line complexity
|
|
382
391
|
export function hasNonHoldTweenForElement(
|
|
383
392
|
iframe: HTMLIFrameElement | null,
|
|
384
393
|
selector: string,
|
|
385
394
|
compositionId?: string,
|
|
395
|
+
channels?: string[],
|
|
386
396
|
): boolean {
|
|
387
397
|
const timelines = timelinesOf(iframe);
|
|
388
398
|
if (!timelines) return false;
|
|
@@ -401,11 +411,13 @@ export function hasNonHoldTweenForElement(
|
|
|
401
411
|
}
|
|
402
412
|
if (!targetEl) return false;
|
|
403
413
|
|
|
414
|
+
// fallow-ignore-next-line code-duplication
|
|
404
415
|
for (const tween of timeline.getChildren(true)) {
|
|
405
416
|
if (!tween.vars || !matchesElement(tween, targetEl)) continue;
|
|
406
417
|
const dur = typeof tween.duration === "function" ? tween.duration() : 0;
|
|
407
418
|
if (isZeroDurationSet(dur)) continue; // skip hold/set tweens (see isZeroDurationSet)
|
|
408
|
-
|
|
419
|
+
const read = readTween(tween.vars);
|
|
420
|
+
if (read && (!channels || readCarriesChannel(read, channels))) return true;
|
|
409
421
|
}
|
|
410
422
|
return false;
|
|
411
423
|
}
|
|
@@ -29,6 +29,7 @@ export async function commitWholePropertyOffset(
|
|
|
29
29
|
callbacks: GsapDragCommitCallbacks,
|
|
30
30
|
label: string,
|
|
31
31
|
): Promise<void> {
|
|
32
|
+
// fallow-ignore-next-line code-duplication
|
|
32
33
|
let effectiveAnim = anim;
|
|
33
34
|
if (anim.keyframes) {
|
|
34
35
|
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
@@ -41,19 +41,30 @@ export type PatchTarget = NonNullable<ReturnType<typeof buildPatchTarget>>;
|
|
|
41
41
|
// The runtime re-reads data-start/data-duration from the DOM on each sync tick
|
|
42
42
|
// (packages/core/src/runtime/init.ts:1324-1368), so attribute mutations here are
|
|
43
43
|
// picked up automatically on the next frame without a rebind call.
|
|
44
|
-
export function
|
|
44
|
+
export function findTimelineElementInIframe(
|
|
45
45
|
iframe: HTMLIFrameElement | null,
|
|
46
46
|
element: TimelineElement,
|
|
47
|
-
|
|
48
|
-
): void {
|
|
47
|
+
): Element | null {
|
|
49
48
|
try {
|
|
50
49
|
const doc = iframe?.contentDocument;
|
|
51
|
-
if (!doc) return;
|
|
52
|
-
|
|
50
|
+
if (!doc) return null;
|
|
51
|
+
return element.domId
|
|
53
52
|
? doc.getElementById(element.domId)
|
|
54
53
|
: element.selector
|
|
55
54
|
? (doc.querySelectorAll(element.selector)[element.selectorIndex ?? 0] ?? null)
|
|
56
55
|
: null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function patchIframeDomTiming(
|
|
62
|
+
iframe: HTMLIFrameElement | null,
|
|
63
|
+
element: TimelineElement,
|
|
64
|
+
attrs: Array<[string, string]>,
|
|
65
|
+
): void {
|
|
66
|
+
try {
|
|
67
|
+
const el = findTimelineElementInIframe(iframe, element);
|
|
57
68
|
if (!el) return;
|
|
58
69
|
for (const [name, value] of attrs) el.setAttribute(name, value);
|
|
59
70
|
} catch {
|
|
@@ -61,6 +72,7 @@ export function patchIframeDomTiming(
|
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
// fallow-ignore-next-line complexity
|
|
64
76
|
export function resolveResizePlaybackStart(
|
|
65
77
|
original: string,
|
|
66
78
|
target: PatchTarget,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { usePlayerStore, type TimelineElement } from "../player";
|
|
5
|
+
import { toggleTimelineElementHidden, toggleTimelineTrackHidden } from "./timelineTrackVisibility";
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
document.body.innerHTML = "";
|
|
9
|
+
vi.unstubAllGlobals();
|
|
10
|
+
usePlayerStore.getState().reset();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function element(overrides: Partial<TimelineElement>): TimelineElement {
|
|
14
|
+
return {
|
|
15
|
+
id: "clip",
|
|
16
|
+
tag: "div",
|
|
17
|
+
start: 0,
|
|
18
|
+
duration: 2,
|
|
19
|
+
track: 0,
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stubProjectFiles(files: Map<string, string>) {
|
|
25
|
+
vi.stubGlobal(
|
|
26
|
+
"fetch",
|
|
27
|
+
vi.fn(async (input: RequestInfo | URL) => {
|
|
28
|
+
const url = String(input);
|
|
29
|
+
const encodedPath = url.slice(url.lastIndexOf("/") + 1);
|
|
30
|
+
const path = decodeURIComponent(encodedPath);
|
|
31
|
+
const content = files.get(path);
|
|
32
|
+
return new Response(JSON.stringify({ content }), {
|
|
33
|
+
status: content === undefined ? 404 : 200,
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
});
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("toggleTimelineTrackHidden", () => {
|
|
41
|
+
it("patches iframe DOM and persists all track elements as one edit-history entry", async () => {
|
|
42
|
+
const iframe = document.createElement("iframe");
|
|
43
|
+
document.body.append(iframe);
|
|
44
|
+
if (iframe.contentDocument) {
|
|
45
|
+
iframe.contentDocument.body.innerHTML = `
|
|
46
|
+
<div id="hero"></div>
|
|
47
|
+
<div id="subtitle"></div>
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const files = new Map([
|
|
52
|
+
[
|
|
53
|
+
"index.html",
|
|
54
|
+
`<div id="hero" data-start="0" data-duration="2"></div>
|
|
55
|
+
<div id="skip" data-start="0" data-duration="2"></div>`,
|
|
56
|
+
],
|
|
57
|
+
["scene.html", `<div id="subtitle" data-start="1" data-duration="2"></div>`],
|
|
58
|
+
]);
|
|
59
|
+
stubProjectFiles(files);
|
|
60
|
+
|
|
61
|
+
const writes = new Map<string, string>();
|
|
62
|
+
const recordEdit = vi.fn();
|
|
63
|
+
const timestampRef = { current: 0 };
|
|
64
|
+
const pendingRef = { current: new Set<string>() };
|
|
65
|
+
|
|
66
|
+
await toggleTimelineTrackHidden({
|
|
67
|
+
projectId: "project-1",
|
|
68
|
+
activeCompPath: "index.html",
|
|
69
|
+
timelineElements: [
|
|
70
|
+
element({ id: "hero", domId: "hero", track: 0 }),
|
|
71
|
+
element({ id: "skip", domId: "skip", track: 1 }),
|
|
72
|
+
element({ id: "subtitle", domId: "subtitle", track: 0, sourceFile: "scene.html" }),
|
|
73
|
+
],
|
|
74
|
+
track: 0,
|
|
75
|
+
hidden: true,
|
|
76
|
+
previewIframe: iframe,
|
|
77
|
+
writeProjectFile: async (path, content) => {
|
|
78
|
+
writes.set(path, content);
|
|
79
|
+
},
|
|
80
|
+
recordEdit,
|
|
81
|
+
domEditSaveTimestampRef: timestampRef,
|
|
82
|
+
pendingTimelineEditPathRef: pendingRef,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(iframe.contentDocument?.getElementById("hero")?.hasAttribute("data-hidden")).toBe(true);
|
|
86
|
+
expect(iframe.contentDocument?.getElementById("subtitle")?.hasAttribute("data-hidden")).toBe(
|
|
87
|
+
true,
|
|
88
|
+
);
|
|
89
|
+
expect(writes.get("index.html")).toContain('id="hero" data-start="0" data-duration="2"');
|
|
90
|
+
expect(writes.get("index.html")).toContain('data-hidden=""');
|
|
91
|
+
expect(writes.get("index.html")).toContain('id="skip" data-start="0" data-duration="2"');
|
|
92
|
+
expect(writes.get("scene.html")).toContain('data-hidden=""');
|
|
93
|
+
expect(pendingRef.current).toEqual(new Set(["index.html", "scene.html"]));
|
|
94
|
+
expect(timestampRef.current).toBeGreaterThan(0);
|
|
95
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide track 0");
|
|
97
|
+
expect(Object.keys(recordEdit.mock.calls[0]?.[0]?.files ?? {}).sort()).toEqual([
|
|
98
|
+
"index.html",
|
|
99
|
+
"scene.html",
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("removes data-hidden from every element on the track", async () => {
|
|
104
|
+
const files = new Map([
|
|
105
|
+
[
|
|
106
|
+
"index.html",
|
|
107
|
+
`<div id="hero" data-start="0" data-duration="2" data-hidden=""></div>
|
|
108
|
+
<div id="caption" data-start="2" data-duration="2" data-hidden=""></div>`,
|
|
109
|
+
],
|
|
110
|
+
]);
|
|
111
|
+
stubProjectFiles(files);
|
|
112
|
+
|
|
113
|
+
const writes = new Map<string, string>();
|
|
114
|
+
|
|
115
|
+
await toggleTimelineTrackHidden({
|
|
116
|
+
projectId: "project-1",
|
|
117
|
+
activeCompPath: "index.html",
|
|
118
|
+
timelineElements: [
|
|
119
|
+
element({ id: "hero", domId: "hero", track: 0, hidden: true }),
|
|
120
|
+
element({ id: "caption", domId: "caption", track: 0, hidden: true }),
|
|
121
|
+
],
|
|
122
|
+
track: 0,
|
|
123
|
+
hidden: false,
|
|
124
|
+
previewIframe: null,
|
|
125
|
+
writeProjectFile: async (path, content) => {
|
|
126
|
+
writes.set(path, content);
|
|
127
|
+
},
|
|
128
|
+
recordEdit: vi.fn(),
|
|
129
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
130
|
+
pendingTimelineEditPathRef: { current: new Set() },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(writes.get("index.html")).not.toContain("data-hidden");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("toggleTimelineElementHidden", () => {
|
|
138
|
+
it("persists data-hidden for only the selected element and updates the player store", async () => {
|
|
139
|
+
const iframe = document.createElement("iframe");
|
|
140
|
+
document.body.append(iframe);
|
|
141
|
+
const seek = vi.fn();
|
|
142
|
+
const win = iframe.contentWindow;
|
|
143
|
+
if (!win) throw new Error("Expected iframe contentWindow");
|
|
144
|
+
const playerWindow: Window & { __player?: { seek?: (time: number) => void } } = win;
|
|
145
|
+
playerWindow.__player = { seek };
|
|
146
|
+
|
|
147
|
+
const files = new Map([
|
|
148
|
+
[
|
|
149
|
+
"index.html",
|
|
150
|
+
`<div id="hero" data-start="0" data-duration="2"></div>
|
|
151
|
+
<div id="track-mate" data-start="1" data-duration="2"></div>`,
|
|
152
|
+
],
|
|
153
|
+
]);
|
|
154
|
+
stubProjectFiles(files);
|
|
155
|
+
|
|
156
|
+
const hero = element({ id: "hero", key: "index.html:#hero", domId: "hero", track: 0 });
|
|
157
|
+
const trackMate = element({
|
|
158
|
+
id: "track-mate",
|
|
159
|
+
key: "index.html:#track-mate",
|
|
160
|
+
domId: "track-mate",
|
|
161
|
+
track: 0,
|
|
162
|
+
});
|
|
163
|
+
usePlayerStore.getState().setElements([hero, trackMate]);
|
|
164
|
+
usePlayerStore.getState().setCurrentTime(1.25);
|
|
165
|
+
|
|
166
|
+
const writes = new Map<string, string>();
|
|
167
|
+
const recordEdit = vi.fn();
|
|
168
|
+
|
|
169
|
+
const changedPaths = await toggleTimelineElementHidden({
|
|
170
|
+
projectId: "project-1",
|
|
171
|
+
activeCompPath: "index.html",
|
|
172
|
+
timelineElements: [hero, trackMate],
|
|
173
|
+
elementKey: "index.html:#hero",
|
|
174
|
+
hidden: true,
|
|
175
|
+
previewIframe: iframe,
|
|
176
|
+
writeProjectFile: async (path, content) => {
|
|
177
|
+
writes.set(path, content);
|
|
178
|
+
},
|
|
179
|
+
recordEdit,
|
|
180
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
181
|
+
pendingTimelineEditPathRef: { current: new Set() },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(changedPaths).toEqual(["index.html"]);
|
|
185
|
+
expect(writes.get("index.html")).toContain('id="hero" data-start="0" data-duration="2"');
|
|
186
|
+
expect(writes.get("index.html")).toContain(
|
|
187
|
+
'id="hero" data-start="0" data-duration="2" data-hidden=""',
|
|
188
|
+
);
|
|
189
|
+
expect(writes.get("index.html")).toContain(
|
|
190
|
+
'id="track-mate" data-start="1" data-duration="2"></div>',
|
|
191
|
+
);
|
|
192
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide element");
|
|
194
|
+
expect(seek).toHaveBeenCalledWith(1.25);
|
|
195
|
+
expect(
|
|
196
|
+
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#hero")?.hidden,
|
|
197
|
+
).toBe(true);
|
|
198
|
+
expect(
|
|
199
|
+
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#track-mate")?.hidden,
|
|
200
|
+
).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
});
|