1import * as NodeModule from "node:module";
2import { act, createElement, useEffect, type ReactNode } from "react";
3import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
4
5import { useTerminalSurfaceBuffer } from "./useTerminalSurfaceBuffer";
6
7const { createRoot } = NodeModule.createRequire(import.meta.url)("react-dom/client") as {
8 createRoot(container: Element): {
9 render(children: ReactNode): void;
10 unmount(): void;
11 };
12};
13
14type Input = Parameters<typeof useTerminalSurfaceBuffer>[0] & { readonly readOnly: boolean };
15let root: ReturnType<typeof createRoot>;
16const displayed = vi.fn<(value: { buffer: string; readOnly: boolean }) => void>();
17
18function BufferProbe(input: Input) {
19 const buffer = useTerminalSurfaceBuffer(input);
20 useEffect(() => {
21 displayed({ buffer, readOnly: input.readOnly });
22 }, [buffer, input.readOnly]);
23 return null;
24}
25
26function render(changes: Partial<Input> = {}) {
27 return act(() =>
28 root.render(
29 createElement(BufferProbe, {
30 terminalKey: "environment:thread:term-1",
31 buffer: "host output",
32 readOnly: false,
33 ...changes,
34 }),
35 ),
36 );
37}
38
39beforeEach(() => {
40 displayed.mockClear();
41 const document = { nodeType: 9, addEventListener() {}, removeEventListener() {} };
42 const container = {
43 nodeType: 1,
44 tagName: "DIV",
45 namespaceURI: "http://www.w3.org/1999/xhtml",
46 ownerDocument: document,
47 addEventListener() {},
48 removeEventListener() {},
49 };
50 vi.stubGlobal("document", document);
51 vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget });
52 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
53 root = createRoot(container as unknown as HTMLElement);
54});
55
56afterEach(async () => {
57 await act(() => root.unmount());
58 vi.unstubAllGlobals();
59});
60
61it("preserves displayed history while permissions change before the next snapshot", async () => {
62 await render();
63 await render({ buffer: null, readOnly: true });
64 expect(displayed).toHaveBeenLastCalledWith({ buffer: "host output", readOnly: true });
65
66 await render({ buffer: "host output\nobserved output", readOnly: true });
67 await render({ buffer: null });
68 expect(displayed).toHaveBeenLastCalledWith({
69 buffer: "host output\nobserved output",
70 readOnly: false,
71 });
72 expect(displayed.mock.calls.every(([value]) => value.buffer.length > 0)).toBe(true);
73});
74
75it("still applies an intentional empty buffer", async () => {
76 await render();
77 await render({ buffer: null });
78 expect(displayed).toHaveBeenCalledTimes(1);
79 await render({ buffer: "" });
80 expect(displayed).toHaveBeenLastCalledWith({ buffer: "", readOnly: false });
81});
82
83it.each([
84 "secondary:thread:term-1",
85 "environment:another-thread:term-1",
86 "environment:thread:term-2",
87])("does not carry history into another terminal target (%s)", async (terminalKey) => {
88 await render();
89 await render({ terminalKey, buffer: null });
90 expect(displayed).toHaveBeenLastCalledWith({ buffer: "", readOnly: false });
91 await render({ terminalKey, buffer: "new target" });
92 expect(displayed).toHaveBeenLastCalledWith({ buffer: "new target", readOnly: false });
93});
94
95it("starts empty until its first snapshot arrives", async () => {
96 await render({ buffer: null });
97 expect(displayed).toHaveBeenLastCalledWith({ buffer: "", readOnly: false });
98 await render();
99 expect(displayed).toHaveBeenLastCalledWith({ buffer: "host output", readOnly: false });
100});