AkurAI Build
Menu

Public repository / 054e5cda0171bd13facff3c16a38f2048a1052cc

AkurAI-Mail / src/framework/shortcuts.test.js

Latest change c3ef99b092caa55fb6a3eed9d3e776734cc99e97 - OLI-241: framework migration plan; shortcuts slice onto AkurAI-Framework signals runtime by Ólafur Búi Ólafsson

// @ts-nocheck
import { describe, expect, test } from "bun:test";
import { resolveKey, moveSelection, SHORTCUT_HELP, createShortcuts, scrollSelectedIntoView } from "./shortcuts.js";

const ev = (key, over = {}) => ({ key, ctrlKey: false, metaKey: false, altKey: false, target: { tagName: "DIV", isContentEditable: false }, ...over });
const ctx = (over = {}) => ({ enabled: true, hasSelection: true, ...over });

describe("framework shortcuts: keymap (ported from OLI-239)", () => {
  test("maps each key to an action when a message is selected", () => {
    const map = { "?": "help", Escape: "close", c: "compose", "/": "search", j: "next", k: "prev", "#": "delete", Delete: "delete", r: "reply", R: "replyAll", f: "forward", e: "archive", s: "star", u: "unread", "!": "spam" };
    for (const [key, action] of Object.entries(map)) expect(resolveKey(ev(key), ctx())).toBe(action);
    expect(resolveKey(ev("z"), ctx())).toBeNull();
  });
  test("Enter opens the next message when nothing is selected, otherwise does nothing", () => {
    expect(resolveKey(ev("Enter"), ctx({ hasSelection: false }))).toBe("next");
    expect(resolveKey(ev("Enter"), ctx())).toBeNull();
    expect(resolveKey(ev("Enter"), ctx({ hasSelection: false, inList: true }))).toBeNull();
  });
  test("# deletes only with a selection", () => {
    expect(resolveKey(ev("#"), ctx())).toBe("delete");
    expect(resolveKey(ev("#"), ctx({ hasSelection: false }))).toBeNull();
  });
  test("arrow keys navigate only inside the list", () => {
    expect(resolveKey(ev("ArrowDown"), ctx())).toBeNull();
    expect(resolveKey(ev("ArrowDown"), ctx({ inList: true }))).toBe("next");
    expect(resolveKey(ev("ArrowUp"), ctx({ inList: true }))).toBe("prev");
  });
  test("ignored in inputs; Escape in an input closes and blurs", () => {
    for (const tagName of ["INPUT", "TEXTAREA", "SELECT"]) expect(resolveKey(ev("j", { target: { tagName } }), ctx())).toBeNull();
    expect(resolveKey(ev("j", { target: { tagName: "DIV", isContentEditable: true } }), ctx())).toBeNull();
    expect(resolveKey(ev("Escape", { target: { tagName: "INPUT" } }), ctx())).toBe("closeTyping");
  });
  test("modifiers ignored", () => {
    for (const m of ["ctrlKey", "metaKey", "altKey"]) expect(resolveKey(ev("r", { [m]: true }), ctx())).toBeNull();
  });
  test("disabled preference silences everything but Escape", () => {
    expect(resolveKey(ev("j"), ctx({ enabled: false }))).toBeNull();
    expect(resolveKey(ev("Escape"), ctx({ enabled: false }))).toBe("close");
  });
  test("moveSelection clamps", () => {
    expect(moveSelection(0, 5, "prev")).toBe(0);
    expect(moveSelection(4, 5, "next")).toBe(4);
    expect(moveSelection(-1, 5, "next")).toBe(0);
    expect(moveSelection(-1, 0, "next")).toBe(-1);
  });
  test("help lists bindings", () => { expect(SHORTCUT_HELP.length).toBeGreaterThanOrEqual(9); });
});

describe("framework shortcuts: signals-driven controller", () => {
  const fakeWindow = () => {
    const handlers = new Set();
    return { addEventListener: (_t, h) => handlers.add(h), removeEventListener: (_t, h) => handlers.delete(h), fire: (e) => [...handlers].forEach((h) => h(e)), handlers };
  };
  const mk = (over = {}) => {
    const win = fakeWindow(); const calls = []; const saved = [];
    const s = createShortcuts({ window: win, enabled: true, hasSelection: () => true, onAction: (a) => calls.push(a), persist: (v) => saved.push(v), ...over });
    return { win, calls, saved, s };
  };
  test("dispatches resolved actions (# deletes, Enter opens)", () => {
    const { win, calls } = mk({ hasSelection: () => false });
    win.fire({ ...ev("Enter"), preventDefault() {} });
    expect(calls).toEqual(["next"]);
    const t = mk();
    t.win.fire({ ...ev("#"), preventDefault() {} });
    expect(t.calls).toEqual(["delete"]);
  });
  test("toggle persists and detaches/attaches the listener", () => {
    const { win, calls, saved, s } = mk();
    s.setEnabled(false);
    expect(saved).toEqual([false]);
    win.fire({ ...ev("j"), preventDefault() {} });
    expect(calls).toEqual([]);
    s.setEnabled(true);
    expect(saved).toEqual([false, true]);
    win.fire({ ...ev("j"), preventDefault() {} });
    expect(calls).toEqual(["next"]);
  });
  test("destroy removes the listener", () => {
    const { win, s } = mk();
    s.destroy();
    expect(win.handlers.size).toBe(0);
  });
  test("selected row scrolls into view", () => {
    const seen = [];
    const root = { querySelector: (sel) => (sel === "[data-message-list] button.selected" ? { scrollIntoView: (o) => seen.push(o) } : null) };
    scrollSelectedIntoView(root);
    expect(seen).toEqual([{ block: "nearest" }]);
    expect(() => scrollSelectedIntoView({ querySelector: () => null })).not.toThrow();
  });
});