Files
Triple-C/app/src/hooks/useTerminal.test.ts
T

84 lines
3.0 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach } from "vitest";
// The queue lives at module scope in useTerminal, so the command layer is
// mocked and the hook's `sendInput` is exercised through `renderHook`.
const terminalInput = vi.fn<(sessionId: string, data: number[]) => Promise<void>>();
vi.mock("../lib/tauri-commands", () => ({
terminalInput: (sessionId: string, data: number[]) => terminalInput(sessionId, data),
openTerminalSession: vi.fn(),
closeTerminalSession: vi.fn(),
terminalResize: vi.fn(),
pasteImageToTerminal: vi.fn(),
updateProject: vi.fn(),
}));
vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn() }));
import { renderHook } from "@testing-library/react";
import { useTerminal } from "./useTerminal";
const decode = (bytes: number[]) => new TextDecoder().decode(new Uint8Array(bytes));
describe("useTerminal input ordering", () => {
beforeEach(() => {
terminalInput.mockReset();
});
it("preserves order even when the underlying invokes resolve out of order", async () => {
// Make the *first* call the slowest, which is exactly the race that put a
// backspace behind the characters typed after it.
const resolvers: Array<() => void> = [];
terminalInput.mockImplementation(
() => new Promise<void>((resolve) => resolvers.push(resolve)),
);
const { result } = renderHook(() => useTerminal());
const first = result.current.sendInput("s1", "\x7f"); // backspace
const rest = ["a", "b", "c"].map((ch) => result.current.sendInput("s1", ch));
// Only one write may be in flight at a time.
expect(terminalInput).toHaveBeenCalledTimes(1);
expect(decode(terminalInput.mock.calls[0][1])).toBe("\x7f");
resolvers.shift()!();
await first;
// The three queued keystrokes coalesce into one ordered write.
expect(terminalInput).toHaveBeenCalledTimes(2);
expect(decode(terminalInput.mock.calls[1][1])).toBe("abc");
resolvers.shift()!();
await Promise.all(rest);
const sent = terminalInput.mock.calls.map((c) => decode(c[1])).join("");
expect(sent).toBe("\x7fabc");
});
it("settles each caller's promise and does not drop later writes on failure", async () => {
terminalInput.mockRejectedValueOnce(new Error("boom")).mockResolvedValue(undefined);
const { result } = renderHook(() => useTerminal());
await expect(result.current.sendInput("s2", "x")).rejects.toThrow("boom");
await expect(result.current.sendInput("s2", "y")).resolves.toBeUndefined();
expect(decode(terminalInput.mock.calls[1][1])).toBe("y");
});
it("keeps separate sessions independent", async () => {
terminalInput.mockResolvedValue(undefined);
const { result } = renderHook(() => useTerminal());
await Promise.all([
result.current.sendInput("a", "1"),
result.current.sendInput("b", "2"),
]);
const bySession = terminalInput.mock.calls.map((c) => [c[0], decode(c[1])]);
expect(bySession).toContainEqual(["a", "1"]);
expect(bySession).toContainEqual(["b", "2"]);
});
});