Public: anyone can read this; the crew can change it.

j/k, enter, esc and ? on settings/devices, with test/keys-<page>.test.ts #10808

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/10797-j-k-enter-esc-and-on-settings-devices-wi → main 1 commit fork

  • Merged. mason/10797-j-k-enter-esc-and-on-settings-devices-wi was merged into main as 91c744a.

Review

Sign in to review this change.
1 file changed +91 −0
test/keys-devices.test.ts added +91
@@ -0,0 +1,91 @@
1 /**
2 * The real /settings/devices page and the shell's keys. Its j/k rows are the
3 * settings sections (chrome `settingsLayout`); a declared printer's card holds
4 * only forms (Remove, Grant, Set limit), has nowhere to open, so it is not a row.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { declareDevice } from "../src/core/devices.ts";
12 import { serve, type Servers } from "../src/http/server.ts";
13 import { adminCookie } from "./helpers/admin.ts";
14 import { quietLogs } from "./helpers/globals.ts";
15 import { mainOf, shellPage } from "./helpers/shell-page.ts";
16
17 quietLogs();
18
19 let dir = "";
20 let app: App;
21 let servers: Servers;
22 let html = "";
23 const priorLoopback = process.env.FORGE_DEVICES_ALLOW_LOOPBACK;
24
25 beforeAll(async () => {
26 process.env.FORGE_DEVICES_ALLOW_LOOPBACK = "1";
27 dir = await mkdtemp(join(tmpdir(), "forge-keys-devices-"));
28 app = createApp({ dataDir: dir, port: 0 });
29 servers = await serve(app);
30 const cookie = await adminCookie(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
31 const dead = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("x") });
32 const url = `ipp://127.0.0.1:${dead.port}/ipp/print`;
33 dead.stop(true);
34 const made = declareDevice(app.db, { name: "Hall printer", kind: "printer", url });
35 if (!made.ok) throw new Error(made.error);
36 const res = await fetch(`${servers.url}/settings/devices`, { headers: { cookie } });
37 html = mainOf(await res.text());
38 });
39 afterAll(async () => {
40 if (priorLoopback === undefined) delete process.env.FORGE_DEVICES_ALLOW_LOOPBACK;
41 else process.env.FORGE_DEVICES_ALLOW_LOOPBACK = priorLoopback;
42 servers.stop();
43 app.close();
44 await rm(dir, { recursive: true, force: true });
45 });
46
47 test("the page draws its device and the sections are the only rows; the card is not a row", () => {
48 expect(html).toContain("Hall printer");
49 const card = html.slice(html.indexOf('class="list setting-list"'));
50 expect(card).not.toContain("data-nav");
51 expect(html).toContain("data-nav");
52 });
53
54 test("j/k walk the sections from the current one, Enter opens, Esc leaves nothing behind, fields keep their keys", async () => {
55 const p = await shellPage("/settings/devices", html + "<input id=f>");
56 const rows = [...p.doc.querySelectorAll("[data-nav]")] as any[];
57 const at = rows.findIndex((r) => r.getAttribute("aria-current") === "page");
58 expect(at).toBeGreaterThanOrEqual(0);
59 expect(rows[at].getAttribute("href")).toBe("/settings/devices");
60
61 // Nothing focused: j steps away from the row the page is on.
62 p.key("j");
63 expect(p.doc.activeElement).toBe(rows[Math.min(at + 1, rows.length - 1)]);
64 const target = p.doc.activeElement.getAttribute("href");
65 p.key("k");
66 expect(p.doc.activeElement).toBe(rows[Math.max(at, 0)]);
67 p.key("j");
68 p.key("Enter");
69 expect(p.clicks).toEqual([target]);
70 expect(JSON.parse(p.win.sessionStorage.getItem("nav-from")!)).toMatchObject({ list: "/settings/devices", href: target });
71
72 // In a field no key moves or opens.
73 const f = p.doc.getElementById("f") as any;
74 f.focus();
75 const before = p.clicks.length;
76 p.key("j", f);
77 p.key("Enter", f);
78 expect(p.doc.activeElement).toBe(f);
79 expect(p.clicks.length).toBe(before);
80
81 // The page that carries the sections is itself a list: Esc has nowhere to return to.
82 const same = await shellPage("/settings/devices", html, { "nav-from": JSON.stringify({ list: "/settings/devices", to: "/settings/devices", href: target }) });
83 same.key("Escape");
84 expect(same.assigned).toEqual([]);
85 });
86
87 test("`?` opens the help from this page", async () => {
88 const p = await shellPage("/settings/devices", html);
89 const e = p.key("?");
90 expect(e.defaultPrevented).toBe(true);
91 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs