`?` toggles a dialog listing every web binding, mirroring the TUI's help overlay. A `?` button in the header covers pointer-only clients, and the welcome screen points at the key. One registry (SHORTCUTS) drives the rendering, so a binding added without an entry reads as missing. main.ts and the overlay now share one isTypingTarget guard instead of two copies.
113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
SHORTCUTS,
|
|
closeHelp,
|
|
installHelpKeybind,
|
|
isHelpOpen,
|
|
isTypingTarget,
|
|
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 every registered shortcut, grouped", () => {
|
|
openHelp();
|
|
const groups = [...document.querySelectorAll(".help-group-title")].map((h) => h.textContent);
|
|
expect(groups).toEqual(SHORTCUTS.map((g) => g.title));
|
|
expect(rows()).toEqual(SHORTCUTS.flatMap((g) => g.items.map((s) => s.keys.join(" "))));
|
|
expect(document.querySelectorAll(".help-desc")).toHaveLength(
|
|
SHORTCUTS.reduce((n, g) => n + g.items.length, 0),
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe("isTypingTarget", () => {
|
|
it("is true for form fields and editable nodes, false otherwise", () => {
|
|
const el = (html: string) => {
|
|
const d = document.createElement("div");
|
|
d.innerHTML = html;
|
|
return d.firstElementChild as HTMLElement;
|
|
};
|
|
expect(isTypingTarget(el("<input>"))).toBe(true);
|
|
expect(isTypingTarget(el("<textarea></textarea>"))).toBe(true);
|
|
expect(isTypingTarget(el("<select></select>"))).toBe(true);
|
|
expect(isTypingTarget(el('<div contenteditable="true"></div>'))).toBe(true);
|
|
expect(isTypingTarget(el("<button></button>"))).toBe(false);
|
|
expect(isTypingTarget(null)).toBe(false);
|
|
});
|
|
});
|