Files
NetMesh/components/terminal/TerminalContextMenu.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

492 lines
15 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import en from "../../application/i18n/locales/en.ts";
import ru from "../../application/i18n/locales/ru.ts";
import es from "../../application/i18n/locales/es.ts";
import zhCN from "../../application/i18n/locales/zh-CN.ts";
import { markMiddleClickContextMenuEvent } from "./runtime/middleClickBehavior.ts";
import * as terminalContextMenu from "./TerminalContextMenu.tsx";
import { shouldEnableYmodemAction } from "./TerminalView.tsx";
const shouldShowReconnectAction = (
terminalContextMenu as {
shouldShowReconnectAction?: (options: {
isReconnectable?: boolean;
onReconnect?: () => void;
}) => boolean;
}
).shouldShowReconnectAction;
const shouldSuppressMouseTrackingContextMenu = (
terminalContextMenu as {
shouldSuppressMouseTrackingContextMenu?: (options: {
isAlternateScreen?: boolean;
terminalMouseTrackingMode?: string;
showReconnectAction?: boolean;
forceMenuInAlternateScreen?: boolean;
}) => boolean;
}
).shouldSuppressMouseTrackingContextMenu;
const shouldShowAddSelectionToAIContextMenuAction = (
terminalContextMenu as {
shouldShowAddSelectionToAIContextMenuAction?: (onAddSelectionToAI?: () => void) => boolean;
}
).shouldShowAddSelectionToAIContextMenuAction;
const shouldShowUploadClipboardImageContextMenuAction = (
terminalContextMenu as {
shouldShowUploadClipboardImageContextMenuAction?: (onUploadClipboardImage?: () => void) => boolean;
}
).shouldShowUploadClipboardImageContextMenuAction;
const shouldOpenTerminalContextMenu = (
terminalContextMenu as {
shouldOpenTerminalContextMenu?: (options: {
event: { shiftKey?: boolean; nativeEvent: MouseEvent };
rightClickBehavior?: "context-menu" | "paste" | "select-word";
isAlternateScreen?: boolean;
terminalMouseTrackingMode?: string;
showReconnectAction?: boolean;
forceMenuInAlternateScreen?: boolean;
}) => boolean;
}
).shouldOpenTerminalContextMenu;
const shouldRenderTerminalContextMenuContent = (
terminalContextMenu as {
shouldRenderTerminalContextMenuContent?: (options: {
isAlternateScreen?: boolean;
terminalMouseTrackingMode?: string;
showReconnectAction?: boolean;
allowSuppressedMenuContent?: boolean;
}) => boolean;
}
).shouldRenderTerminalContextMenuContent;
const shouldAllowSuppressedTerminalContextMenuContent = (
terminalContextMenu as {
shouldAllowSuppressedTerminalContextMenuContent?: (options: {
event: { shiftKey?: boolean; nativeEvent: MouseEvent };
isAlternateScreen?: boolean;
terminalMouseTrackingMode?: string;
showReconnectAction?: boolean;
}) => boolean;
}
).shouldAllowSuppressedTerminalContextMenuContent;
test("shows reconnect only for reconnectable terminals with a handler", () => {
assert.equal(typeof shouldShowReconnectAction, "function");
if (typeof shouldShowReconnectAction !== "function") return;
assert.equal(
shouldShowReconnectAction({
isReconnectable: true,
onReconnect: () => {},
}),
true,
);
assert.equal(
shouldShowReconnectAction({
isReconnectable: false,
onReconnect: () => {},
}),
false,
);
assert.equal(shouldShowReconnectAction({ isReconnectable: true }), false);
});
test("localizes the reconnect context menu label", () => {
assert.equal(en["terminal.menu.reconnect"], "Reconnect");
assert.equal(zhCN["terminal.menu.reconnect"], "重新连接");
});
test("shows add selection to AI context menu action when a handler exists", () => {
assert.equal(typeof shouldShowAddSelectionToAIContextMenuAction, "function");
if (typeof shouldShowAddSelectionToAIContextMenuAction !== "function") return;
assert.equal(shouldShowAddSelectionToAIContextMenuAction(() => {}), true);
assert.equal(shouldShowAddSelectionToAIContextMenuAction(), false);
});
test("shows upload clipboard image context menu action when a handler exists", () => {
assert.equal(typeof shouldShowUploadClipboardImageContextMenuAction, "function");
if (typeof shouldShowUploadClipboardImageContextMenuAction !== "function") return;
assert.equal(shouldShowUploadClipboardImageContextMenuAction(() => {}), true);
assert.equal(shouldShowUploadClipboardImageContextMenuAction(), false);
});
test("localizes the upload clipboard image context menu label", () => {
const locales = { en, ru, es, "zh-CN": zhCN };
const keys = [
"terminal.menu.uploadClipboardImage",
"terminal.clipboardImageUpload.noImage",
"terminal.clipboardImageUpload.failed",
] as const;
for (const [locale, messages] of Object.entries(locales)) {
for (const key of keys) {
assert.equal(
typeof messages[key],
"string",
`${locale} should include ${key}`,
);
assert.notEqual(messages[key], "", `${locale} should not leave ${key} empty`);
assert.notEqual(messages[key], key, `${locale} should translate ${key}`);
}
}
assert.equal(en["terminal.menu.uploadClipboardImage"], "Upload clipboard image");
assert.equal(zhCN["terminal.menu.uploadClipboardImage"], "上传剪贴板图片");
assert.equal(ru["terminal.menu.uploadClipboardImage"], "Загрузить изображение из буфера");
});
test("localizes the YMODEM serial send actions", () => {
assert.equal(en["terminal.menu.sendYmodem"], "Send with YMODEM");
assert.equal(en["terminal.menu.receiveYmodem"], "Receive with YMODEM");
assert.equal(en["terminal.toolbar.sendYmodem"], "Send with YMODEM");
assert.equal(en["terminal.toolbar.receiveYmodem"], "Receive with YMODEM");
assert.equal(zhCN["terminal.menu.sendYmodem"], "YMODEM 发送");
assert.equal(zhCN["terminal.menu.receiveYmodem"], "YMODEM 接收");
assert.equal(zhCN["terminal.toolbar.sendYmodem"], "YMODEM 发送");
assert.equal(zhCN["terminal.toolbar.receiveYmodem"], "YMODEM 接收");
});
test("enables YMODEM action only for connected serial terminals", () => {
const handler = () => {};
assert.equal(shouldEnableYmodemAction({
isSerialConnection: true,
status: "connected",
handleSendYmodem: handler,
}), true);
assert.equal(shouldEnableYmodemAction({
isSerialConnection: true,
status: "connected",
handleReceiveYmodem: handler,
}), true);
assert.equal(shouldEnableYmodemAction({
isSerialConnection: true,
status: "disconnected",
handleReceiveYmodem: handler,
}), false);
assert.equal(shouldEnableYmodemAction({
isSerialConnection: true,
status: "disconnected",
handleSendYmodem: handler,
}), false);
assert.equal(shouldEnableYmodemAction({
isSerialConnection: false,
status: "connected",
handleSendYmodem: handler,
}), false);
assert.equal(shouldEnableYmodemAction({
isSerialConnection: true,
status: "connected",
}), false);
});
test("allows reconnect menu while stale mouse tracking is still active", () => {
assert.equal(typeof shouldSuppressMouseTrackingContextMenu, "function");
if (typeof shouldSuppressMouseTrackingContextMenu !== "function") return;
assert.equal(
shouldSuppressMouseTrackingContextMenu({
isAlternateScreen: true,
showReconnectAction: true,
}),
false,
);
assert.equal(
shouldSuppressMouseTrackingContextMenu({
isAlternateScreen: true,
showReconnectAction: false,
}),
true,
);
});
test("forceMenuInAlternateScreen opts out of alternate-screen suppression", () => {
assert.equal(typeof shouldSuppressMouseTrackingContextMenu, "function");
assert.equal(typeof shouldOpenTerminalContextMenu, "function");
if (
typeof shouldSuppressMouseTrackingContextMenu !== "function" ||
typeof shouldOpenTerminalContextMenu !== "function"
) {
return;
}
// Setting on: no suppression, right-click opens the app menu in tmux/vim.
assert.equal(
shouldSuppressMouseTrackingContextMenu({
isAlternateScreen: true,
showReconnectAction: false,
forceMenuInAlternateScreen: true,
}),
false,
);
assert.equal(
shouldOpenTerminalContextMenu({
event: { shiftKey: false, nativeEvent: {} as MouseEvent },
rightClickBehavior: "context-menu",
isAlternateScreen: true,
showReconnectAction: false,
forceMenuInAlternateScreen: true,
}),
true,
);
// Setting off (default): alternate screen still suppresses the menu.
assert.equal(
shouldSuppressMouseTrackingContextMenu({
isAlternateScreen: true,
showReconnectAction: false,
forceMenuInAlternateScreen: false,
}),
true,
);
assert.equal(
shouldSuppressMouseTrackingContextMenu({
isAlternateScreen: true,
showReconnectAction: false,
forceMenuInAlternateScreen: false,
isHistoryPreviewTarget: true,
}),
false,
);
assert.equal(
shouldOpenTerminalContextMenu({
event: { shiftKey: false, nativeEvent: {} as MouseEvent },
rightClickBehavior: "paste",
isAlternateScreen: true,
showReconnectAction: false,
isHistoryPreviewTarget: true,
}),
true,
);
});
test("opens a middle-click menu even when right-click is configured to paste", () => {
assert.equal(typeof shouldOpenTerminalContextMenu, "function");
if (typeof shouldOpenTerminalContextMenu !== "function") return;
assert.equal(
shouldOpenTerminalContextMenu({
event: {
shiftKey: false,
nativeEvent: markMiddleClickContextMenuEvent({} as MouseEvent),
},
rightClickBehavior: "paste",
}),
true,
);
assert.equal(
shouldOpenTerminalContextMenu({
event: {
shiftKey: false,
nativeEvent: {} as MouseEvent,
},
rightClickBehavior: "paste",
}),
false,
);
});
test("opens and renders middle-click menu while alternate-screen mouse tracking suppresses right-click menus", () => {
assert.equal(typeof shouldOpenTerminalContextMenu, "function");
assert.equal(typeof shouldRenderTerminalContextMenuContent, "function");
assert.equal(typeof shouldAllowSuppressedTerminalContextMenuContent, "function");
if (
typeof shouldOpenTerminalContextMenu !== "function" ||
typeof shouldRenderTerminalContextMenuContent !== "function" ||
typeof shouldAllowSuppressedTerminalContextMenuContent !== "function"
) {
return;
}
const middleClickEvent = {
shiftKey: false,
nativeEvent: markMiddleClickContextMenuEvent({} as MouseEvent),
};
assert.equal(
shouldOpenTerminalContextMenu({
event: middleClickEvent,
rightClickBehavior: "paste",
isAlternateScreen: true,
showReconnectAction: false,
}),
true,
);
const allowSuppressedMenuContent = shouldAllowSuppressedTerminalContextMenuContent({
event: middleClickEvent,
isAlternateScreen: true,
showReconnectAction: false,
});
assert.equal(allowSuppressedMenuContent, true);
assert.equal(
shouldRenderTerminalContextMenuContent({
isAlternateScreen: true,
showReconnectAction: false,
allowSuppressedMenuContent,
}),
true,
);
assert.equal(
shouldOpenTerminalContextMenu({
event: {
shiftKey: false,
nativeEvent: {} as MouseEvent,
},
rightClickBehavior: "context-menu",
isAlternateScreen: true,
showReconnectAction: false,
}),
false,
);
assert.equal(
shouldAllowSuppressedTerminalContextMenuContent({
event: {
nativeEvent: {} as MouseEvent,
},
isAlternateScreen: true,
showReconnectAction: false,
}),
false,
);
assert.equal(
shouldRenderTerminalContextMenuContent({
isAlternateScreen: true,
showReconnectAction: false,
allowSuppressedMenuContent: false,
}),
false,
);
});
test("uses the current mouse tracking mode when the cached state is stale", () => {
assert.equal(typeof shouldOpenTerminalContextMenu, "function");
assert.equal(typeof shouldRenderTerminalContextMenuContent, "function");
if (
typeof shouldOpenTerminalContextMenu !== "function" ||
typeof shouldRenderTerminalContextMenuContent !== "function"
) {
return;
}
const event = {
nativeEvent: {} as MouseEvent,
};
// xterm has already stopped reporting mouse events, but React still has
// the previous tracking state: paste/select-word must not be dropped.
assert.equal(
shouldOpenTerminalContextMenu({
event,
rightClickBehavior: "paste",
isAlternateScreen: true,
terminalMouseTrackingMode: "none",
showReconnectAction: false,
}),
false,
);
assert.equal(
shouldRenderTerminalContextMenuContent({
isAlternateScreen: true,
terminalMouseTrackingMode: "none",
showReconnectAction: false,
allowSuppressedMenuContent: false,
}),
true,
);
// Conversely, a newly active xterm mode must still suppress the app menu
// while the cached React state has not caught up.
assert.equal(
shouldOpenTerminalContextMenu({
event,
rightClickBehavior: "context-menu",
isAlternateScreen: false,
terminalMouseTrackingMode: "vt200",
showReconnectAction: false,
}),
false,
);
assert.equal(
shouldRenderTerminalContextMenuContent({
isAlternateScreen: false,
terminalMouseTrackingMode: "vt200",
showReconnectAction: false,
allowSuppressedMenuContent: false,
}),
false,
);
});
test("opens Shift right-click menu content for all right-click modes while mouse tracking suppresses unmodified menus", () => {
assert.equal(typeof shouldOpenTerminalContextMenu, "function");
assert.equal(typeof shouldRenderTerminalContextMenuContent, "function");
assert.equal(typeof shouldAllowSuppressedTerminalContextMenuContent, "function");
if (
typeof shouldOpenTerminalContextMenu !== "function" ||
typeof shouldRenderTerminalContextMenuContent !== "function" ||
typeof shouldAllowSuppressedTerminalContextMenuContent !== "function"
) {
return;
}
const event = {
shiftKey: true,
nativeEvent: {} as MouseEvent,
};
for (const rightClickBehavior of ["context-menu", "paste", "select-word"] as const) {
assert.equal(
shouldOpenTerminalContextMenu({
event,
rightClickBehavior,
isAlternateScreen: true,
showReconnectAction: false,
}),
true,
);
const allowSuppressedMenuContent = shouldAllowSuppressedTerminalContextMenuContent({
event,
isAlternateScreen: true,
showReconnectAction: false,
});
assert.equal(allowSuppressedMenuContent, true);
assert.equal(
shouldRenderTerminalContextMenuContent({
isAlternateScreen: true,
showReconnectAction: false,
allowSuppressedMenuContent,
}),
true,
);
}
assert.equal(
shouldOpenTerminalContextMenu({
event: {
nativeEvent: {} as MouseEvent,
},
rightClickBehavior: "context-menu",
isAlternateScreen: true,
showReconnectAction: false,
}),
false,
);
assert.equal(
shouldAllowSuppressedTerminalContextMenuContent({
event: {
nativeEvent: {} as MouseEvent,
},
isAlternateScreen: true,
showReconnectAction: false,
}),
false,
);
});