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

/system rows are keyboard-first: j/k move through its panels, enter drills in #10350

Merged opened this

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

Merged

scout/forge:scout/10340-system-rows-are-keyboard-first-j-k-move → main 2 commits fork

  • Merged. scout/10340-system-rows-are-keyboard-first-j-k-move was merged into main as 6327ae9.

Review

Sign in to review this change.
2 files changed +67 −2
src/web/pages/system.ts +10 −2
@@ -137,8 +137,16 @@ export function cachesSection(rows: readonly CacheRow[]): Html {
137137 export function featuresSection(rows: readonly FeatureHealth[]): Html {
138138 if (rows.length === 0) return html``;
139139 return html`<section class="settings-block" id="sys-features"><h2>Features from apps</h2>
140 <ul class="list" aria-label="Absorbed features and their health">${rows.map((r) => html`<li data-feature="${r.slug}" data-healthy="${String(r.healthy)}"><strong>${r.name}</strong> ${r.healthy ? html`<span class="small muted">${r.state}</span>` : badge(r.state, { tone: "danger" })}
141 <span class="small muted mono">${[r.commit, r.since].filter((x) => x !== null).join(" · ")}</span>${r.problem === null ? null : html` <span class="small muted">${r.problem}</span>`}</li>`)}</ul>
140 <ul class="list" aria-label="Absorbed features and their health">${rows.map((r) =>
141 row({
142 href: `/${r.repo}`,
143 nav: true,
144 title: r.name,
145 meta: html`${r.healthy ? html`<span class="small muted">${r.state}</span>` : badge(r.state, { tone: "danger" })}
146 <span class="small muted mono">${[r.commit, r.since].filter((x) => x !== null).join(" · ")}</span>${r.problem === null ? null : html` <span class="small muted">${r.problem}</span>`}`,
147 data: { feature: r.slug, healthy: String(r.healthy) },
148 }),
149 )}</ul>
142150 <p class="small"><a href="/settings/features">Settings › Features</a></p></section>`;
143151 }
144152
test/keys-system.test.ts added +57
@@ -0,0 +1,57 @@
1 /** /system's feature rows join j/k traversal (#10340): one `data-nav` per row, linking to the app's own page; none when empty. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createApp as insertApp } from "../src/core/apps.ts";
9 import { absorb } from "../src/core/features.ts";
10 import { createToken } from "../src/core/tokens.ts";
11 import { createUser } from "../src/core/users.ts";
12 import { serve } from "../src/http/server.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 let dir: string;
18 let app: ReturnType<typeof createApp>;
19 let servers: Awaited<ReturnType<typeof serve>>;
20 let token = "";
21 let ada: Awaited<ReturnType<typeof createUser>> & { ok: true };
22
23 const get = async () => (await fetch(`${servers.url}/system`, { headers: { authorization: `Bearer ${token}` } })).text();
24 const navs = (html: string) => html.match(/<a [^>]*data-nav[^>]*>/g) ?? [];
25
26 beforeAll(async () => {
27 dir = await mkdtemp(join(tmpdir(), "forge-keys-system-"));
28 app = createApp({ dataDir: dir, port: 0 });
29 servers = await serve(app);
30 const r = await createUser(app.db, { origin: "seed", trusted: true, username: "ada", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
31 if (!r.ok) throw new Error(r.error);
32 ada = r as typeof ada;
33 token = createToken(app.db, r.value.id, "t", "admin").token;
34 });
35 afterAll(async () => {
36 await servers.stop();
37 await rm(dir, { recursive: true, force: true });
38 });
39
40 test("a page with no features has no data-nav", async () => {
41 expect(await get()).not.toContain("data-nav");
42 });
43
44 test("each feature row is one data-nav linking to its own page", async () => {
45 for (const name of ["one", "two"]) {
46 const repo = await createRepo(app, ada.value, { name, visibility: "public", description: "" });
47 if (!repo.ok) throw new Error(String(repo.error));
48 const a = insertApp(app.db, { repoId: repo.value.id, slug: name, access: "public" });
49 if (!a.ok) throw new Error(a.error);
50 absorb(app.db, a.value.id, ada.value.id, name, "/");
51 }
52 const found = navs(await get());
53 expect(found).toHaveLength(2);
54 expect(found.some((a) => a.includes('href="/ada/one"'))).toBe(true);
55 expect(found.some((a) => a.includes('href="/ada/two"'))).toBe(true);
56 expect(found.every((a) => a.includes('tabindex="-1"'))).toBe(true);
57 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs