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

j/k, enter, esc and ? on upstream-follow (sources), with test/keys-<page>.test.ts #10807

Merged opened this

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

Merged

mason/forge:mason/10796-j-k-enter-esc-and-on-upstream-follow-sou → main 2 commits fork

  • Merged. mason/10796-j-k-enter-esc-and-on-upstream-follow-sou was merged into main as c62dcc9.

Review

Sign in to review this change.
2 files changed +76 −0
src/web/pages/upstream-follow.ts +1
@@ -223,6 +223,7 @@ function sourceLine(base: string, s: FollowSource): Html {
223223 const stamp = stampOf(s);
224224 const stand = sourceStanding(s);
225225 return row({
226 nav: true,
226227 lead: icon("sync"),
227228 title: html`<a href="${s.url}" rel="noreferrer noopener">${upstreamShort(s.url)}</a> <span class="mono">${s.branch}</span>`,
228229 meta: html`${stamp === undefined ? "" : `${stamp.account}@${stamp.host}`} · ${stand}${
test/keys-upstream-follow.test.ts added +75
@@ -0,0 +1,75 @@
1 /**
2 * The followed sources join the shell's j/k traversal (#10796): one focusable
3 * row each on the real settings page, Enter opening the source's own link.
4 * Left out: the check history (no link to open) and the forms around it,
5 * which keep their fields and buttons.
6 */
7 import { afterAll, beforeAll, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { addFollowSources } from "../src/actions/follow_sources.ts";
13 import { createRepo } from "../src/actions/repos.ts";
14 import { createUser } from "../src/core/users.ts";
15 import { serve, type Servers } from "../src/http/server.ts";
16 import { expectListKeys, mainOf, navHrefs, shellPage } from "./helpers/shell-page.ts";
17
18 let dataDir = "";
19 let app: App;
20 let servers: Servers;
21 let cookie = "";
22 const urls = ["https://example.com/a/one.git", "https://example.com/b/two.git"];
23
24 const page = async (): Promise<string> =>
25 mainOf(await (await fetch(`${servers.url}/alice/square/settings`, { headers: { cookie } })).text());
26 /** The sources card of the real page: the settings tabs above it are the page's own rows. */
27 const settings = async (): Promise<string> => {
28 const whole = await page();
29 const at = whole.indexOf('id="sources"');
30 return whole.slice(whole.lastIndexOf("<section", at), whole.indexOf("</section>", at) + 10);
31 };
32
33 beforeAll(async () => {
34 dataDir = await mkdtemp(join(tmpdir(), "forge-keys-follow-"));
35 app = createApp({ dataDir, port: 0 });
36 const made = await createUser(app.db, {
37 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
38 password: "a-good-passphrase", adminIfFirst: true,
39 });
40 if (!made.ok) throw new Error(JSON.stringify(made.error));
41 const repo = await createRepo(app, made.value, { name: "square", visibility: "public" });
42 if (!repo.ok) throw new Error(JSON.stringify(repo.error));
43 const added = addFollowSources(app, made.value, repo.value, urls.map((u) => `${u} main`).join("\n"));
44 if (!added.ok) throw new Error(JSON.stringify(added.error));
45 servers = await serve(app);
46 const res = await fetch(`${servers.url}/signin`, {
47 method: "POST", body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), redirect: "manual",
48 });
49 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0]!;
50 }, 30_000);
51
52 afterAll(async () => {
53 servers?.stop();
54 app?.close();
55 await rm(dataDir, { recursive: true, force: true });
56 });
57
58 test("each followed source is a focusable row; j/k focus it, Enter opens it, Esc has nowhere to go, fields keep their keys", async () => {
59 const html = await settings();
60 const hrefs = await navHrefs(html);
61 expect(hrefs).toEqual(urls);
62 await expectListKeys({ path: "/alice/square/settings", html, hrefs, opened: null });
63 });
64
65 test("a focused source row is reachable by script only (tabindex -1)", async () => {
66 const p = await shellPage("/alice/square/settings", await settings());
67 p.key("j");
68 expect(p.doc.activeElement.getAttribute("tabindex")).toBe("-1");
69 expect(p.doc.activeElement.hasAttribute("data-nav")).toBe(true);
70 });
71
72 test("on the whole page the sources follow the settings tabs in the same traversal", async () => {
73 const hrefs = await navHrefs(await page());
74 expect(hrefs.slice(-urls.length)).toEqual(urls);
75 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs