import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { GhosttyTerminalCore, type GhosttyCell, type GhosttyRow } from "./core";import { DEFAULT_TERMINAL_FONT_FAMILY, DEFAULT_TERMINAL_FONT_SIZE, advanceTerminalSelectionClickSequence, applyTerminalCopyEvent, clearPrimedTerminalCopyInput, ghosttyMouseButton, isTerminalAltGraphText, isTerminalCompositionCommitInput, isTerminalCompositionKey, isTerminalCopyShortcut, isTerminalPasteShortcut, loadTerminalFontFamily, primeTerminalCopyInput, resolveTerminalMouseData, resolveTerminalMouseTrackingState, shouldBlinkTerminalCursor, shouldReportTerminalMouse, terminalGridCellAt, terminalScrollbarGeometry, terminalScrollbarOffsetAtPointer, terminalLinkAtPositionWithRange, terminalContentOriginY, terminalFontFamily, terminalFontSize, terminalWheelArrowData, terminalWheelDeltaRows, GhosttyTerminalSurface, type GhosttyTerminalSurfaceOptions,} from "./surface"; vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({ default: (await import("./vendor/ghostty-vt.wasm?inline")).default,}));vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({ default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default,})); describe("GhosttyTerminalSurface visibility", () => { const surfaces = new Set<GhosttyTerminalSurface>(); // Keep the real surface, renderer, and WASM core. Only browser layout and // scheduling are replaced so tests can count work while the terminal is hidden. function createHarness() { vi.useFakeTimers(); const frames = new Map<number, FrameRequestCallback>(); const resizeCallbacks = new Set<() => void>(); const paint = vi.fn((_operation: string, _args: ReadonlyArray<unknown>) => {}); let frameId = 0; const requestFrame = vi.fn((callback: FrameRequestCallback) => { frames.set(++frameId, callback); return frameId; }); class TerminalTestElement extends EventTarget { style: Record<string, string> = {}; parentElement: TerminalTestElement | null = null; clientWidth = 168; clientHeight = 104; width = 300; height = 150; value = ""; private readonly captures = new Set<number>(); setAttribute() {} append(...children: TerminalTestElement[]) { for (const child of children) child.parentElement = this; } replaceChildren(...children: TerminalTestElement[]) { this.append(...children); } remove() { this.parentElement = null; } getContext() { return context; } focus() { this.dispatchEvent(new Event("focus")); } setPointerCapture(pointerId: number) { this.captures.add(pointerId); } hasPointerCapture(pointerId: number) { return this.captures.has(pointerId); } releasePointerCapture(pointerId: number) { this.captures.delete(pointerId); } getBoundingClientRect() { return { left: 0, top: 0, right: 168, bottom: 104, width: 168, height: 104 }; } } const canvas = new TerminalTestElement(); const mount = new TerminalTestElement(); const context = { canvas, beginPath() {}, clip() {}, rect() {}, resetTransform() {}, restore() {}, save() {}, setTransform() {}, fillRect: (...args: number[]) => paint("fillRect", args), strokeRect: (...args: number[]) => paint("strokeRect", args), fillText: (...args: [string, number, number, number?]) => paint("fillText", args), measureText: (text: string) => ({ width: text.length * 8, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 3, }), }; vi.stubGlobal("document", { createElement: (tag: string) => (tag === "canvas" ? canvas : new TerminalTestElement()), fonts: Object.assign(new EventTarget(), { load: async () => [], add() {} }), }); vi.stubGlobal( "window", Object.assign(new EventTarget(), { devicePixelRatio: 1, requestAnimationFrame: requestFrame, cancelAnimationFrame: (id: number) => frames.delete(id), setTimeout, clearTimeout, setInterval, clearInterval, matchMedia: () => Object.assign(new EventTarget(), { matches: false }), }), ); vi.stubGlobal( "ResizeObserver", class { constructor(private readonly callback: () => void) { resizeCallbacks.add(callback); } observe() {} disconnect() { resizeCallbacks.delete(this.callback); } }, ); const snapshot = vi.spyOn(GhosttyTerminalCore.prototype, "snapshot"); const onData = vi.fn<(data: string) => void>(); return { mount, frames, paint, requestFrame, snapshot, onData, get renderedSnapshot() { const result = snapshot.mock.results.at(-1); if (result?.type !== "return") throw new Error("No terminal snapshot was rendered"); return result.value; }, flushFrame() { const queued = [...frames.values()]; frames.clear(); for (const callback of queued) callback(0); }, resize() { for (const callback of resizeCallbacks) callback(); }, pointer(type: string, clientX: number, buttons: number, shiftKey = false, button = 0) { canvas.dispatchEvent( Object.assign(new Event(type, { cancelable: true }), { clientX, clientY: 5, pointerId: 1, button, buttons, shiftKey, }), ); }, async create(options: Partial<GhosttyTerminalSurfaceOptions> = {}) { const surface = await GhosttyTerminalSurface.create(mount as unknown as HTMLElement, { theme: { foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, cursor: { r: 255, g: 255, b: 255 }, }, onData, onResize() {}, onSelectionChange() {}, beforeKey: () => false, onLinkActivate() {}, ...options, get visible() { return options.visible ?? true; }, }); surfaces.add(surface); return surface; }, }; } afterEach(() => { for (const surface of surfaces) surface.dispose(); surfaces.clear(); vi.useRealTimers(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); it("stops hidden snapshots and paint while preserving live VT replies and the next cursor", async () => { const harness = createHarness(); const surface = await harness.create(); surface.focus(); surface.write("ready\x1b[1 q"); harness.flushFrame(); vi.advanceTimersByTime(500); harness.flushFrame(); surface.write("queued"); expect(harness.frames.size).toBe(1); surface.setVisible(false); expect(harness.frames.size).toBe(0); expect(vi.getTimerCount()).toBe(0); harness.snapshot.mockClear(); harness.paint.mockClear(); harness.requestFrame.mockClear(); surface.write("\x1b[2J\x1b[H\x1b[3"); surface.write("1mhidden"); surface.write("界🙂\x1b[0m\x1b[5n\x1b[6n"); surface.fit(); vi.advanceTimersByTime(2_000); harness.flushFrame(); expect(harness.onData.mock.calls).toEqual([["\x1b[0n"], ["\x1b[1;11R"]]); expect(harness.snapshot).not.toHaveBeenCalled(); expect(harness.paint).not.toHaveBeenCalled(); expect(harness.requestFrame).not.toHaveBeenCalled(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(harness.renderedSnapshot).toMatchObject({ cursorX: 10, cursorY: 0 }); expect(harness.renderedSnapshot.rowData[0]?.text).toContain("hidden"); expect(harness.paint.mock.calls).toContainEqual(["fillRect", [84, 4, 8, 16]]); expect(harness.frames.size).toBe(0); }); it("keeps the selection on reveal and applies a hidden selection clear", async () => { const harness = createHarness(); const surface = await harness.create(); surface.write("hello world"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); harness.flushFrame(); expect(surface.getSelection()).toBe("hello"); const position = surface.getSelectionPosition(); surface.setVisible(false); harness.snapshot.mockClear(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(surface.getSelectionPosition()).toEqual(position); expect( harness.renderedSnapshot.rowData[0]?.cells.slice(0, 5).every((cell) => cell.selected), ).toBe(true); surface.setVisible(false); harness.snapshot.mockClear(); surface.clearSelection(); surface.write("!"); expect(harness.snapshot).not.toHaveBeenCalled(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(surface.getSelection()).toBe(""); expect(surface.getSelectionPosition()).toBeNull(); expect(harness.renderedSnapshot.rowData[0]?.cells.some((cell) => cell.selected)).toBe(false); }); it("pastes the terminal selection, and only that, on a Linux middle click", async () => { const harness = createHarness(); const readText = vi.fn(async () => "clipboard text"); vi.stubGlobal("navigator", { platform: "Linux x86_64", clipboard: { readText } }); const surface = await harness.create(); surface.write("hello world"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); expect(surface.getSelection()).toBe("hello"); harness.onData.mockClear(); harness.pointer("pointerdown", 5, 4, false, 1); await vi.waitFor(() => expect(harness.onData).toHaveBeenCalled()); expect(harness.onData.mock.calls.at(-1)?.[0]).toBe("hello"); expect(surface.getSelection()).toBe("hello"); // Without a selection there is no primary buffer to paste; the clipboard // holds what the user copied and must not be substituted. surface.clearSelection(); harness.pointer("pointerdown", 5, 4, false, 1); expect(readText).not.toHaveBeenCalled(); }); it("starts a selection when dragging from a link", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); expect(onLinkActivate).not.toHaveBeenCalled(); expect(surface.getSelection()).toBe("https"); }); it("keeps a link click active through slight pointer movement", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 6, 1); harness.pointer("pointerup", 6, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); }); it("uses repeated link clicks for word and line selection", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com tail"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); expect(surface.getSelection()).not.toBe(""); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); expect(surface.getSelection()).toBe("https://example.com tail"); }); it("uses Shift drags over links for selection", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1, true); harness.pointer("pointermove", 37, 1, true); harness.pointer("pointerup", 37, 0, true); expect(onLinkActivate).not.toHaveBeenCalled(); expect(surface.getSelection()).toBe("https"); }); it("does not activate a link replaced before pointer release", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://first.example"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); surface.write("\x1b[2J\x1b[Hhttps://second.example"); harness.flushFrame(); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).not.toHaveBeenCalled(); }); it("stops zero-size mounts and repaints when the same size returns", async () => { const harness = createHarness(); const surface = await harness.create(); surface.focus(); surface.write("visible"); harness.flushFrame(); harness.snapshot.mockClear(); harness.paint.mockClear(); harness.mount.clientWidth = 0; surface.write("!"); harness.flushFrame(); harness.requestFrame.mockClear(); for (let index = 0; index < 8; index += 1) surface.write("x"); vi.advanceTimersByTime(2_000); expect(harness.snapshot).not.toHaveBeenCalled(); expect(harness.paint).not.toHaveBeenCalled(); expect(harness.requestFrame).not.toHaveBeenCalled(); harness.mount.clientWidth = 168; harness.resize(); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(harness.renderedSnapshot.rowData[0]?.text).toContain("visible!xxxxxxxx"); expect(harness.paint).toHaveBeenCalled(); }); it.each([false, true])( "uses visibility %s if it changes while WASM initializes", async (visible) => { const harness = createHarness(); let markCreated!: () => void; const created = new Promise<void>((resolve) => { markCreated = resolve; }); let finishLoading!: () => void; const release = new Promise<void>((resolve) => { finishLoading = resolve; }); const create = GhosttyTerminalCore.create; vi.spyOn(GhosttyTerminalCore, "create").mockImplementation(async (...args) => { const core = await create(...args); markCreated(); await release; return core; }); let currentVisible = !visible; const pending = harness.create({ get visible() { return currentVisible; }, }); await created; currentVisible = visible; harness.paint.mockClear(); finishLoading(); const surface = await pending; expect(harness.snapshot).toHaveBeenCalledTimes(visible ? 1 : 0); if (!visible) expect(harness.paint).not.toHaveBeenCalled(); surface.write("ready\x1b[5n"); expect(harness.onData).toHaveBeenCalledWith("\x1b[0n"); if (!visible) { expect(harness.frames.size).toBe(0); surface.setVisible(true); } else { harness.flushFrame(); } expect(harness.renderedSnapshot.rowData[0]?.text).toContain("ready"); }, );}); const cell = (text: string): GhosttyCell => ({ text, wide: 0, foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, bold: false, italic: false, invisible: false, strikethrough: false, overline: false, underline: false, selected: false,}); describe("isTerminalAltGraphText", () => { it("defers printable AltGr output to the textarea input event", () => { expect( isTerminalAltGraphText({ key: "@", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(true); expect( isTerminalAltGraphText({ key: "ArrowRight", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(false); });}); describe("terminalGridCellAt", () => { const options = { bounds: { left: 100, top: 200 }, cols: 3, rows: 2, metrics: { width: 10, height: 20 }, padding: 4, originY: 24, }; it("maps points inside the rendered grid without clamping its padding", () => { expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 224 })).toEqual({ x: 0,Lines 1–500 of 1062. Load more
import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { GhosttyTerminalCore, type GhosttyCell, type GhosttyRow } from "./core"; import { DEFAULT_TERMINAL_FONT_FAMILY, DEFAULT_TERMINAL_FONT_SIZE, advanceTerminalSelectionClickSequence, applyTerminalCopyEvent, clearPrimedTerminalCopyInput, ghosttyMouseButton, isTerminalAltGraphText, isTerminalCompositionCommitInput, isTerminalCompositionKey, isTerminalCopyShortcut, isTerminalPasteShortcut, loadTerminalFontFamily, primeTerminalCopyInput, resolveTerminalMouseData, resolveTerminalMouseTrackingState, shouldBlinkTerminalCursor, shouldReportTerminalMouse, terminalGridCellAt, terminalScrollbarGeometry, terminalScrollbarOffsetAtPointer, terminalLinkAtPositionWithRange, terminalContentOriginY, terminalFontFamily, terminalFontSize, terminalWheelArrowData, terminalWheelDeltaRows, GhosttyTerminalSurface, type GhosttyTerminalSurfaceOptions, } from "./surface"; vi.mock("./vendor/ghostty-vt.wasm?url", async () => ({ default: (await import("./vendor/ghostty-vt.wasm?inline")).default, })); vi.mock("./vendor/ghostty-write-pty.wasm?url&no-inline", async () => ({ default: (await import("./vendor/ghostty-write-pty.wasm?inline")).default, })); describe("GhosttyTerminalSurface visibility", () => { const surfaces = new Set<GhosttyTerminalSurface>(); // Keep the real surface, renderer, and WASM core. Only browser layout and // scheduling are replaced so tests can count work while the terminal is hidden. function createHarness() { vi.useFakeTimers(); const frames = new Map<number, FrameRequestCallback>(); const resizeCallbacks = new Set<() => void>(); const paint = vi.fn((_operation: string, _args: ReadonlyArray<unknown>) => {}); let frameId = 0; const requestFrame = vi.fn((callback: FrameRequestCallback) => { frames.set(++frameId, callback); return frameId; }); class TerminalTestElement extends EventTarget { style: Record<string, string> = {}; parentElement: TerminalTestElement | null = null; clientWidth = 168; clientHeight = 104; width = 300; height = 150; value = ""; private readonly captures = new Set<number>(); setAttribute() {} append(...children: TerminalTestElement[]) { for (const child of children) child.parentElement = this; } replaceChildren(...children: TerminalTestElement[]) { this.append(...children); } remove() { this.parentElement = null; } getContext() { return context; } focus() { this.dispatchEvent(new Event("focus")); } setPointerCapture(pointerId: number) { this.captures.add(pointerId); } hasPointerCapture(pointerId: number) { return this.captures.has(pointerId); } releasePointerCapture(pointerId: number) { this.captures.delete(pointerId); } getBoundingClientRect() { return { left: 0, top: 0, right: 168, bottom: 104, width: 168, height: 104 }; } } const canvas = new TerminalTestElement(); const mount = new TerminalTestElement(); const context = { canvas, beginPath() {}, clip() {}, rect() {}, resetTransform() {}, restore() {}, save() {}, setTransform() {}, fillRect: (...args: number[]) => paint("fillRect", args), strokeRect: (...args: number[]) => paint("strokeRect", args), fillText: (...args: [string, number, number, number?]) => paint("fillText", args), measureText: (text: string) => ({ width: text.length * 8, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 3, }), }; vi.stubGlobal("document", { createElement: (tag: string) => (tag === "canvas" ? canvas : new TerminalTestElement()), fonts: Object.assign(new EventTarget(), { load: async () => [], add() {} }), }); vi.stubGlobal( "window", Object.assign(new EventTarget(), { devicePixelRatio: 1, requestAnimationFrame: requestFrame, cancelAnimationFrame: (id: number) => frames.delete(id), setTimeout, clearTimeout, setInterval, clearInterval, matchMedia: () => Object.assign(new EventTarget(), { matches: false }), }), ); vi.stubGlobal( "ResizeObserver", class { constructor(private readonly callback: () => void) { resizeCallbacks.add(callback); } observe() {} disconnect() { resizeCallbacks.delete(this.callback); } }, ); const snapshot = vi.spyOn(GhosttyTerminalCore.prototype, "snapshot"); const onData = vi.fn<(data: string) => void>(); return { mount, frames, paint, requestFrame, snapshot, onData, get renderedSnapshot() { const result = snapshot.mock.results.at(-1); if (result?.type !== "return") throw new Error("No terminal snapshot was rendered"); return result.value; }, flushFrame() { const queued = [...frames.values()]; frames.clear(); for (const callback of queued) callback(0); }, resize() { for (const callback of resizeCallbacks) callback(); }, pointer(type: string, clientX: number, buttons: number, shiftKey = false, button = 0) { canvas.dispatchEvent( Object.assign(new Event(type, { cancelable: true }), { clientX, clientY: 5, pointerId: 1, button, buttons, shiftKey, }), ); }, async create(options: Partial<GhosttyTerminalSurfaceOptions> = {}) { const surface = await GhosttyTerminalSurface.create(mount as unknown as HTMLElement, { theme: { foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, cursor: { r: 255, g: 255, b: 255 }, }, onData, onResize() {}, onSelectionChange() {}, beforeKey: () => false, onLinkActivate() {}, ...options, get visible() { return options.visible ?? true; }, }); surfaces.add(surface); return surface; }, }; } afterEach(() => { for (const surface of surfaces) surface.dispose(); surfaces.clear(); vi.useRealTimers(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); it("stops hidden snapshots and paint while preserving live VT replies and the next cursor", async () => { const harness = createHarness(); const surface = await harness.create(); surface.focus(); surface.write("ready\x1b[1 q"); harness.flushFrame(); vi.advanceTimersByTime(500); harness.flushFrame(); surface.write("queued"); expect(harness.frames.size).toBe(1); surface.setVisible(false); expect(harness.frames.size).toBe(0); expect(vi.getTimerCount()).toBe(0); harness.snapshot.mockClear(); harness.paint.mockClear(); harness.requestFrame.mockClear(); surface.write("\x1b[2J\x1b[H\x1b[3"); surface.write("1mhidden"); surface.write("界🙂\x1b[0m\x1b[5n\x1b[6n"); surface.fit(); vi.advanceTimersByTime(2_000); harness.flushFrame(); expect(harness.onData.mock.calls).toEqual([["\x1b[0n"], ["\x1b[1;11R"]]); expect(harness.snapshot).not.toHaveBeenCalled(); expect(harness.paint).not.toHaveBeenCalled(); expect(harness.requestFrame).not.toHaveBeenCalled(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(harness.renderedSnapshot).toMatchObject({ cursorX: 10, cursorY: 0 }); expect(harness.renderedSnapshot.rowData[0]?.text).toContain("hidden"); expect(harness.paint.mock.calls).toContainEqual(["fillRect", [84, 4, 8, 16]]); expect(harness.frames.size).toBe(0); }); it("keeps the selection on reveal and applies a hidden selection clear", async () => { const harness = createHarness(); const surface = await harness.create(); surface.write("hello world"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); harness.flushFrame(); expect(surface.getSelection()).toBe("hello"); const position = surface.getSelectionPosition(); surface.setVisible(false); harness.snapshot.mockClear(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(surface.getSelectionPosition()).toEqual(position); expect( harness.renderedSnapshot.rowData[0]?.cells.slice(0, 5).every((cell) => cell.selected), ).toBe(true); surface.setVisible(false); harness.snapshot.mockClear(); surface.clearSelection(); surface.write("!"); expect(harness.snapshot).not.toHaveBeenCalled(); surface.setVisible(true); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(surface.getSelection()).toBe(""); expect(surface.getSelectionPosition()).toBeNull(); expect(harness.renderedSnapshot.rowData[0]?.cells.some((cell) => cell.selected)).toBe(false); }); it("pastes the terminal selection, and only that, on a Linux middle click", async () => { const harness = createHarness(); const readText = vi.fn(async () => "clipboard text"); vi.stubGlobal("navigator", { platform: "Linux x86_64", clipboard: { readText } }); const surface = await harness.create(); surface.write("hello world"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); expect(surface.getSelection()).toBe("hello"); harness.onData.mockClear(); harness.pointer("pointerdown", 5, 4, false, 1); await vi.waitFor(() => expect(harness.onData).toHaveBeenCalled()); expect(harness.onData.mock.calls.at(-1)?.[0]).toBe("hello"); expect(surface.getSelection()).toBe("hello"); // Without a selection there is no primary buffer to paste; the clipboard // holds what the user copied and must not be substituted. surface.clearSelection(); harness.pointer("pointerdown", 5, 4, false, 1); expect(readText).not.toHaveBeenCalled(); }); it("starts a selection when dragging from a link", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 37, 1); harness.pointer("pointerup", 37, 0); expect(onLinkActivate).not.toHaveBeenCalled(); expect(surface.getSelection()).toBe("https"); }); it("keeps a link click active through slight pointer movement", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointermove", 6, 1); harness.pointer("pointerup", 6, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); }); it("uses repeated link clicks for word and line selection", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com tail"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); expect(surface.getSelection()).not.toBe(""); harness.pointer("pointerdown", 5, 1); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).toHaveBeenCalledOnce(); expect(surface.getSelection()).toBe("https://example.com tail"); }); it("uses Shift drags over links for selection", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://example.com"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1, true); harness.pointer("pointermove", 37, 1, true); harness.pointer("pointerup", 37, 0, true); expect(onLinkActivate).not.toHaveBeenCalled(); expect(surface.getSelection()).toBe("https"); }); it("does not activate a link replaced before pointer release", async () => { const harness = createHarness(); const onLinkActivate = vi.fn(); const surface = await harness.create({ onLinkActivate }); surface.write("https://first.example"); harness.flushFrame(); harness.pointer("pointerdown", 5, 1); surface.write("\x1b[2J\x1b[Hhttps://second.example"); harness.flushFrame(); harness.pointer("pointerup", 5, 0); expect(onLinkActivate).not.toHaveBeenCalled(); }); it("stops zero-size mounts and repaints when the same size returns", async () => { const harness = createHarness(); const surface = await harness.create(); surface.focus(); surface.write("visible"); harness.flushFrame(); harness.snapshot.mockClear(); harness.paint.mockClear(); harness.mount.clientWidth = 0; surface.write("!"); harness.flushFrame(); harness.requestFrame.mockClear(); for (let index = 0; index < 8; index += 1) surface.write("x"); vi.advanceTimersByTime(2_000); expect(harness.snapshot).not.toHaveBeenCalled(); expect(harness.paint).not.toHaveBeenCalled(); expect(harness.requestFrame).not.toHaveBeenCalled(); harness.mount.clientWidth = 168; harness.resize(); expect(harness.snapshot).toHaveBeenCalledTimes(1); expect(harness.renderedSnapshot.rowData[0]?.text).toContain("visible!xxxxxxxx"); expect(harness.paint).toHaveBeenCalled(); }); it.each([false, true])( "uses visibility %s if it changes while WASM initializes", async (visible) => { const harness = createHarness(); let markCreated!: () => void; const created = new Promise<void>((resolve) => { markCreated = resolve; }); let finishLoading!: () => void; const release = new Promise<void>((resolve) => { finishLoading = resolve; }); const create = GhosttyTerminalCore.create; vi.spyOn(GhosttyTerminalCore, "create").mockImplementation(async (...args) => { const core = await create(...args); markCreated(); await release; return core; }); let currentVisible = !visible; const pending = harness.create({ get visible() { return currentVisible; }, }); await created; currentVisible = visible; harness.paint.mockClear(); finishLoading(); const surface = await pending; expect(harness.snapshot).toHaveBeenCalledTimes(visible ? 1 : 0); if (!visible) expect(harness.paint).not.toHaveBeenCalled(); surface.write("ready\x1b[5n"); expect(harness.onData).toHaveBeenCalledWith("\x1b[0n"); if (!visible) { expect(harness.frames.size).toBe(0); surface.setVisible(true); } else { harness.flushFrame(); } expect(harness.renderedSnapshot.rowData[0]?.text).toContain("ready"); }, ); }); const cell = (text: string): GhosttyCell => ({ text, wide: 0, foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, bold: false, italic: false, invisible: false, strikethrough: false, overline: false, underline: false, selected: false, }); describe("isTerminalAltGraphText", () => { it("defers printable AltGr output to the textarea input event", () => { expect( isTerminalAltGraphText({ key: "@", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(true); expect( isTerminalAltGraphText({ key: "ArrowRight", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(false); }); }); describe("terminalGridCellAt", () => { const options = { bounds: { left: 100, top: 200 }, cols: 3, rows: 2, metrics: { width: 10, height: 20 }, padding: 4, originY: 24, }; it("maps points inside the rendered grid without clamping its padding", () => { expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 224 })).toEqual({ x: 0,