diff --git a/packages/studio/src/components/panels/VariablesPanel.tsx b/packages/studio/src/components/panels/VariablesPanel.tsx index f61620222a..63369c9f62 100644 --- a/packages/studio/src/components/panels/VariablesPanel.tsx +++ b/packages/studio/src/components/panels/VariablesPanel.tsx @@ -258,12 +258,9 @@ export const VariablesPanel = memo(function VariablesPanel({ const { refreshKey } = useStudioPlaybackContext(); const { readProjectFile, writeProjectFile, fileTree, compositions } = useFileManagerContext(); const { domEditSelection } = useDomEditContext(); - // On the master view (no activeCompPath) the panel targets the project's real - // main composition — the first composition in the tree — not a hardcoded - // index.html that may not exist. This same path is used for the persist write - // target (so an edit never lands in a phantom index.html) AND the handoff - // render command. Null only when the project has no composition yet, in which - // case sdkSession is also null and the panel is inert. + // Master view (no activeCompPath) targets the real main composition, not a + // hardcoded index.html — used for both the persist write target and the + // handoff render command. Null only when the project has no composition. const effectiveCompPath = activeCompPath ?? resolveMasterCompositionPath(compositions); const previewValues = usePreviewVariablesStore((s) => s.values); const setPreviewValues = usePreviewVariablesStore((s) => s.setValues); diff --git a/packages/studio/src/hooks/useAutoOpenRootComposition.ts b/packages/studio/src/hooks/useAutoOpenRootComposition.ts index 100b3524e7..d098eba0ef 100644 --- a/packages/studio/src/hooks/useAutoOpenRootComposition.ts +++ b/packages/studio/src/hooks/useAutoOpenRootComposition.ts @@ -1,9 +1,8 @@ import { useEffect, useRef } from "react"; /** - * Opens the root composition once hydration settles on no selection; fires at most once per - * `projectId`. Relies on the caller resetting selection on an in-session project switch - * (see `useResetSelectionOnProjectSwitch`) or a stale selection reads as "already open". + * Opens the root composition once hydration settles with no selection, at most once per + * `projectId`. Caller must reset selection on a project switch (`useResetSelectionOnProjectSwitch`). */ export function useAutoOpenRootComposition({ projectId, diff --git a/packages/studio/src/hooks/useFileTree.test.tsx b/packages/studio/src/hooks/useFileTree.test.tsx index 44998393bf..e37634492a 100644 --- a/packages/studio/src/hooks/useFileTree.test.tsx +++ b/packages/studio/src/hooks/useFileTree.test.tsx @@ -156,6 +156,64 @@ describe("useFileTree.refreshFileTree", () => { fetchSpy.mockRestore(); }); + it("aborts the superseded request's fetch when a newer refresh is issued", async () => { + let call = 0; + const signals: AbortSignal[] = []; + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async (_url, init?: RequestInit) => { + call += 1; + if (call === 1) { + return new Response( + JSON.stringify({ files: ["index.html"], compositions: ["index.html"] }), + { status: 200 }, + ); + } + if (init?.signal) signals.push(init.signal); + return new Promise(() => {}); + }); + + const handleRef = { current: null as Handle | null }; + const host = document.createElement("div"); + document.body.append(host); + root = createRoot(host); + + await act(async () => { + root?.render( + { + handleRef.current = h; + }} + projectId="project-a" + />, + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + act(() => { + void handleRef.current?.refresh(); + }); + await act(async () => { + await Promise.resolve(); + }); + expect(signals).toHaveLength(1); + expect(signals[0].aborted).toBe(false); + + act(() => { + void handleRef.current?.refresh(); + }); + await act(async () => { + await Promise.resolve(); + }); + + expect(signals).toHaveLength(2); + expect(signals[0].aborted).toBe(true); + expect(signals[1].aborted).toBe(false); + + fetchSpy.mockRestore(); + }); + it("keeps the prior compositions list when a refresh response omits the field", async () => { let call = 0; const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => {