Files
NetMesh/components/terminal/runtime/middleClickBehavior.test.ts

481 lines
14 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import {
createRightClickMouseTrackingPressClaim,
isMiddleClickContextMenuEvent,
isShiftSelectionReplayMouseEvent,
markMiddleClickContextMenuEvent,
markShiftSelectionReplayMouseEvent,
captureMiddleClickTerminalMouseEvent,
resolveMiddleClickBehavior,
shouldInterceptMouseTrackingContextMenu,
shouldReplayShiftMouseSelectionAsMacOption,
shouldStopRightClickMouseTrackingMouseUp,
shouldStopShiftRightClickMouseTrackingMouseDown,
} from "./middleClickBehavior";
test("resolveMiddleClickBehavior uses the explicit middle-click behavior", () => {
assert.equal(resolveMiddleClickBehavior({ middleClickBehavior: "context-menu" }), "context-menu");
assert.equal(resolveMiddleClickBehavior({ middleClickBehavior: "disabled" }), "disabled");
});
test("resolveMiddleClickBehavior ignores unsupported middle-click behavior values", () => {
assert.equal(
resolveMiddleClickBehavior({ middleClickBehavior: "select-word" as never }),
"paste",
);
});
test("resolveMiddleClickBehavior falls back to the legacy middle-click paste flag", () => {
assert.equal(resolveMiddleClickBehavior({ middleClickPaste: true }), "paste");
assert.equal(resolveMiddleClickBehavior({ middleClickPaste: false }), "disabled");
assert.equal(resolveMiddleClickBehavior(undefined), "paste");
});
test("middle-click context menu events are identifiable", () => {
const event = {} as MouseEvent;
assert.equal(isMiddleClickContextMenuEvent(event), false);
assert.equal(isMiddleClickContextMenuEvent(markMiddleClickContextMenuEvent(event)), true);
});
test("mouse-tracking context menu capture lets middle-click menu events pass through", () => {
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: markMiddleClickContextMenuEvent({} as MouseEvent),
mouseTracking: true,
status: "connected",
}),
false,
);
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: {} as MouseEvent,
mouseTracking: true,
status: "connected",
}),
true,
);
});
test("mouse-tracking context menu capture lets Shift-modified mouse events pass through", () => {
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: true } as MouseEvent,
mouseTracking: true,
status: "connected",
}),
false,
);
});
test("mouse-tracking context menu capture prefers the terminal's current mode over stale cached state", () => {
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: false } as MouseEvent,
mouseTracking: false,
terminalMouseTrackingMode: "vt200",
status: "connected",
}),
true,
);
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: false } as MouseEvent,
mouseTracking: true,
terminalMouseTrackingMode: "none",
status: "connected",
}),
false,
);
});
test("mouse-tracking context menu capture yields to the fullscreen-apps menu setting for context-menu clicks", () => {
// Setting on + context-menu behavior: do NOT intercept, so Radix opens the menu.
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: true,
}),
false,
);
// Setting on but paste behavior: still intercept (setting is menu-only).
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "paste",
forceMenuInAlternateScreen: true,
}),
true,
);
// Setting off (default): still intercept even for context-menu behavior.
assert.equal(
shouldInterceptMouseTrackingContextMenu({
event: { shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: false,
}),
true,
);
});
test("Shift selection replay events are identifiable", () => {
const event = {} as MouseEvent;
assert.equal(isShiftSelectionReplayMouseEvent(event), false);
assert.equal(isShiftSelectionReplayMouseEvent(markShiftSelectionReplayMouseEvent(event)), true);
});
test("macOS mouse tracking replays plain Shift left-click as xterm option selection", () => {
const event = {
button: 0,
shiftKey: true,
altKey: false,
ctrlKey: false,
metaKey: false,
} as MouseEvent;
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event,
mouseTracking: true,
status: "connected",
isMacPlatform: true,
}),
true,
);
});
test("Shift selection replay is limited to the macOS connected mouse-tracking case", () => {
const baseEvent = {
button: 0,
shiftKey: true,
altKey: false,
ctrlKey: false,
metaKey: false,
} as MouseEvent;
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: baseEvent,
mouseTracking: true,
status: "connected",
isMacPlatform: false,
}),
false,
);
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: baseEvent,
mouseTracking: false,
status: "connected",
isMacPlatform: true,
}),
false,
);
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: baseEvent,
mouseTracking: true,
status: "disconnected",
isMacPlatform: true,
}),
false,
);
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: { ...baseEvent, button: 2 } as MouseEvent,
mouseTracking: true,
status: "connected",
isMacPlatform: true,
}),
false,
);
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: { ...baseEvent, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
isMacPlatform: true,
}),
false,
);
});
test("Shift selection replay ignores modified and already replayed mouse events", () => {
const baseEvent = {
button: 0,
shiftKey: true,
altKey: false,
ctrlKey: false,
metaKey: false,
} as MouseEvent;
for (const event of [
{ ...baseEvent, altKey: true },
{ ...baseEvent, ctrlKey: true },
{ ...baseEvent, metaKey: true },
markShiftSelectionReplayMouseEvent({ ...baseEvent } as MouseEvent),
]) {
assert.equal(
shouldReplayShiftMouseSelectionAsMacOption({
event: event as MouseEvent,
mouseTracking: true,
status: "connected",
isMacPlatform: true,
}),
false,
);
}
});
test("Shift right-click mousedown is stopped while connected mouse tracking is active", () => {
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: {
button: 2,
shiftKey: true,
} as MouseEvent,
mouseTracking: true,
status: "connected",
}),
true,
);
});
test("right-click mousedown is stopped when the fullscreen-apps menu setting forces the context menu", () => {
// Unmodified right-click + setting on + context-menu behavior: stop it, like Shift+right-click.
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: true,
}),
true,
);
// Setting on but paste behavior: do not stop (menu-only setting).
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "paste",
forceMenuInAlternateScreen: true,
}),
false,
);
});
test("right-click mousedown also uses the terminal's current mode", () => {
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: false,
terminalMouseTrackingMode: "vt200",
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: true,
}),
true,
);
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
terminalMouseTrackingMode: "none",
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: true,
}),
false,
);
});
test("Shift right-click mousedown capture is limited to connected mouse tracking", () => {
const baseEvent = {
button: 2,
shiftKey: true,
} as MouseEvent;
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: baseEvent,
mouseTracking: false,
status: "connected",
}),
false,
);
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: baseEvent,
mouseTracking: true,
status: "disconnected",
}),
false,
);
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: {
button: 2,
shiftKey: false,
} as MouseEvent,
mouseTracking: true,
status: "connected",
}),
false,
);
assert.equal(
shouldStopShiftRightClickMouseTrackingMouseDown({
event: {
button: 0,
shiftKey: true,
} as MouseEvent,
mouseTracking: true,
status: "connected",
}),
false,
);
});
test("right-click mouseup reaches mouse-tracking apps when Netcatty did not claim the press", () => {
// Herdr / Terminal.app: button-down was delivered, so button-up must be too.
// Swallowing mouseup leaves the TUI stuck thinking the right button is held (#2721).
const claim = createRightClickMouseTrackingPressClaim();
assert.equal(
claim.noteMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: false,
}),
false,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 2, shiftKey: false } as MouseEvent,
claimedMatchingMouseDown: claim.consumeMouseUpClaim({ button: 2 } as MouseEvent),
}),
false,
);
});
test("right-click mouseup is stopped only when Netcatty claimed the matching mousedown", () => {
const shiftClaim = createRightClickMouseTrackingPressClaim();
assert.equal(
shiftClaim.noteMouseDown({
event: { button: 2, shiftKey: true } as MouseEvent,
mouseTracking: true,
status: "connected",
}),
true,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 2, shiftKey: true } as MouseEvent,
claimedMatchingMouseDown: shiftClaim.consumeMouseUpClaim({ button: 2 } as MouseEvent),
}),
true,
);
const forceMenuClaim = createRightClickMouseTrackingPressClaim();
assert.equal(
forceMenuClaim.noteMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
rightClickBehavior: "context-menu",
forceMenuInAlternateScreen: true,
}),
true,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 2, shiftKey: false } as MouseEvent,
claimedMatchingMouseDown: forceMenuClaim.consumeMouseUpClaim({ button: 2 } as MouseEvent),
}),
true,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 0, shiftKey: false } as MouseEvent,
claimedMatchingMouseDown: false,
}),
false,
);
});
test("right-click mouseup pairs with the claimed mousedown, not the release modifiers", () => {
// Shift+right press claimed by Netcatty, then Shift released before mouseup:
// still swallow the release so xterm never sees a lone button-up.
const claimedThenShiftReleased = createRightClickMouseTrackingPressClaim();
assert.equal(
claimedThenShiftReleased.noteMouseDown({
event: { button: 2, shiftKey: true } as MouseEvent,
mouseTracking: true,
status: "connected",
}),
true,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 2, shiftKey: false } as MouseEvent,
claimedMatchingMouseDown: claimedThenShiftReleased.consumeMouseUpClaim(
{ button: 2, shiftKey: false } as MouseEvent,
),
}),
true,
);
// App-owned press, then Shift held on release: release must still reach xterm.
const appOwnedThenShiftAdded = createRightClickMouseTrackingPressClaim();
assert.equal(
appOwnedThenShiftAdded.noteMouseDown({
event: { button: 2, shiftKey: false } as MouseEvent,
mouseTracking: true,
status: "connected",
}),
false,
);
assert.equal(
shouldStopRightClickMouseTrackingMouseUp({
event: { button: 2, shiftKey: true } as MouseEvent,
claimedMatchingMouseDown: appOwnedThenShiftAdded.consumeMouseUpClaim(
{ button: 2, shiftKey: true } as MouseEvent,
),
}),
false,
);
});
test("middle-click terminal mouse down/up events are captured before xterm sees them", () => {
const calls: string[] = [];
const middleClickEvent = {
button: 1,
preventDefault: () => calls.push("preventDefault"),
stopImmediatePropagation: () => calls.push("stopImmediatePropagation"),
} as unknown as MouseEvent;
assert.equal(captureMiddleClickTerminalMouseEvent(middleClickEvent), true);
assert.deepEqual(calls, ["preventDefault", "stopImmediatePropagation"]);
calls.length = 0;
assert.equal(captureMiddleClickTerminalMouseEvent({
button: 0,
preventDefault: () => calls.push("preventDefault"),
stopImmediatePropagation: () => calls.push("stopImmediatePropagation"),
} as unknown as MouseEvent), false);
assert.deepEqual(calls, []);
});