mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 23:55:23 +00:00
feat: move the terminal fully into the ssh-terminal plugin (B9)
This commit is contained in:
1 parent
535126fd1b
commit
79d77678c9
309 files changed
+41789
-14958
No files matched your search
@@ -0,0 +1,332 @@
|
||||
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/ui", () => ({
|
||||
getCookie: (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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user