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

The apps list is keyboard-first: j/k move between apps, enter opens one #10349

Merged opened this

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

Merged

torque/forge:torque/10339-the-apps-list-is-keyboard-first-j-k-move → main 1 commit fork

  • Merged. torque/10339-the-apps-list-is-keyboard-first-j-k-move was merged into main as f6386a1.

Review

Sign in to review this change.
1 file changed +73 −0
test/keys-apps-list.test.ts added +73
@@ -0,0 +1,73 @@
1 /** The apps list joins the shell's j/k row traversal: one `data-nav` row per app, none when empty (#10339). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { createApp as makeAppRecord } from "../src/core/apps.ts";
9 import { createUser, type User } from "../src/core/users.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { proveAll } from "./helpers/verified.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13
14 quietLogs();
15
16 let root = "";
17 let app: App;
18 let servers: Servers;
19 let cookie = "";
20 let alice: User;
21
22 beforeAll(async () => {
23 root = mkdtempSync(join(tmpdir(), "forge-apps-keys-"));
24 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
25 const u = await createUser(app.db, {
26 origin: "seed",
27 trusted: true,
28 username: "alice",
29 email: "[email protected]",
30 password: "a-good-passphrase",
31 adminIfFirst: true,
32 });
33 if (!u.ok) throw new Error(u.error);
34 proveAll(app);
35 alice = u.value;
36 for (const name of ["keeper", "second"]) {
37 const made = await createRepo(app, alice, { name });
38 if (!made.ok) throw new Error(made.error);
39 const rec = makeAppRecord(app.db, { repoId: made.value.id, slug: name, access: "public" });
40 if (!rec.ok) throw new Error(rec.error);
41 }
42 servers = await serve(app);
43 const signin = await fetch(`${servers.url}/signin`, {
44 method: "POST",
45 headers: { origin: servers.url },
46 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
47 redirect: "manual",
48 });
49 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
50 });
51 afterAll(() => {
52 servers.stop();
53 app.close();
54 rmSync(root, { recursive: true, force: true });
55 });
56
57 const get = async (path: string) =>
58 (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
59
60 test("each app row is a data-nav link to the app's own page", async () => {
61 const text = await get("/apps");
62 const navs = text.match(/<a [^>]*data-nav[^>]*>/g) ?? [];
63 expect(navs.length).toBe(2);
64 expect(navs.some((a) => a.includes('href="/alice/keeper"'))).toBe(true);
65 expect(navs.some((a) => a.includes('href="/alice/second"'))).toBe(true);
66 expect(navs.every((a) => a.includes('tabindex="-1"'))).toBe(true);
67 });
68
69 test("a page with no rows has no data-nav", async () => {
70 const text = await get("/apps?archived=1");
71 expect(text).toContain("Nothing is archived");
72 expect(text).not.toContain("data-nav");
73 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs