import { act, render, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MonacoModule } from "./loadMonaco"; import { loadMonaco } from "./loadMonaco"; import { AgentMonacoDiffViewer } from "./AgentMonacoDiffViewer"; vi.mock("./loadMonaco", () => ({ loadMonaco: vi.fn() })); describe("AgentMonacoDiffViewer", () => { beforeEach(() => { vi.mocked(loadMonaco).mockReset(); }); it("detaches models before disposing the editor and owned models", async () => { const harness = createMonacoHarness(); vi.mocked(loadMonaco).mockResolvedValue(harness.monaco); const view = render( ); await waitFor(() => { expect(harness.createDiffEditor).toHaveBeenCalledTimes(1); }); view.unmount(); expect(harness.events).toEqual([ "editor.setModel", "editor.setModel(null)", "editor.dispose", "originalModel.dispose", "modifiedModel.dispose" ]); }); it("does not create Monaco resources after unmounting during load", async () => { const harness = createMonacoHarness(); let resolveMonaco: ((monaco: MonacoModule) => void) | null = null; vi.mocked(loadMonaco).mockReturnValue( new Promise((resolve) => { resolveMonaco = resolve; }) ); const view = render( ); view.unmount(); await act(async () => { resolveMonaco?.(harness.monaco); await Promise.resolve(); }); expect(harness.createModel).not.toHaveBeenCalled(); expect(harness.createDiffEditor).not.toHaveBeenCalled(); }); it("updates the existing models when diff content changes", async () => { const harness = createMonacoHarness(); vi.mocked(loadMonaco).mockResolvedValue(harness.monaco); const view = render( ); await waitFor(() => { expect(harness.createDiffEditor).toHaveBeenCalledTimes(1); }); view.rerender( ); expect(harness.originalModel.setValue).toHaveBeenCalledWith("older"); expect(harness.modifiedModel.setValue).toHaveBeenCalledWith("newer"); expect(harness.createDiffEditor).toHaveBeenCalledTimes(1); }); }); function createMonacoHarness(): { monaco: MonacoModule; events: string[]; createModel: ReturnType; createDiffEditor: ReturnType; originalModel: ReturnType; modifiedModel: ReturnType; } { const events: string[] = []; const originalModel = createTextModel("before", "typescript", () => { events.push("originalModel.dispose"); }); const modifiedModel = createTextModel("after", "typescript", () => { events.push("modifiedModel.dispose"); }); const createModel = vi .fn() .mockReturnValueOnce(originalModel) .mockReturnValueOnce(modifiedModel); const editor = { setModel: vi.fn((model: unknown) => { events.push(model === null ? "editor.setModel(null)" : "editor.setModel"); }), dispose: vi.fn(() => { events.push("editor.dispose"); }) }; const createDiffEditor = vi.fn(() => editor); const monaco = { editor: { createModel, createDiffEditor, setTheme: vi.fn(), setModelLanguage: vi.fn() } } as unknown as MonacoModule; return { monaco, events, createModel, createDiffEditor, originalModel, modifiedModel }; } function createTextModel( initialValue: string, language: string, onDispose: () => void ) { let value = initialValue; let disposed = false; return { getValue: vi.fn(() => value), setValue: vi.fn((nextValue: string) => { value = nextValue; }), getLanguageId: vi.fn(() => language), isDisposed: vi.fn(() => disposed), dispose: vi.fn(() => { disposed = true; onDispose(); }) }; }