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

The feature flags list is keyboard-first: j/k move between rows, enter opens one #10372

Merged opened this

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

Merged

scout/forge:scout/10358-the-feature-flags-list-is-keyboard-first → main 4 commits fork

  • Merged. scout/10358-the-feature-flags-list-is-keyboard-first was merged into main as 2071426.

Review

Sign in to review this change.
2 files changed +70 −0
src/web/pages/flags.ts +1
@@ -81,6 +81,7 @@ export function flagsPage(ctx: Ctx): Response {
8181 usesMs += t2 - t1;
8282 const lead = winnerOffer(month, v);
8383 return row({
84 nav: true,
8485 lead: icon("activity"),
8586 title: html`<span class="mono">${name}</span>`,
8687 meta: html`${decl.why}<br />Default
test/keys-flags-list.test.ts added +69
@@ -0,0 +1,69 @@
1 /** The flags list joins the shell's j/k row traversal (#10358): one data-nav per row, none on the empty page. */
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, type App } from "../src/app.ts";
7 import { makeFlags } from "../src/core/flags.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { useFlagRegistry } from "../src/web/pages/flags.ts";
11
12 let dataDir: string;
13 let app: App;
14 let servers: Servers;
15 let cookie = "";
16
17 beforeAll(async () => {
18 dataDir = await mkdtemp(join(tmpdir(), "forge-keys-flags-"));
19 app = createApp({ dataDir, port: 0, forgeRepo: "acme/forge" });
20 servers = await serve(app);
21 const made = await createUser(app.db, {
22 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
23 password: "a-good-passphrase", adminIfFirst: true,
24 });
25 if (!made.ok) throw new Error(made.error);
26 const res = await fetch(`${servers.url}/signin`, {
27 method: "POST",
28 redirect: "manual",
29 headers: { "content-type": "application/x-www-form-urlencoded", origin: servers.url },
30 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
31 });
32 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0] ?? "";
33 });
34
35 afterAll(async () => {
36 useFlagRegistry(undefined);
37 await servers.stop();
38 app.close();
39 await rm(dataDir, { recursive: true, force: true });
40 });
41
42 const get = async () => (await fetch(`${servers.url}/settings/flags`, { headers: { cookie } })).text();
43
44 test("each flag row is one data-nav row whose primary link is its owner issue", async () => {
45 useFlagRegistry(
46 makeFlags({
47 one: { why: "first", default: false, issue: 11 },
48 two: { why: "second", default: false, issue: 12 },
49 three: { why: "third", default: true, issue: 13 },
50 }),
51 );
52 const text = await get();
53 const rows = text.split("<li").filter((li) => /class="row"[^>]*data-nav tabindex="-1"/.test(li));
54 expect(rows.length).toBe(3);
55 for (const [i, n] of [11, 12, 13].entries()) {
56 expect(rows[i]).toContain(`href="/acme/forge/issues/${n}"`);
57 // the first link in the row is the one enter opens
58 expect(rows[i]!.match(/<a href="([^"]+)"/)![1]).toBe(`/acme/forge/issues/${n}`);
59 }
60 // the settings sidebar's own data-nav rows are not nested in a flag row
61 expect(rows.every((r) => !r.includes("<li"))).toBe(true);
62 });
63
64 test("the empty list draws no flag data-nav row", async () => {
65 useFlagRegistry(makeFlags({}));
66 const text = await get();
67 expect(text).toContain("No flag is declared yet.");
68 expect(text).not.toMatch(/class="row"[^>]*data-nav tabindex="-1"/);
69 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs