1import * as NodeFS from "node:fs";
2import * as NodeURL from "node:url";
3import { afterEach, beforeAll, expect, it } from "vite-plus/test";
4
5import { GhosttyModule, Terminal } from "../src/index.ts";
6
7let module: GhosttyModule;
8const terminals = new Set<Terminal>();
9
10beforeAll(async () => {
11 const path = NodeURL.fileURLToPath(import.meta.url).replace(
12 /test\/terminal\.test\.ts$/,
13 "vendor/ghostty-vt.wasm",
14 );
15 module = await GhosttyModule.load(new Uint8Array(NodeFS.readFileSync(path)));
16});
17
18afterEach(() => {
19 terminals.forEach((terminal) => terminal.free());
20 terminals.clear();
21});
22
23function create() {
24 const terminal = new Terminal(module, { cols: 20, rows: 4, maxScrollbackBytes: 1024 * 1024 });
25 terminals.add(terminal);
26 return terminal;
27}
28
29it("preserves Unicode, wide cells and ANSI under arbitrary byte chunking", () => {
30 const whole = create();
31 const split = create();
32 const bytes = new TextEncoder().encode("hello 👩‍💻 日本\r\n\x1b[31mred\x1b[0m");
33 whole.write(bytes);
34 for (let index = 0; index < bytes.length; index += 3) split.write(bytes.slice(index, index + 3));
35 expect(split.snapshot({ full: true })).toEqual(whole.snapshot({ full: true }));
36 expect(split.text()).toContain("hello 👩‍💻 日本");
37 expect(split.text()).toContain("red");
38});
39
40it("retains scrollback and reflows the same terminal through a resize", () => {
41 const terminal = create();
42 terminal.write("one\r\ntwo\r\nthree\r\nfour\r\nfive");
43 const before = terminal.scrollbar();
44 expect(before.total).toBeGreaterThan(before.len);
45 terminal.scrollToTop();
46 expect(terminal.scrollbar().offset).toBe(0);
47 terminal.resize(10, 3);
48 expect(terminal.cols).toBe(10);
49 expect(terminal.rows).toBe(3);
50 terminal.scrollToBottom();
51 expect(terminal.text()).toContain("five");
52});
53
54it("keeps a single destructive frame reader until disposal", () => {
55 const terminal = create();
56 const reader = terminal.acquireFrameReader();
57 terminal.write("hello");
58 expect(() => terminal.snapshot()).toThrow();
59 expect(
60 reader
61 .snapshot({ full: true })
62 .dirtyRows.some((row) => row.cells.some((cell) => cell.text === "h")),
63 ).toBe(true);
64 reader.dispose();
65 expect(terminal.snapshot({ full: true }).cols).toBe(20);
66});