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

The forges list is keyboard-first: j/k move between rows, enter opens one #10369

Merged opened this

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

Merged

torque/forge:torque/10354-the-forges-list-is-keyboard-first-j-k-mo → main 6 commits fork 2 threads · 2 unresolved

  • Merged. torque/10354-the-forges-list-is-keyboard-first-j-k-mo was merged into main as 379c60c.

Review

Sign in to review this change.
2 files changed +71 −0
src/web/pages/forges.ts +2
@@ -62,6 +62,8 @@ function forgeRow(r: ClientRow): Html {
6262 return row({
6363 title: html`<a href="${r.url}">${r.name}</a>`,
6464 meta: html`${meta.map((m, i) => html`${i > 0 ? " · " : ""}${m}`)}`,
65 // Enter opens the row's first link, the forge's name.
66 nav: true,

Shorten the comment above this line to one line.

6567 data: { forge: r.name, claimed: state },
6668 ...(r.reach === null
6769 ? {}
test/keys-forges-list.test.ts added +69
@@ -0,0 +1,69 @@
1 /** The forges list joins the shell's j/k row traversal (#10354): one data-nav per forge, none on the empty page. */
2 import { afterAll, expect, test } from "bun:test";
3 import { mkdir, mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createToken } from "../src/core/tokens.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { useChildFetch } from "../src/spawn/clients.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11
12 let dirs: string[] = [];
13 const apps: App[] = [];
14 const servers: Servers[] = [];
15
16 async function boot(names: string[]): Promise<{ url: string; bearer: string; app: App }> {
17 const dataDir = await mkdtemp(join(tmpdir(), "forge-keys-forges-"));
18 dirs.push(dataDir);
19 const app = createApp({ dataDir, port: 0 });
20 apps.push(app);
21 useChildFetch(app, async () => new Response("{}", { status: 404 }));
22 const s = await serve(app);
23 servers.push(s);
24 const made = await createUser(app.db, {
25 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
26 password: "a-good-passphrase", adminIfFirst: true,
27 });
28 if (!made.ok) throw new Error(made.error);
29 await mkdir(join(dataDir, "spawn"), { recursive: true });
30 for (const name of names)
31 await Bun.write(
32 join(dataDir, "spawn", `${name}.json`),
33 JSON.stringify({ name, bodyId: `body-${name}`, url: `https://${name}.example.test`, planHash: "x", done: false, forWhom: "Maria", apps: ["notes"] }),
34 );
35 return { url: s.url, bearer: createToken(app.db, made.value.id, "t", "admin").token, app };
36 }
37
38 afterAll(async () => {
39 for (const s of servers) await s.stop();
40 for (const a of apps) a.close();
41 for (const d of dirs) await rm(d, { recursive: true, force: true });
42 });
43
44 test("each forge row is one data-nav holding the link to that forge", async () => {
45 const { url, bearer } = await boot(["maria", "omar", "lena"]);
46 const res = await fetch(`${url}/forges`, { headers: { authorization: `Bearer ${bearer}` } });
47 expect(res.status).toBe(200);
48 const text = await res.text();
49 const rows = text.split("data-nav").slice(1);
50 expect(rows.length).toBe(3);
51 const names = ["maria", "omar", "lena"];
52 for (const name of names) {
53 const row = rows.find((r) => r.includes(`>${name}</a>`));
54 expect(row).toBeDefined();
55 // The row's first <a> is that forge's own link.
56 const first = /<a href="([^"]+)">([^<]*)<\/a>/.exec(row!);
57 expect(first![2]).toBe(name);
58 expect(first![1]).toContain(name);
59 }
60 });
61
62 test("the empty page draws no data-nav", async () => {
63 const { url, bearer } = await boot([]);
64 const res = await fetch(`${url}/forges`, { headers: { authorization: `Bearer ${bearer}` } });
65 expect(res.status).toBe(200);
66 const text = await res.text();
67 expect(text).toContain("No forges sent yet");
68 expect(text).not.toContain("data-nav");
69 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs