diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 3b1ad464a1..7c35cd7ae5 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -1,5 +1,5 @@ import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore"; -import { notePreviewReload } from "./player/sceneRemount"; +import { usePreviewReloadKey } from "./player/sceneRemount"; import { buildProjectApiPath } from "./utils/projectRouting"; import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react"; import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents"; @@ -82,7 +82,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St const [compIdToSrc, setCompIdToSrc] = useState>(new Map()); const previewIframe = useOwnPreviewIframe(); const [compositionLoading, setCompositionLoading] = useState(true); - const [refreshKey, setRefreshKey] = useState(0); + const { refreshKey, setRefreshKey, reloadPreview } = usePreviewReloadKey(); const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion(); const [blockPreview, setBlockPreview] = useState(null); const previewIframeRef = useRef(null); @@ -107,10 +107,6 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St const handleDomZIndexReorderCommitRef = useRef(null); const pendingTimelineEditPathRef = useRef(new Set()); const isGestureRecordingRef = useRef(false); - const reloadPreview = useCallback((changedPaths?: readonly string[]) => { - notePreviewReload(Array.isArray(changedPaths) ? changedPaths : undefined); - setRefreshKey((k) => k + 1); - }, []); const fileManager = useFileManager({ projectId, showToast, diff --git a/packages/studio/src/player/sceneRemount.test.ts b/packages/studio/src/player/sceneRemount.test.ts new file mode 100644 index 0000000000..861e204c3b --- /dev/null +++ b/packages/studio/src/player/sceneRemount.test.ts @@ -0,0 +1,45 @@ +// @vitest-environment happy-dom +import { act, createElement } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it } from "vitest"; +import { notePreviewReload, takePreviewReloadPaths, usePreviewReloadKey } from "./sceneRemount"; + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +afterEach(() => void takePreviewReloadPaths()); + +describe("preview reload paths", () => { + it("collects the scene files named before the reload runs", () => { + notePreviewReload(["compositions/a.html"]); + notePreviewReload(["compositions/b.html"]); + expect(takePreviewReloadPaths()).toEqual(["compositions/a.html", "compositions/b.html"]); + expect(takePreviewReloadPaths()).toBeNull(); + }); + + it("reloads the whole film when any reload in the same batch names no files", () => { + notePreviewReload(["compositions/a.html"]); + notePreviewReload(); + notePreviewReload(["compositions/b.html"]); + expect(takePreviewReloadPaths()).toBeNull(); + }); + + it("usePreviewReloadKey: a plain setRefreshKey bump in the same batch turns a scene swap into a film reload", () => { + let api: ReturnType | null = null; + const Probe = () => { + api = usePreviewReloadKey(); + return null; + }; + const root = createRoot(document.createElement("div")); + act(() => root.render(createElement(Probe))); + act(() => api!.reloadPreview(["compositions/a.html"])); + expect(api!.refreshKey).toBe(1); + expect(takePreviewReloadPaths()).toEqual(["compositions/a.html"]); + act(() => { + api!.reloadPreview(["compositions/a.html"]); + api!.setRefreshKey((key) => key + 1); + }); + expect(api!.refreshKey).toBe(3); + expect(takePreviewReloadPaths()).toBeNull(); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/player/sceneRemount.ts b/packages/studio/src/player/sceneRemount.ts index 83ab8abca6..84b4f7513b 100644 --- a/packages/studio/src/player/sceneRemount.ts +++ b/packages/studio/src/player/sceneRemount.ts @@ -1,3 +1,5 @@ +import { useCallback, useState, type SetStateAction } from "react"; + // Which files the next preview reload is for. A reload with no paths reloads the whole film. let pendingPaths: Set | "film" | null = null; @@ -48,3 +50,20 @@ function resolveUrl(path: string, base: string): string | null { return null; } } + +/** + * The preview reload counter. `reloadPreview(paths)` names the scene files that changed; every other + * bump, including `setRefreshKey`, reloads the whole film. + */ +export function usePreviewReloadKey() { + const [refreshKey, setKey] = useState(0); + const setRefreshKey = useCallback((update: SetStateAction) => { + notePreviewReload(); + setKey(update); + }, []); + const reloadPreview = useCallback((changedPaths?: readonly string[]) => { + notePreviewReload(Array.isArray(changedPaths) ? changedPaths : undefined); + setKey((key) => key + 1); + }, []); + return { refreshKey, setRefreshKey, reloadPreview }; +}