Files
Termix/plugins/ssh-terminal/tests/frontend/terminal/terminal-clipboard.test.ts
T

333 lines
9.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Terminal } from "@xterm/xterm";
const getCookieMock = vi.fn<(name: string) => string | undefined>();
vi.mock("@termix/plugin-sdk/frontend", () => ({
getClientPreference: (name: string) => getCookieMock(name),
}));
import {
getUseRightClickCopyPaste,
handleTerminalClipboardKeyEvent,
createTerminalContextMenuHandler,
} from "../../../src/frontend/terminal/terminal-clipboard";
function createFakeTerminal({ selection = "" }: { selection?: string } = {}) {
return {
hasSelection: vi.fn(() => selection.length > 0),
getSelection: vi.fn(() => selection),
clearSelection: vi.fn(),
paste: vi.fn(),
};
}
function createActions() {
return {
writeTextToClipboard: vi.fn().mockResolvedValue(true),
readTextFromClipboard: vi.fn().mockResolvedValue("clipboard-text"),
};
}
function createKeyEvent(init: {
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
key?: string;
code?: string;
}) {
return {
type: "keydown",
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
key: "",
code: "",
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
...init,
} as unknown as KeyboardEvent;
}
function createMouseEvent(init: { ctrlKey?: boolean } = {}) {
return {
ctrlKey: false,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
...init,
} as unknown as MouseEvent;
}
beforeEach(() => {
getCookieMock.mockReset();
getCookieMock.mockReturnValue(undefined);
});
describe("getUseRightClickCopyPaste", () => {
it("defaults to enabled when the cookie is unset", () => {
getCookieMock.mockReturnValue(undefined);
expect(getUseRightClickCopyPaste()).toBe(true);
});
it('is disabled only when the cookie is explicitly "false"', () => {
getCookieMock.mockReturnValue("false");
expect(getUseRightClickCopyPaste()).toBe(false);
getCookieMock.mockReturnValue("true");
expect(getUseRightClickCopyPaste()).toBe(true);
});
});
describe("handleTerminalClipboardKeyEvent", () => {
it("copies the selection on Ctrl+C and clears it", () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const e = createKeyEvent({ ctrlKey: true, code: "KeyC", key: "c" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(false);
expect(e.preventDefault).toHaveBeenCalled();
expect(e.stopPropagation).toHaveBeenCalled();
expect(actions.writeTextToClipboard).toHaveBeenCalledWith("hello");
expect(terminal.clearSelection).toHaveBeenCalled();
});
it("leaves Ctrl+C alone (SIGINT) when there is no selection", () => {
const terminal = createFakeTerminal({ selection: "" });
const actions = createActions();
const e = createKeyEvent({ ctrlKey: true, code: "KeyC", key: "c" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(true);
expect(e.preventDefault).not.toHaveBeenCalled();
expect(actions.writeTextToClipboard).not.toHaveBeenCalled();
});
it("copies via Ctrl+Insert without clearing the selection", () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const e = createKeyEvent({ ctrlKey: true, key: "Insert" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(false);
expect(actions.writeTextToClipboard).toHaveBeenCalledWith("hello");
expect(terminal.clearSelection).not.toHaveBeenCalled();
});
it("copies and clears the selection on Ctrl+Shift+C", () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const e = createKeyEvent({
ctrlKey: true,
shiftKey: true,
code: "KeyC",
key: "C",
});
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(false);
expect(actions.writeTextToClipboard).toHaveBeenCalledWith("hello");
expect(terminal.clearSelection).toHaveBeenCalled();
});
it("is a no-op for Ctrl+Shift+C without a selection", () => {
const terminal = createFakeTerminal({ selection: "" });
const actions = createActions();
const e = createKeyEvent({
ctrlKey: true,
shiftKey: true,
code: "KeyC",
key: "C",
});
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(true);
expect(actions.writeTextToClipboard).not.toHaveBeenCalled();
});
it("always pastes on Ctrl+Shift+V regardless of selection", async () => {
const terminal = createFakeTerminal({ selection: "irrelevant" });
const actions = createActions();
const e = createKeyEvent({
ctrlKey: true,
shiftKey: true,
code: "KeyV",
key: "V",
});
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
await Promise.resolve();
expect(handled).toBe(false);
expect(e.preventDefault).toHaveBeenCalled();
expect(actions.readTextFromClipboard).toHaveBeenCalled();
expect(terminal.paste).toHaveBeenCalledWith("clipboard-text");
});
it("leaves plain Ctrl+V to the browser's native paste event by default", () => {
const terminal = createFakeTerminal();
const actions = createActions();
const e = createKeyEvent({ ctrlKey: true, code: "KeyV", key: "v" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(false);
expect(e.preventDefault).not.toHaveBeenCalled();
expect(e.stopPropagation).not.toHaveBeenCalled();
expect(actions.readTextFromClipboard).not.toHaveBeenCalled();
});
it("reads the clipboard explicitly for plain Ctrl+V when configured", async () => {
const terminal = createFakeTerminal();
const actions = createActions();
const e = createKeyEvent({ ctrlKey: true, code: "KeyV", key: "v" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
{ plainPasteMode: "explicit" },
);
await Promise.resolve();
expect(handled).toBe(false);
expect(e.preventDefault).toHaveBeenCalled();
expect(actions.readTextFromClipboard).toHaveBeenCalled();
expect(terminal.paste).toHaveBeenCalledWith("clipboard-text");
});
it("ignores keys with no clipboard shortcut", () => {
const terminal = createFakeTerminal();
const actions = createActions();
const e = createKeyEvent({ key: "a", code: "KeyA" });
const handled = handleTerminalClipboardKeyEvent(
e,
terminal as unknown as Terminal,
actions,
);
expect(handled).toBe(true);
expect(actions.writeTextToClipboard).not.toHaveBeenCalled();
expect(actions.readTextFromClipboard).not.toHaveBeenCalled();
});
});
describe("createTerminalContextMenuHandler", () => {
it("routes Ctrl+right-click to the passthrough callback instead of the clipboard", () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const onCtrlClick = vi.fn();
const handler = createTerminalContextMenuHandler(
terminal as unknown as Terminal,
actions,
{ onCtrlClick },
);
const e = createMouseEvent({ ctrlKey: true });
handler(e);
expect(onCtrlClick).toHaveBeenCalledTimes(1);
expect(e.preventDefault).toHaveBeenCalled();
expect(actions.writeTextToClipboard).not.toHaveBeenCalled();
});
it("falls back to normal right-click behavior when Ctrl is held with no passthrough configured", () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const handler = createTerminalContextMenuHandler(
terminal as unknown as Terminal,
actions,
);
const e = createMouseEvent({ ctrlKey: true });
handler(e);
expect(actions.writeTextToClipboard).toHaveBeenCalledWith("hello");
});
it("does nothing when the right-click preference is disabled", () => {
getCookieMock.mockReturnValue("false");
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const handler = createTerminalContextMenuHandler(
terminal as unknown as Terminal,
actions,
);
const e = createMouseEvent();
handler(e);
expect(e.preventDefault).not.toHaveBeenCalled();
expect(actions.writeTextToClipboard).not.toHaveBeenCalled();
});
it("copies the current selection when right-click copy/paste is enabled", async () => {
const terminal = createFakeTerminal({ selection: "hello" });
const actions = createActions();
const handler = createTerminalContextMenuHandler(
terminal as unknown as Terminal,
actions,
);
const e = createMouseEvent();
handler(e);
await Promise.resolve();
expect(e.preventDefault).toHaveBeenCalled();
expect(actions.writeTextToClipboard).toHaveBeenCalledWith("hello");
expect(terminal.clearSelection).toHaveBeenCalled();
});
it("pastes when there is no selection", async () => {
const terminal = createFakeTerminal({ selection: "" });
const actions = createActions();
const handler = createTerminalContextMenuHandler(
terminal as unknown as Terminal,
actions,
);
const e = createMouseEvent();
handler(e);
await Promise.resolve();
expect(actions.readTextFromClipboard).toHaveBeenCalled();
expect(terminal.paste).toHaveBeenCalledWith("clipboard-text");
});
});