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
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/10354-the-forges-list-is-keyboard-first-j-k-mo → main 6 commits fork 2 threads · 2 unresolved
2 files
changed +71
−0
src/web/pages/forges.ts +2
| @@ -62,6 +62,8 @@ function forgeRow(r: ClientRow): Html { | ||
| 62 | 62 | return row({ |
| 63 | 63 | title: html`<a href="${r.url}">${r.name}</a>`, |
| 64 | 64 | 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, | |
| 65 | 67 | data: { forge: r.name, claimed: state }, |
| 66 | 68 | ...(r.reach === null |
| 67 | 69 | ? {} |
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 | }); |
Shorten the comment above this line to one line.