From c4ff2b0c391c1e68faf03cc722f1d6dd10bb2519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 30 Sep 2026 15:30:46 -0400 Subject: [PATCH 1/2] fix(studio): undo that reloads the preview keeps the timeline clips on screen --- .../usePreviewPersistence.history.test.tsx | 47 +++++++++++++++++++ .../studio/src/hooks/usePreviewPersistence.ts | 8 ++-- 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 packages/studio/src/hooks/usePreviewPersistence.history.test.tsx diff --git a/packages/studio/src/hooks/usePreviewPersistence.history.test.tsx b/packages/studio/src/hooks/usePreviewPersistence.history.test.tsx new file mode 100644 index 0000000000..91f0c18f73 --- /dev/null +++ b/packages/studio/src/hooks/usePreviewPersistence.history.test.tsx @@ -0,0 +1,47 @@ +// @vitest-environment happy-dom + +import React, { act } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { usePlayerStore } from "../player/store/playerStore"; +import type { TimelineElement } from "../player/store/timelineElement"; +import { mountReactHarness } from "./domSelectionTestHarness"; +import { usePreviewPersistence } from "./usePreviewPersistence"; + +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +afterEach(() => { + document.body.innerHTML = ""; + usePlayerStore.getState().reset(); +}); + +describe("undo that reloads the preview", () => { + it("keeps the timeline's clips until the reloaded preview reports its own", async () => { + const clips = [{ id: "a" }, { id: "b" }] as unknown as TimelineElement[]; + usePlayerStore.getState().setElements(clips); + usePlayerStore.getState().setTimelineReady(true); + const reloadPreview = vi.fn(); + let sync: ReturnType["syncHistoryPreviewAfterApply"] | null = + null; + function Harness() { + sync = usePreviewPersistence({ + showToast: () => {}, + readOptionalProjectFile: async () => "", + writeProjectFile: async () => {}, + recordEdit: async () => {}, + previewIframeRef: { current: null }, + activeCompPathRef: { current: "index.html" }, + reloadPreview, + }).syncHistoryPreviewAfterApply; + return null; + } + mountReactHarness(); + + // A nested composition's undo is not the active file's, so it reloads the preview. + const files = { "compositions/sub.html": { previous: "

1

", restored: "

2

" } }; + await act(async () => sync!({ paths: Object.keys(files), files })); + + expect(reloadPreview).toHaveBeenCalledTimes(1); + const player = usePlayerStore.getState(); + expect([player.elements, player.timelineReady]).toEqual([clips, true]); + }); +}); diff --git a/packages/studio/src/hooks/usePreviewPersistence.ts b/packages/studio/src/hooks/usePreviewPersistence.ts index 913614a31b..70d86ef03e 100644 --- a/packages/studio/src/hooks/usePreviewPersistence.ts +++ b/packages/studio/src/hooks/usePreviewPersistence.ts @@ -206,17 +206,15 @@ export function usePreviewPersistence({ usePlayerStore.getState().currentTime, reloadPreview, ); + // The timeline keeps its clips until the reloaded preview reports its own, as after any other reload. if (strategy === "full") { - const player = usePlayerStore.getState(); - player.setElements([]); - player.setSelectedElementId(null); - player.setTimelineReady(false); + usePlayerStore.getState().setSelectedElementId(null); return; } // A soft restore patched the reverted attributes onto the live preview, but the // player store keeps its own copy and that copy is what the automation lanes // draw — so without this an undone envelope edit stayed invisible until a - // reload. The full path above clears the store and waits for discovery instead. + // reload. The full path above waits for the reloaded preview to report instead. syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null); }, [previewIframeRef, activeCompPathRef, reloadPreview], From 4451c3ced7129b45b5a7130ad12765fffca1890b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 30 Sep 2026 16:23:38 -0400 Subject: [PATCH 2/2] chore(studio): keep the comment share and complexity gates green --- packages/studio/src/hooks/usePreviewPersistence.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/studio/src/hooks/usePreviewPersistence.ts b/packages/studio/src/hooks/usePreviewPersistence.ts index 70d86ef03e..3b6144ab62 100644 --- a/packages/studio/src/hooks/usePreviewPersistence.ts +++ b/packages/studio/src/hooks/usePreviewPersistence.ts @@ -206,7 +206,6 @@ export function usePreviewPersistence({ usePlayerStore.getState().currentTime, reloadPreview, ); - // The timeline keeps its clips until the reloaded preview reports its own, as after any other reload. if (strategy === "full") { usePlayerStore.getState().setSelectedElementId(null); return;