// @vitest-environment happy-dom import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; vi.mock("./useFileTree", () => ({ useFileTree: () => ({ projectDir: "", fileTree: [], setFileTree: vi.fn(), fileTreeLoaded: true, refreshFileTree: vi.fn(async () => {}), compositions: [], assets: [], fontAssets: [], }), })); vi.mock("./useEditorSave", () => ({ useEditorSave: () => ({ saveRafRef: { current: null }, handleContentChange: vi.fn(), }), })); import { useFileManager } from "./useFileManager"; (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; describe("useFileManager project ownership", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("keeps delayed callbacks bound to the project that created them", async () => { let resolveProjectARead: ((value: Response) => void) | undefined; const projectARead = new Promise((resolve) => { resolveProjectARead = resolve; }); const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (url.endsWith("/files/missing.html") && !init?.method) { return Promise.resolve({ ok: false, status: 404 } as Response); } if (url.includes("project-a") && !init?.method) return projectARead; if (!init?.method) { return Promise.resolve({ ok: true, json: async () => ({ content: "PROJECT_B", version: "b-v1" }), } as Response); } return Promise.resolve({ ok: true, json: async () => ({ version: "a-v2" }) } as Response); }); vi.stubGlobal("fetch", fetchMock); const captured: { manager: ReturnType | null } = { manager: null }; function Probe({ projectId }: { projectId: string }) { captured.manager = useFileManager({ projectId, showToast: vi.fn(), recordEdit: vi.fn(async () => {}), domEditSaveTimestampRef: { current: 0 }, setRefreshKey: vi.fn(), }); return null; } const root = createRoot(document.createElement("div")); await act(async () => root.render()); const managerA = captured.manager; if (!managerA) throw new Error("project A manager did not render"); const delayedRead = managerA.readProjectFile("index.html"); await act(async () => root.render()); const managerB = captured.manager; if (!managerB) throw new Error("project B manager did not render"); expect(managerB.writeProjectFile).not.toBe(managerA.writeProjectFile); resolveProjectARead?.({ ok: true, json: async () => ({ content: "PROJECT_A", version: "a-v1" }), } as Response); await expect(delayedRead).resolves.toBe("PROJECT_A"); await managerA.writeProjectFile("index.html", "A_AFTER"); await managerA.writeProjectFile("missing.html", "A_NEW"); await expect(managerB.readProjectFile("index.html")).resolves.toBe("PROJECT_B"); await expect(managerB.readOptionalProjectFile("index.html")).resolves.toBe("PROJECT_B"); expect(fetchMock).toHaveBeenCalledWith( "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html", ); expect(fetchMock).toHaveBeenCalledWith( "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html", expect.objectContaining({ method: "PUT", body: "A_AFTER" }), ); expect(fetchMock).toHaveBeenCalledWith( "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/missing.html", ); expect(fetchMock).toHaveBeenCalledWith( "/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/missing.html", expect.objectContaining({ method: "PUT", body: "A_NEW" }), ); expect(fetchMock).toHaveBeenCalledWith("/api/projects/project-b%23fragment/files/index.html"); expect(fetchMock).toHaveBeenCalledWith( "/api/projects/project-b%23fragment/files/index.html?optional=1", ); await act(async () => root.unmount()); }); });