import { EnvironmentId } from "@t3tools/contracts"; import { AsyncResult } from "effect/unstable/reactivity"; import { act, StrictMode } from "react"; import { create, type ReactTestRenderer } from "react-test-renderer"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { writeFile, confirmFile } = vi.hoisted(() => ({ writeFile: vi.fn(), confirmFile: vi.fn(), })); vi.mock("~/state/projects", () => ({ projectEnvironment: { writeFile: {} } })); vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => writeFile })); vi.mock("./projectFilesQueryState", () => ({ confirmProjectFileQueryData: confirmFile })); import { setMarkdownTaskChecked } from "./filePreviewMode"; import { useFileSaveCoordinator } from "./useFileSaveCoordinator"; const environmentId = EnvironmentId.make("save-lifecycle-audit"); const onPendingChange = vi.fn(); const defaultProps = { environmentId, cwd: "/workspace", relativePath: "file.txt", onPendingChange, }; let renderer: ReactTestRenderer | null; function ChangeSource(_props: { onChange: (contents: string) => void }) { return null; } function FileSurface(props: Parameters[0]) { const coordinator = useFileSaveCoordinator(props); return coordinator.change(contents)} />; } function mount(props = defaultProps) { act(() => { renderer = create( , ); }); } function changeHandler(): (contents: string) => void { return renderer!.root.findByType(ChangeSource).props.onChange; } beforeEach(() => { renderer = null; vi.useFakeTimers(); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); writeFile.mockReset().mockResolvedValue(AsyncResult.success(undefined)); confirmFile.mockReset(); onPendingChange.mockReset(); }); afterEach(async () => { await act(async () => renderer?.unmount()); vi.useRealTimers(); vi.unstubAllGlobals(); }); describe("file-save React lifecycle", () => { it("persists editor model changes after StrictMode setup replay", async () => { mount(); changeHandler()("AUDIT7907NATIVE\n"); expect(onPendingChange).toHaveBeenCalledWith("file.txt", true); await vi.advanceTimersByTimeAsync(500); expect(writeFile).toHaveBeenCalledExactlyOnceWith({ environmentId, input: { cwd: "/workspace", relativePath: "file.txt", contents: "AUDIT7907NATIVE\n" }, }); expect(confirmFile).toHaveBeenCalledExactlyOnceWith( environmentId, "/workspace", "file.txt", "AUDIT7907NATIVE\n", ); expect(onPendingChange).toHaveBeenLastCalledWith("file.txt", false); }); it("persists rendered Markdown task changes after StrictMode setup replay", async () => { mount({ ...defaultProps, relativePath: "README.md" }); const nextContents = setMarkdownTaskChecked("- [ ] task\n", 2, true); changeHandler()(nextContents); await vi.advanceTimersByTimeAsync(500); expect(writeFile).toHaveBeenCalledExactlyOnceWith({ environmentId, input: { cwd: "/workspace", relativePath: "README.md", contents: "- [x] task\n" }, }); }); it("keeps the debounce across rerenders of the same file", async () => { mount(); changeHandler()("first"); await vi.advanceTimersByTimeAsync(300); act(() => renderer!.update( , ), ); changeHandler()("latest"); await vi.advanceTimersByTimeAsync(499); expect(writeFile).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(1); expect(writeFile).toHaveBeenCalledTimes(1); expect(writeFile.mock.calls[0]![0].input.contents).toBe("latest"); }); it("flushes on unmount and ignores a retired editor callback", async () => { mount(); const retiredChange = changeHandler(); retiredChange("pending edit"); await act(async () => renderer!.unmount()); renderer = null; retiredChange("stale editor contents"); await vi.runAllTimersAsync(); expect(writeFile).toHaveBeenCalledTimes(1); expect(writeFile.mock.calls[0]![0].input.contents).toBe("pending edit"); }); it.each([ { relativePath: "other.txt" }, { cwd: "/other-workspace" }, { environmentId: EnvironmentId.make("other-environment") }, ])("retires callbacks when the file identity changes: %j", async (change) => { mount(); const retiredChange = changeHandler(); retiredChange("old file edit"); const nextProps = { ...defaultProps, ...change }; act(() => renderer!.update( , ), ); retiredChange("stale editor contents"); changeHandler()("new file edit"); await vi.runAllTimersAsync(); expect(writeFile.mock.calls.map(([request]) => request)).toEqual([ { environmentId, input: { cwd: "/workspace", relativePath: "file.txt", contents: "old file edit" }, }, { environmentId: nextProps.environmentId, input: { cwd: nextProps.cwd, relativePath: nextProps.relativePath, contents: "new file edit", }, }, ]); }); it("does not reactivate a retired callback when the same file mounts again", async () => { mount(); const retiredChange = changeHandler(); await act(async () => renderer!.unmount()); renderer = null; mount(); retiredChange("stale contents"); changeHandler()("current contents"); await vi.runAllTimersAsync(); expect(writeFile).toHaveBeenCalledTimes(1); expect(writeFile.mock.calls[0]![0].input.contents).toBe("current contents"); }); });