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