89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|||
|
|
import { renderHook } from "@testing-library/react";
|
||
|
|
import { useKeyboardShortcuts } from "./useKeyboardShortcuts";
|
||
|
|
import { useAppState, homeTabKey, terminalTabKey } from "../store/appState";
|
||
|
|
|
||
|
|
vi.mock("./useTerminal", () => ({
|
||
|
|
useTerminal: () => ({ open: vi.fn(), close: vi.fn() }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const HOME = homeTabKey("p1");
|
||
|
|
const S1 = terminalTabKey("s1");
|
||
|
|
const S2 = terminalTabKey("s2");
|
||
|
|
|
||
|
|
const order = () => useAppState.getState().tabOrder;
|
||
|
|
|
||
|
|
/** Press a chord, from whatever is focused. */
|
||
|
|
function press(key: string, { shift = false } = {}) {
|
||
|
|
document.dispatchEvent(
|
||
|
|
new KeyboardEvent("keydown", { key, ctrlKey: true, shiftKey: shift, bubbles: true }),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Focus a real element of the given kind, inside `parent` if given. */
|
||
|
|
function focus(tag: "input" | "textarea", parentClass?: string): HTMLElement {
|
||
|
|
const el = document.createElement(tag);
|
||
|
|
if (parentClass) {
|
||
|
|
const parent = document.createElement("div");
|
||
|
|
parent.className = parentClass;
|
||
|
|
parent.appendChild(el);
|
||
|
|
document.body.appendChild(parent);
|
||
|
|
} else {
|
||
|
|
document.body.appendChild(el);
|
||
|
|
}
|
||
|
|
el.focus();
|
||
|
|
return el;
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
useAppState.setState({ tabOrder: [HOME, S1, S2], activeTabKey: S1, activeSessionId: "s1" });
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
document.body.innerHTML = "";
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("Ctrl+Shift+←/→", () => {
|
||
|
|
it("moves the active tab along the strip", () => {
|
||
|
|
renderHook(() => useKeyboardShortcuts());
|
||
|
|
|
||
|
|
press("ArrowLeft", { shift: true });
|
||
|
|
expect(order()).toEqual([S1, HOME, S2]);
|
||
|
|
|
||
|
|
press("ArrowRight", { shift: true });
|
||
|
|
expect(order()).toEqual([HOME, S1, S2]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("leaves word-wise selection alone in a text field", () => {
|
||
|
|
// Ctrl+Shift+←/→ already means "extend the selection by a word" in every
|
||
|
|
// input in the app — the rename field, Config fields, Settings fields.
|
||
|
|
// Taking it there would break selection *and* silently reorder tabs.
|
||
|
|
renderHook(() => useKeyboardShortcuts());
|
||
|
|
focus("input");
|
||
|
|
|
||
|
|
press("ArrowLeft", { shift: true });
|
||
|
|
|
||
|
|
expect(order()).toEqual([HOME, S1, S2]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("still moves tabs from the terminal, whose focus lives in a textarea", () => {
|
||
|
|
// xterm keeps a hidden textarea focused as its input-method shim. It is
|
||
|
|
// not a field anyone edits word-wise, and the terminal is where these
|
||
|
|
// shortcuts matter most, so it is not treated as one.
|
||
|
|
renderHook(() => useKeyboardShortcuts());
|
||
|
|
focus("textarea", "xterm xterm-helper-textarea-host");
|
||
|
|
|
||
|
|
press("ArrowLeft", { shift: true });
|
||
|
|
|
||
|
|
expect(order()).toEqual([S1, HOME, S2]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does nothing without Shift — that chord is readline's word motion", () => {
|
||
|
|
renderHook(() => useKeyboardShortcuts());
|
||
|
|
|
||
|
|
press("ArrowLeft");
|
||
|
|
|
||
|
|
expect(order()).toEqual([HOME, S1, S2]);
|
||
|
|
});
|
||
|
|
});
|