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

j/k, enter, esc and ? on resilience, with test/keys-<page>.test.ts #10802

Merged opened this

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

Merged

scout/forge:scout/10794-j-k-enter-esc-and-on-resilience-with-tes → main 3 commits fork

  • Merged. scout/10794-j-k-enter-esc-and-on-resilience-with-tes was merged into main as 99509b9.

Review

Sign in to review this change.
2 files changed +68 −1
src/web/pages/resilience.ts +1 −1
@@ -37,7 +37,7 @@ export function backupsLineHtml(ctx: Ctx): Html {
3737 export function nightlyHtml(ctx: Ctx): Html {
3838 const lines = nightlyLines(ctx.app.config.dataDir);
3939 return html`<ul id="nightly" class="muted">
40 ${JOBS.map((job, i) => html`<li>${lines[i] ?? ""} <a href="${runPath(job.name)}">Output</a></li>`)}
40 ${JOBS.map((job, i) => html`<li data-nav tabindex="-1">${lines[i] ?? ""} <a href="${runPath(job.name)}">Output</a></li>`)}
4141 ${lines.slice(JOBS.length).map((line) => html`<li>${line}</li>`)}
4242 </ul>`;
4343 }
test/keys-resilience.test.ts added +67
@@ -0,0 +1,67 @@
1 /**
2 * /resilience: the nightly jobs' Output links are data-nav rows; j/k, Enter, Esc and the field guard
3 * run on the real rendered page (#10794). The protection rows hold no link, so they stay out.
4 */
5 import { afterAll, beforeAll, expect, test } from "bun:test";
6 import { mkdtempSync, rmSync } from "node:fs";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { createApp, type App } from "../src/app.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12 import { JOBS } from "../src/nightly/jobs.ts";
13 import { runPath } from "../src/nightly/output.ts";
14 import { quietLogs } from "./helpers/globals.ts";
15 import { expectListKeys, mainOf } from "./helpers/shell-page.ts";
16
17 quietLogs();
18
19 let root = "";
20 let app: App;
21 let servers: Servers;
22 let cookie = "";
23
24 beforeAll(async () => {
25 root = mkdtempSync(join(tmpdir(), "forge-keys-resilience-"));
26 app = createApp({ dataDir: root, port: 0 });
27 const u = await createUser(app.db, {
28 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
29 password: "a-good-passphrase", adminIfFirst: true,
30 });
31 if (!u.ok) throw new Error(u.error);
32 servers = await serve(app);
33 const r = await fetch(`${servers.url}/signin`, {
34 method: "POST",
35 headers: { origin: servers.url },
36 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
37 redirect: "manual",
38 });
39 cookie = (r.headers.get("set-cookie") ?? "").split(";")[0]!;
40 }, 60_000);
41
42 afterAll(() => {
43 servers.stop();
44 app.close();
45 rmSync(root, { recursive: true, force: true });
46 });
47
48 const get = async (path: string) => (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
49
50 test("each nightly job is one data-nav row; protections are not", async () => {
51 const text = await get("/resilience");
52 expect(text.match(/data-nav/g)?.length).toBe(JOBS.length);
53 const protections = text.slice(text.indexOf('id="protections-h"'));
54 expect(protections).not.toContain("data-nav");
55 });
56
57 test("j/k focus a nightly row, Enter opens its output, Esc on that page returns, fields are left alone", async () => {
58 const text = await get("/resilience");
59 const hrefs = JOBS.map((j) => runPath(j.name));
60 const job = JOBS[0]!;
61 await expectListKeys({
62 path: "/resilience",
63 html: mainOf(text),
64 hrefs,
65 opened: { path: runPath(job.name), body: mainOf(await get(runPath(job.name))) },
66 });
67 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs