One entry per command — stable id, plain-language label, key chord, run() — replacing the ad-hoc keydown listeners in main.ts, catalog.ts, debug.ts and attach.ts and the parallel SHORTCUTS constant in help.ts. The `?` overlay now renders the registry, so a binding and its documentation cannot disagree. Keys are canonicalised the way the TUI's `InputKey::canonical` spells them, and chords resolve like its `ChordState`: hold while the sequence is a strict prefix, otherwise dispatch. That gives the web a leader, `ctrl-a` by default like `[tui].leader`, held as a partial chord and `preventDefault`ed so it stops being select-all — the escape hatch in docs/COMMAND-BAR.md needs somewhere to land (#149). The leader stays out of real form fields, where select-all is still what the operator wants. Same keys, same effects: plain single-key bindings remain deaf inside form fields and the terminal's hidden textarea, chorded ones do not. Refs #148 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import { keyCaps, publishedCommands } from "./commands.js";
|
|
import { closeHelp, groupedCommands, installHelpKeybind, isHelpOpen, openHelp } from "./help.js";
|
|
|
|
beforeEach(() => {
|
|
installHelpKeybind();
|
|
});
|
|
|
|
afterEach(() => {
|
|
closeHelp();
|
|
document.body.replaceChildren();
|
|
});
|
|
|
|
function press(key: string, init: KeyboardEventInit = {}): void {
|
|
window.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, ...init }));
|
|
}
|
|
|
|
function rows(): string[] {
|
|
return [...document.querySelectorAll(".help-row")].map((r) =>
|
|
[...r.querySelectorAll("kbd")].map((k) => k.textContent).join(" "),
|
|
);
|
|
}
|
|
|
|
describe("help overlay", () => {
|
|
it("renders exactly the commands the registry has bound, grouped", () => {
|
|
openHelp();
|
|
const groups = groupedCommands();
|
|
expect([...document.querySelectorAll(".help-group-title")].map((h) => h.textContent)).toEqual(
|
|
groups.map((g) => g.title),
|
|
);
|
|
expect(rows()).toEqual(
|
|
groups.flatMap((g) => g.items.map((c) => keyCaps(c.keys[0]).join(" "))),
|
|
);
|
|
expect([...document.querySelectorAll(".help-desc")].map((d) => d.textContent)).toEqual(
|
|
groups.flatMap((g) => g.items.map((c) => c.label)),
|
|
);
|
|
});
|
|
|
|
it("names each row by its command id, so a row can't drift from its command", () => {
|
|
openHelp();
|
|
expect([...document.querySelectorAll<HTMLElement>(".help-row")].map((r) => r.dataset.commandId))
|
|
.toEqual(publishedCommands().map((c) => c.id));
|
|
});
|
|
|
|
it("opens on ? and closes on a second ?", () => {
|
|
press("?", { shiftKey: true });
|
|
expect(isHelpOpen()).toBe(true);
|
|
press("?", { shiftKey: true });
|
|
expect(isHelpOpen()).toBe(false);
|
|
});
|
|
|
|
it("closes on Escape, on the close button, and on a click outside the panel", () => {
|
|
openHelp();
|
|
press("Escape");
|
|
expect(isHelpOpen()).toBe(false);
|
|
|
|
openHelp();
|
|
(document.querySelector(".help-close") as HTMLButtonElement).click();
|
|
expect(isHelpOpen()).toBe(false);
|
|
|
|
openHelp();
|
|
(document.querySelector(".help-overlay") as HTMLElement).dispatchEvent(
|
|
new MouseEvent("click", { bubbles: true }),
|
|
);
|
|
expect(isHelpOpen()).toBe(false);
|
|
});
|
|
|
|
it("stays open for a click inside the panel", () => {
|
|
openHelp();
|
|
(document.querySelector(".help-panel") as HTMLElement).dispatchEvent(
|
|
new MouseEvent("click", { bubbles: true }),
|
|
);
|
|
expect(isHelpOpen()).toBe(true);
|
|
});
|
|
|
|
it("opening twice leaves one overlay", () => {
|
|
openHelp();
|
|
openHelp();
|
|
expect(document.querySelectorAll(".help-overlay")).toHaveLength(1);
|
|
});
|
|
|
|
it("ignores ? while typing and when a non-shift modifier is held", () => {
|
|
const input = document.createElement("input");
|
|
document.body.appendChild(input);
|
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "?", bubbles: true, shiftKey: true }));
|
|
expect(isHelpOpen()).toBe(false);
|
|
|
|
press("?", { ctrlKey: true });
|
|
expect(isHelpOpen()).toBe(false);
|
|
});
|
|
|
|
it("installing the keybind twice still toggles once", () => {
|
|
installHelpKeybind();
|
|
press("?", { shiftKey: true });
|
|
expect(isHelpOpen()).toBe(true);
|
|
expect(document.querySelectorAll(".help-overlay")).toHaveLength(1);
|
|
});
|
|
});
|