nandi/oripublic Fork 0
35061753be581c0ba47a3189520d64e7788c183d
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

EditorPane.test.tsx · 103 lines · 3.1 KBTypeScript Blame HistoryRaw
  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(),
		);
	});
});