forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | import { fireEvent, render, screen, waitFor } from "@testing-library/react"; |
| 2 | import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| 3 | import { EditorPane } from "./EditorPane"; | |
| 4 | ||
| 5 | const { readFileMock, writeFileMock } = vi.hoisted(() => ({ | |
| 6 | readFileMock: vi.fn(), | |
| 7 | writeFileMock: vi.fn(), | |
| 8 | })); | |
| 9 | ||
| 10 | vi.mock("../api", () => ({ | |
| 11 | readFile: readFileMock, | |
| 12 | writeFile: writeFileMock, | |
| 13 | })); | |
| 14 | ||
| 15 | // Monaco is replaced by a plain textarea driving the same onChange contract. | |
| 16 | vi.mock("./MonacoViewer", () => ({ | |
| 17 | default: ({ | |
| 18 | content, | |
| 19 | onChange, | |
| 20 | }: { content: string; onChange?: (value: string) => void }) => ( | |
| 21 | <textarea | |
| 22 | aria-label="editor" | |
| 23 | value={content} | |
| 24 | onChange={(event) => onChange?.(event.target.value)} | |
| 25 | /> | |
| 26 | ), | |
| 27 | })); | |
| 28 | ||
| 29 | async function openEditorOn(path: string, content: string) { | |
| 30 | readFileMock.mockResolvedValue({ path, content }); | |
| 31 | render(<EditorPane path={path} />); | |
| 32 | return await screen.findByLabelText("editor"); | |
| 33 | } | |
| 34 | ||
| 35 | describe("EditorPane", () => { | |
| 36 | beforeEach(() => { | |
| 37 | readFileMock.mockReset(); | |
| 38 | writeFileMock.mockReset(); | |
| 39 | }); | |
| 40 | ||
| 41 | it("asks for a file when none is open", () => { | |
| 42 | render(<EditorPane path={null} />); | |
| 43 | expect(screen.getByText(/Double-click a file/)).toBeDefined(); | |
| 44 | }); | |
| 45 | ||
| 46 | it("loads the file into the editor with Save disabled", async () => { | |
| 47 | const editor = await openEditorOn("/w/main.go", "package main"); | |
| 48 | expect((editor as HTMLTextAreaElement).value).toBe("package main"); | |
| 49 | expect( | |
| 50 | (screen.getByRole("button", { name: "Save" }) as HTMLButtonElement) | |
| 51 | .disabled, | |
| 52 | ).toBe(true); | |
| 53 | }); | |
| 54 | ||
| 55 | it("marks the file dirty on edit and saves through the API", async () => { | |
| 56 | writeFileMock.mockResolvedValue(undefined); | |
| 57 | const editor = await openEditorOn("/w/main.go", "package main"); | |
| 58 | ||
| 59 | fireEvent.change(editor, { target: { value: "package main // edited" } }); | |
| 60 | expect(screen.getByText(/●/)).toBeDefined(); | |
| 61 | ||
| 62 | fireEvent.click(screen.getByRole("button", { name: "Save" })); | |
| 63 | await waitFor(() => | |
| 64 | expect(writeFileMock).toHaveBeenCalledWith( | |
| 65 | "/w/main.go", | |
| 66 | "package main // edited", | |
| 67 | ), | |
| 68 | ); | |
| 69 | await waitFor(() => expect(screen.getByText("saved")).toBeDefined()); | |
| 70 | expect(screen.queryByText(/●/)).toBeNull(); | |
| 71 | }); | |
| 72 | ||
| 73 | it("saves with Ctrl+S", async () => { | |
| 74 | writeFileMock.mockResolvedValue(undefined); | |
| 75 | const editor = await openEditorOn("/w/notes.txt", "a"); | |
| 76 | ||
| 77 | fireEvent.change(editor, { target: { value: "ab" } }); | |
| 78 | fireEvent.keyDown(editor, { key: "s", ctrlKey: true }); | |
| 79 | ||
| 80 | await waitFor(() => | |
| 81 | expect(writeFileMock).toHaveBeenCalledWith("/w/notes.txt", "ab"), | |
| 82 | ); | |
| 83 | }); | |
| 84 | ||
| 85 | it("keeps the dirty state and shows the error when saving fails", async () => { | |
| 86 | writeFileMock.mockRejectedValue(new Error("disk full")); | |
| 87 | const editor = await openEditorOn("/w/notes.txt", "a"); | |
| 88 | ||
| 89 | fireEvent.change(editor, { target: { value: "ab" } }); | |
| 90 | fireEvent.click(screen.getByRole("button", { name: "Save" })); | |
| 91 | ||
| 92 | await waitFor(() => expect(screen.getByText("disk full")).toBeDefined()); | |
| 93 | expect(screen.getByText(/●/)).toBeDefined(); | |
| 94 | }); | |
| 95 | ||
| 96 | it("surfaces read errors", async () => { | |
| 97 | readFileMock.mockRejectedValue(new Error("not a text file")); | |
| 98 | render(<EditorPane path="/w/blob.bin" />); | |
| 99 | await waitFor(() => | |
| 100 | expect(screen.getByText("not a text file")).toBeDefined(), | |
| 101 | ); | |
| 102 | }); | |
| 103 | }); |