638 lines Β· 22.6 KB Β· TypeScript
RawDownload
import { describe, expect, it, vi } from "vite-plus/test"; import type { GhosttyCell, GhosttyRow } from "./core";import {  DEFAULT_TERMINAL_FONT_FAMILY,  DEFAULT_TERMINAL_FONT_SIZE,  advanceTerminalSelectionClickSequence,  applyTerminalCopyEvent,  clearPrimedTerminalCopyInput,  ghosttyMouseButton,  isTerminalAltGraphText,  isTerminalCompositionCommitInput,  isTerminalCompositionKey,  isTerminalCopyShortcut,  isTerminalLinkPointerGesture,  isTerminalPasteShortcut,  loadTerminalFontFamily,  primeTerminalCopyInput,  resolveTerminalMouseData,  resolveTerminalMouseTrackingState,  shouldBlinkTerminalCursor,  shouldReportTerminalMouse,  terminalGridCellAt,  terminalScrollbarGeometry,  terminalScrollbarOffsetAtPointer,  terminalLinkAtColumn,  terminalLinkAtPosition,  terminalLinkAtPositionWithRange,  terminalContentOriginY,  terminalFontFamily,  terminalFontSize,  terminalWheelArrowData,  terminalWheelDeltaRows,} from "./surface"; 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,      y: 0,    });    expect(terminalGridCellAt({ ...options, clientX: 133, clientY: 263 })).toEqual({      x: 2,      y: 1,    });    expect(terminalGridCellAt({ ...options, clientX: 103, clientY: 224 })).toBeNull();    expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 223 })).toBeNull();    expect(terminalGridCellAt({ ...options, clientX: 134, clientY: 224 })).toBeNull();    expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 264 })).toBeNull();  });}); describe("shouldBlinkTerminalCursor", () => {  const blinking = {    focused: true,    cursorBlinking: true,    cursorVisible: true,    reducedMotion: false,  };   it("blinks a focused visible cursor the terminal asked to blink", () => {    expect(shouldBlinkTerminalCursor(blinking)).toBe(true);  });   it("holds the cursor steady when blinking would be unwanted", () => {    // Unfocused surfaces draw a steady hollow cursor, DECSCUSR steady styles and    // DEC mode 12 turn blinking off, a hidden cursor has nothing to toggle, and    // reduced-motion readers get no permanently animating element.    expect(shouldBlinkTerminalCursor({ ...blinking, focused: false })).toBe(false);    expect(shouldBlinkTerminalCursor({ ...blinking, cursorBlinking: false })).toBe(false);    expect(shouldBlinkTerminalCursor({ ...blinking, cursorVisible: false })).toBe(false);    expect(shouldBlinkTerminalCursor({ ...blinking, reducedMotion: true })).toBe(false);  });}); describe("terminalLinkAtColumn", () => {  it("maps terminal cells to UTF-16 offsets after a wide emoji", () => {    const cells = [      cell("πŸ™‚"),      cell(""),      ...Array.from("https://t3.codes", (character) => cell(character)),    ];    const row: GhosttyRow = {      cells,      text: cells        .map((value) => value.text || " ")        .join("")        .trimEnd(),      isWrapContinuation: false,      wrapsToNext: false,    };     expect(terminalLinkAtColumn(row, 2)).toBe("https://t3.codes");    expect(terminalLinkAtColumn(row, cells.length - 1)).toBe("https://t3.codes");    expect(terminalLinkAtColumn(row, 0)).toBeNull();    expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({      start: { x: 2, y: 0 },      end: { x: cells.length - 1, y: 0 },    });  });   it("uses shared path matching and reconstructs soft-wrapped links", () => {    const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({      cells: Array.from(text.padEnd(16), (character) => cell(character)),      text: text.trimEnd(),      isWrapContinuation,      wrapsToNext,    });    const rows = [      row("https://example.", false),      row("com/reference", true),      row("~/project/file", false),      row("C:\\repo\\file.ts", false),    ];     expect(terminalLinkAtPosition(rows, 0, 8)).toBe("https://example.com/reference");    expect(terminalLinkAtPosition(rows, 1, 4)).toBe("https://example.com/reference");    expect(terminalLinkAtPosition(rows, 2, 2)).toBe("~/project/file");    expect(terminalLinkAtPosition(rows, 3, 4)).toBe("C:\\repo\\file.ts");    expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({      text: "https://example.com/reference",      range: {        start: { x: 0, y: 0 },        end: { x: 12, y: 1 },      },    });  });   it("refuses links truncated at the viewport edges instead of mis-resolving", () => {    const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({      cells: Array.from(text.padEnd(16), (character) => cell(character)),      text: text.trimEnd(),      isWrapContinuation,      wrapsToNext,    });    // The head of the wrapped line scrolled above the viewport.    const headCut = [row("ple.com/missing", true), row("head", true)];    expect(terminalLinkAtPosition(headCut, 0, 4)).toBeNull();    // The bottom row soft-wraps on below the viewport.    const tailCut = [row("https://t3.codes", false, true)];    expect(terminalLinkAtPosition(tailCut, 0, 8)).toBeNull();    // A partial bottom row is provably complete and still resolves.    const complete = [row("https://t3.codes", false), row("", false)];    expect(terminalLinkAtPosition(complete, 0, 8)).toBe("https://t3.codes");    // A wide grapheme earlier in the row must not break truncation detection:    // the soft-wrap flag decides, not string-length-versus-cell-count.    const wideFull: GhosttyRow = {      cells: [        { ...cell("πŸ™‚"), wide: 1 },        { ...cell(""), wide: 2 },        ...Array.from("https://t3.code", (character) => cell(character)),      ],      text: "πŸ™‚ https://t3.code",      isWrapContinuation: false,      wrapsToNext: true,    };    expect(terminalLinkAtPosition([wideFull], 0, 8)).toBeNull();    // Unwritten trailing cells prove the bottom row is complete.    const unwrittenTail: GhosttyRow = {      cells: [        ...Array.from("https://t3.codes", (character) => cell(character)),        cell(""),        cell(""),      ],      text: "https://t3.codes",      isWrapContinuation: false,      wrapsToNext: false,    };    expect(terminalLinkAtPosition([unwrittenTail], 0, 8)).toBe("https://t3.codes");  });}); describe("isTerminalCopyShortcut", () => {  const event = (overrides: Partial<Parameters<typeof isTerminalCopyShortcut>[0]> = {}) => ({    ctrlKey: false,    key: "c",    metaKey: false,    shiftKey: false,    ...overrides,  });   it("keeps Ctrl+C available for SIGINT on macOS", () => {    expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false);    expect(isTerminalCopyShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);  });   it("copies with Ctrl+C and Ctrl+Shift+C elsewhere", () => {    expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(true);    expect(isTerminalCopyShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe(      true,    );    expect(isTerminalCopyShortcut(event({}), "Linux x86_64")).toBe(false);  });   it("uses the produced character instead of the physical key position", () => {    expect(isTerminalCopyShortcut(event({ key: "C", metaKey: true }), "MacIntel")).toBe(true);    expect(isTerminalCopyShortcut(event({ key: "j", metaKey: true }), "MacIntel")).toBe(false);  });}); describe("applyTerminalCopyEvent", () => {  it("writes the selection and claims the fallback when clipboardData is present", () => {    const setData = vi.fn();    expect(applyTerminalCopyEvent("ls -la", { setData })).toEqual({      preventDefault: true,      claimWriteFallback: true,    });    expect(setData).toHaveBeenCalledWith("text/plain", "ls -la");  });   it("leaves the writeText fallback alive when clipboardData is missing", () => {    // Electron's edit-menu Copy often delivers a copy event with no    // clipboardData. Claiming that event used to skip writeText and copy the    // empty IME textarea, which is the blank clipboard users paste.    expect(applyTerminalCopyEvent("ls -la", null)).toEqual({      preventDefault: false,      claimWriteFallback: false,    });    expect(applyTerminalCopyEvent("", { setData: vi.fn() })).toEqual({      preventDefault: false,      claimWriteFallback: false,    });  });   it("primes the current selection before a copy event with no clipboardData", () => {    const input = {      value: "stale",      selectionStart: 0,      selectionEnd: 0,      select() {        this.selectionStart = 0;        this.selectionEnd = this.value.length;      },    };    primeTerminalCopyInput(input, "git status");    expect(applyTerminalCopyEvent("git status", null)).toEqual({      preventDefault: false,      claimWriteFallback: false,    });    expect(input.value).toBe("git status");    expect(input.selectionStart).toBe(0);    expect(input.selectionEnd).toBe(10);  });}); describe("primeTerminalCopyInput", () => {  it("selects the Ghostty selection in the hidden textarea so native copy has text", () => {    const input = {      value: "",      selectionStart: 0,      selectionEnd: 0,      select() {        this.selectionStart = 0;        this.selectionEnd = this.value.length;      },    };    primeTerminalCopyInput(input, "git status");    expect(input.value).toBe("git status");    expect(input.selectionStart).toBe(0);    expect(input.selectionEnd).toBe(10);    clearPrimedTerminalCopyInput(input, "git status");    expect(input.value).toBe("");  });   it("does not wipe an IME candidate that replaced the primed copy", () => {    const input = { value: "", select() {} };    primeTerminalCopyInput(input, "git status");    input.value = "あ";    clearPrimedTerminalCopyInput(input, "git status");    expect(input.value).toBe("あ");  });}); describe("isTerminalPasteShortcut", () => {  const event = (overrides: Partial<Parameters<typeof isTerminalPasteShortcut>[0]> = {}) => ({    ctrlKey: false,    key: "v",    metaKey: false,    shiftKey: false,    ...overrides,  });   it("uses Cmd+V on macOS", () => {    expect(isTerminalPasteShortcut(event({ metaKey: true }), "MacIntel")).toBe(true);    expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false);  });   it("preserves Ctrl+V and uses Ctrl+Shift+V elsewhere", () => {    expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false);    expect(isTerminalPasteShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe(      true,    );  });   it("supports the conventional Shift+Insert paste shortcut", () => {    expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "Linux x86_64")).toBe(      true,    );    expect(isTerminalPasteShortcut(event({ key: "Insert" }), "Linux x86_64")).toBe(false);    expect(      isTerminalPasteShortcut(        event({ key: "Insert", ctrlKey: true, shiftKey: true }),        "Linux x86_64",      ),    ).toBe(false);    expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "MacIntel")).toBe(      false,    );  });}); describe("isTerminalCompositionCommitInput", () => {  it("identifies browser composition follow-up input", () => {    expect(isTerminalCompositionCommitInput({ inputType: "" })).toBe(true);    expect(isTerminalCompositionCommitInput({ inputType: "insertCompositionText" })).toBe(true);    expect(isTerminalCompositionCommitInput({ inputType: "insertFromComposition" })).toBe(true);  });   it("keeps a fast repeated input as legitimate text", () => {    expect(isTerminalCompositionCommitInput({ inputType: "insertText" })).toBe(false);  });}); describe("isTerminalCompositionKey", () => {  const event = (    overrides: Partial<Pick<KeyboardEvent, "isComposing" | "key" | "keyCode">> = {},  ) => ({    isComposing: false,    key: "a",    keyCode: 65,    ...overrides,  });   it("treats in-progress IME keydowns as composition so the copy primer cannot wipe them", () => {    expect(isTerminalCompositionKey(event({ isComposing: true }), false)).toBe(true);    expect(isTerminalCompositionKey(event(), true)).toBe(true);    expect(isTerminalCompositionKey(event({ key: "Process" }), false)).toBe(true);    expect(isTerminalCompositionKey(event({ keyCode: 229 }), false)).toBe(true);  });   it("lets ordinary keydowns clear a primed copy", () => {    expect(isTerminalCompositionKey(event(), false)).toBe(false);  });}); describe("application mouse reporting", () => {  const event = (overrides: Partial<Parameters<typeof shouldReportTerminalMouse>[1]> = {}) => ({    ctrlKey: false,    metaKey: false,    shiftKey: false,    ...overrides,  });   it("keeps Shift and link activation modifiers available to the browser", () => {    expect(shouldReportTerminalMouse(true, event())).toBe(true);    expect(shouldReportTerminalMouse(true, event({ shiftKey: true }))).toBe(false);    expect(shouldReportTerminalMouse(true, event({ ctrlKey: true }))).toBe(false);    expect(shouldReportTerminalMouse(true, event({ metaKey: true }))).toBe(false);    expect(shouldReportTerminalMouse(false, event())).toBe(false);  });   it("maps browser buttons to Ghostty's button enum", () => {    expect([0, 1, 2, 3, 4, 5].map(ghosttyMouseButton)).toEqual([1, 3, 2, 4, 5, null]);  });   it("drops repeated motion reports until another mouse action resets the cell", () => {    const first = resolveTerminalMouseData("motion", "\u001b[<35;8;4M", "");    expect(first).toEqual({ send: true, nextMotionData: "\u001b[<35;8;4M" });     const duplicate = resolveTerminalMouseData("motion", "\u001b[<35;8;4M", first.nextMotionData);    expect(duplicate).toEqual({ send: false, nextMotionData: "\u001b[<35;8;4M" });     const press = resolveTerminalMouseData("press", "\u001b[<0;8;4M", duplicate.nextMotionData);    expect(press).toEqual({ send: true, nextMotionData: "" });     expect(resolveTerminalMouseData("motion", "\u001b[<35;8;4M", press.nextMotionData)).toEqual({      send: true,      nextMotionData: "\u001b[<35;8;4M",    });  });   it("clears the motion baseline when application mouse tracking changes", () => {    expect(resolveTerminalMouseTrackingState(true, false, "\u001b[<35;8;4M")).toEqual({      tracking: false,      motionData: "",    });    expect(resolveTerminalMouseTrackingState(false, true, "\u001b[<35;8;4M")).toEqual({      tracking: true,      motionData: "",    });    expect(resolveTerminalMouseTrackingState(true, true, "\u001b[<35;8;4M")).toEqual({      tracking: true,      motionData: "\u001b[<35;8;4M",    });    expect(resolveTerminalMouseTrackingState(false, false, "\u001b[<35;8;4M")).toEqual({      tracking: false,      motionData: "\u001b[<35;8;4M",    });  });}); describe("terminal font resolution", () => {  it("validates the requested face after its styles load", async () => {    let loaded = false;    const load = vi.fn(async () => {      loaded = true;      return [];    });    const resolve = vi.fn(() => {      expect(loaded).toBe(true);      return DEFAULT_TERMINAL_FONT_FAMILY;    });     await expect(      loadTerminalFontFamily("Proportional Test", 12, {        load,        resolve,      }),    ).resolves.toBe(DEFAULT_TERMINAL_FONT_FAMILY);    expect(load).toHaveBeenCalledTimes(4);    expect(resolve).toHaveBeenCalledWith("Proportional Test");  });   it("keeps the glyph fallbacks behind a custom text face", () => {    expect(terminalFontFamily()).toBe(DEFAULT_TERMINAL_FONT_FAMILY);    expect(terminalFontFamily("  ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);    const custom = terminalFontFamily('"Fira Code"');    expect(custom.startsWith('"Fira Code", ')).toBe(true);    expect(custom).toContain('"Symbols Nerd Font Mono"');    expect(custom.endsWith("monospace")).toBe(true);  });   it("ignores proportional families the cell grid cannot lay out", () => {    // jsdom has no canvas metrics, so the probe answers "monospace" and the    // family is kept; the guard is exercised in the browser instead. Assert the    // shape stays intact so a rejected face still yields a usable stack.    const stack = terminalFontFamily("Helvetica Neue");    expect(stack.endsWith("monospace")).toBe(true);  });   it("quotes families the canvas font shorthand would otherwise reject", () => {    expect(terminalFontFamily("3270 Nerd Font").startsWith('"3270 Nerd Font", ')).toBe(true);    expect(terminalFontFamily("M+ 1m").startsWith('"M+ 1m", ')).toBe(true);    expect(terminalFontFamily("Cascadia Code, Menlo").startsWith('"Cascadia Code", Menlo, ')).toBe(      true,    );    expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY);  });   it("clamps requested font sizes to the supported range", () => {    expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE);    expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE);    expect(terminalFontSize(13.4)).toBe(13);    expect(terminalFontSize(2)).toBe(6);    expect(terminalFontSize(90)).toBe(32);  });}); describe("terminalContentOriginY", () => {  it("stays top-anchored like a fresh terminal until scrollback exists", () => {

Lines 1–500 of 638.