import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { shouldClearTerminalSelectionAction, shouldHandleTerminalExit, terminalContextMenuItems, terminalSelectionLineRange, terminalSelectionMenuItems, terminalThemeFromApp, } from "./ThreadTerminalDrawer"; describe("terminal selection menus", () => { it("omits Add to chat when the terminal has no chat target", () => { expect(terminalSelectionMenuItems().map(({ id }) => id)).toEqual(["add-to-chat", "copy"]); expect(terminalContextMenuItems({ hasSelection: true }).map(({ id }) => id)).toEqual([ "add-to-chat", "copy", "paste", ]); expect(terminalSelectionMenuItems({ canAddToChat: false }).map(({ id }) => id)).toEqual([ "copy", ]); expect( terminalContextMenuItems({ hasSelection: true, canAddToChat: false }).map(({ id }) => id), ).toEqual(["copy", "paste"]); }); }); describe("terminalThemeFromApp", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("uses terminal colors inherited by the mount instead of a light document theme", () => { const root = { classList: { contains: () => false } }; const body = {}; const drawer = {}; let canvasColor = "#000"; const colors: Record = { "#000": [0, 0, 0, 255], "#fff": [255, 255, 255, 255], "#ddd": [221, 221, 221, 255], "#111": [17, 17, 17, 255], }; vi.stubGlobal("document", { documentElement: root, body, querySelector: () => drawer, createElement: () => ({ width: 0, height: 0, getContext: () => ({ clearRect: () => undefined, fillRect: () => undefined, get fillStyle() { return canvasColor; }, set fillStyle(value: string) { canvasColor = value; }, getImageData: () => ({ data: colors[canvasColor] ?? [0, 0, 0, 0] }), }), }), }); vi.stubGlobal("getComputedStyle", (element: object) => { const local = element === drawer; const values = local ? { "--terminal-background": "#000", "--terminal-foreground": "#fff", "--terminal-cursor": "#ddd", "--terminal-selection-background": "rgba(255, 255, 255, 0.2)", } : { "--terminal-background": "#fff", "--terminal-foreground": "#111", }; return { backgroundColor: local ? "#000" : "#fff", color: local ? "#fff" : "#111", colorScheme: local ? "dark" : "light", getPropertyValue: (name: string) => values[name as keyof typeof values] ?? "", }; }); const theme = terminalThemeFromApp(); expect(theme.background).toEqual({ r: 0, g: 0, b: 0 }); expect(theme.foreground).toEqual({ r: 255, g: 255, b: 255 }); expect(theme.cursor).toEqual({ r: 221, g: 221, b: 221 }); }); }); describe("terminal selection actions", () => { it("clears a pending or currently owned menu when the selection disappears", () => { expect( shouldClearTerminalSelectionAction({ actionPending: true, openMenuRequestId: null, currentRequestId: 4, }), ).toBe(true); expect( shouldClearTerminalSelectionAction({ actionPending: false, openMenuRequestId: 4, currentRequestId: 4, }), ).toBe(true); }); it("does not let an old selection popup cancel its replacement right-click menu", () => { expect( shouldClearTerminalSelectionAction({ actionPending: false, openMenuRequestId: 3, currentRequestId: 4, }), ).toBe(false); expect( shouldClearTerminalSelectionAction({ actionPending: false, openMenuRequestId: null, currentRequestId: 4, }), ).toBe(false); }); it("uses Ghostty's physical screen range for visually wrapped selections", () => { expect( terminalSelectionLineRange({ start: { y: 4 }, end: { y: 6 }, }), ).toEqual({ lineStart: 5, lineEnd: 7 }); }); it("handles an exit that lands while the terminal surface is still loading", () => { expect(shouldHandleTerminalExit("exited", "running", false)).toBe(true); expect(shouldHandleTerminalExit("exited", "exited", false)).toBe(false); expect(shouldHandleTerminalExit("closed", "running", true)).toBe(false); }); });