1062 lines · 37.1 KB · TypeScript
RawDownload
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.