mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
* fix(studio): enforce optimistic file concurrency * fix(studio): harden conditional file writes * fix(studio): honor explicit file preconditions * test(producer): allow zero-ms encode timing
110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
// @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<Response>((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<typeof useFileManager> | 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(<Probe projectId="project-a/../other?x=1" />));
|
|
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(<Probe projectId="project-b#fragment" />));
|
|
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());
|
|
});
|
|
});
|