mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
333 lines
9.7 KiB
TypeScript
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");
|
|
});
|
|
});
|