194 lines
5.2 KiB
TypeScript
194 lines
5.2 KiB
TypeScript
import { page } from "@vitest/browser/context";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { TerminalEmulatorRuntime } from "./terminal-emulator-runtime";
|
|
|
|
vi.mock("@xterm/addon-webgl", () => ({
|
|
WebglAddon: class WebglAddon {
|
|
activate(): void {}
|
|
dispose(): void {}
|
|
onContextLoss(): void {}
|
|
},
|
|
}));
|
|
|
|
type TerminalSize = { rows: number; cols: number };
|
|
|
|
type BrowserTerminal = TerminalSize & {
|
|
refresh: (start: number, end: number) => void;
|
|
reset: () => void;
|
|
};
|
|
|
|
type MountedTerminal = {
|
|
host: HTMLDivElement;
|
|
root: HTMLDivElement;
|
|
runtime: TerminalEmulatorRuntime;
|
|
sizes: TerminalSize[];
|
|
};
|
|
|
|
const mountedTerminals: MountedTerminal[] = [];
|
|
|
|
function nextFrame(): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
requestAnimationFrame(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitFor(input: { predicate: () => boolean; timeoutMs?: number }): Promise<void> {
|
|
const startedAt = performance.now();
|
|
const timeoutMs = input.timeoutMs ?? 2_000;
|
|
|
|
while (!input.predicate()) {
|
|
if (performance.now() - startedAt > timeoutMs) {
|
|
throw new Error("Timed out waiting for terminal browser condition");
|
|
}
|
|
await nextFrame();
|
|
}
|
|
}
|
|
|
|
function createTerminalHost(input: { width: number; height: number }): MountedTerminal {
|
|
const root = document.createElement("div");
|
|
root.style.width = `${input.width}px`;
|
|
root.style.height = `${input.height}px`;
|
|
root.style.position = "fixed";
|
|
root.style.left = "0";
|
|
root.style.top = "0";
|
|
root.style.overflow = "hidden";
|
|
|
|
const host = document.createElement("div");
|
|
host.style.width = "100%";
|
|
host.style.height = "100%";
|
|
root.appendChild(host);
|
|
document.body.appendChild(root);
|
|
|
|
const sizes: TerminalSize[] = [];
|
|
const runtime = new TerminalEmulatorRuntime();
|
|
runtime.setCallbacks({
|
|
callbacks: {
|
|
onResize: (size) => {
|
|
sizes.push(size);
|
|
},
|
|
},
|
|
});
|
|
runtime.mount({
|
|
root,
|
|
host,
|
|
initialSnapshot: null,
|
|
theme: {
|
|
background: "#0b0b0b",
|
|
foreground: "#e6e6e6",
|
|
cursor: "#e6e6e6",
|
|
},
|
|
});
|
|
|
|
const mounted = { host, root, runtime, sizes };
|
|
mountedTerminals.push(mounted);
|
|
return mounted;
|
|
}
|
|
|
|
function latestSize(sizes: TerminalSize[]): TerminalSize {
|
|
const size = sizes.at(-1);
|
|
if (!size) {
|
|
throw new Error("Terminal did not report a size");
|
|
}
|
|
return size;
|
|
}
|
|
|
|
function getBrowserTerminal(): BrowserTerminal {
|
|
const terminal = window.__paseoTerminal as BrowserTerminal | undefined;
|
|
if (!terminal) {
|
|
throw new Error("Expected xterm to be exposed for browser test inspection");
|
|
}
|
|
return terminal;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const mounted of mountedTerminals.splice(0)) {
|
|
mounted.runtime.unmount();
|
|
mounted.root.remove();
|
|
}
|
|
});
|
|
|
|
describe("terminal emulator runtime in a real browser", () => {
|
|
it("reports a larger PTY size when the terminal container grows", async () => {
|
|
await page.viewport(900, 600);
|
|
const mounted = createTerminalHost({ width: 360, height: 180 });
|
|
|
|
await waitFor({ predicate: () => mounted.sizes.length > 0 });
|
|
const initialSize = latestSize(mounted.sizes);
|
|
|
|
mounted.root.style.width = "720px";
|
|
mounted.root.style.height = "360px";
|
|
await nextFrame();
|
|
mounted.runtime.resize({ force: true });
|
|
|
|
await waitFor({
|
|
predicate: () => {
|
|
const size = latestSize(mounted.sizes);
|
|
return size.cols > initialSize.cols && size.rows > initialSize.rows;
|
|
},
|
|
});
|
|
|
|
const grownSize = latestSize(mounted.sizes);
|
|
expect(grownSize.cols).toBeGreaterThan(initialSize.cols);
|
|
expect(grownSize.rows).toBeGreaterThan(initialSize.rows);
|
|
});
|
|
|
|
it("refreshes visible rows on a forced same-size resize", async () => {
|
|
await page.viewport(900, 600);
|
|
const mounted = createTerminalHost({ width: 720, height: 360 });
|
|
|
|
await waitFor({ predicate: () => mounted.sizes.length > 0 });
|
|
|
|
const terminal = getBrowserTerminal();
|
|
const refreshCalls: Array<[number, number]> = [];
|
|
const originalRefresh = terminal.refresh.bind(terminal);
|
|
terminal.refresh = (start, end) => {
|
|
refreshCalls.push([start, end]);
|
|
originalRefresh(start, end);
|
|
};
|
|
|
|
mounted.runtime.resize({ force: true });
|
|
|
|
await waitFor({ predicate: () => refreshCalls.length > 0 });
|
|
expect(refreshCalls.at(-1)).toEqual([0, terminal.rows - 1]);
|
|
});
|
|
|
|
it("replays snapshots without synchronously resetting the visible terminal", async () => {
|
|
await page.viewport(900, 600);
|
|
const mounted = createTerminalHost({ width: 720, height: 360 });
|
|
|
|
await waitFor({ predicate: () => mounted.sizes.length > 0 });
|
|
|
|
const terminal = getBrowserTerminal();
|
|
const originalReset = terminal.reset.bind(terminal);
|
|
const reset = vi.fn(originalReset);
|
|
terminal.reset = reset;
|
|
|
|
mounted.runtime.renderSnapshot({
|
|
state: {
|
|
rows: terminal.rows,
|
|
cols: terminal.cols,
|
|
scrollback: [],
|
|
grid: [
|
|
[
|
|
{ char: "p" },
|
|
{ char: "r" },
|
|
{ char: "o" },
|
|
{ char: "m" },
|
|
{ char: "p" },
|
|
{ char: "t" },
|
|
],
|
|
],
|
|
cursor: {
|
|
row: 0,
|
|
col: 6,
|
|
},
|
|
},
|
|
});
|
|
await nextFrame();
|
|
|
|
expect(reset).not.toHaveBeenCalled();
|
|
});
|
|
});
|