Public: anyone can read this; the crew can change it.
j/k, enter, esc and ? on returns, with test/keys-<page>.test.ts #10803
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10795-j-k-enter-esc-and-on-returns-with-test-k → main 2 commits fork
2 files
changed +52
−0
src/web/pages/returns.ts +1
| @@ -138,6 +138,7 @@ export function returnsPage(ctx: Ctx): Response { | ||
| 138 | 138 | const slug = repo ? `${repo.ownerName}/${repo.name}` : ""; |
| 139 | 139 | return row({ |
| 140 | 140 | href: `/${slug}/issues/${c.number}`, |
| 141 | nav: true, | |
| 141 | 142 | title: html`#${c.number} ${c.title}`, |
| 142 | 143 | meta: html`${slug} · ${runs(c)} · |
| 143 | 144 | ${c.outcome === "landed" |
test/keys-returns.test.ts added +51
| @@ -0,0 +1,51 @@ | ||
| 1 | /** `/board/returns` joins the shell's j/k traversal (#10795): the costliest issues are data-nav rows; the figure rows are not links and stay out. */ | |
| 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 { createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { openIssue } from "../src/actions/issues.ts"; | |
| 9 | import { createRun } from "../src/core/runs.ts"; | |
| 10 | import { forgeHttp, type ForgeHttp } from "../src/http/server.ts"; | |
| 11 | import { createSession } from "../src/core/sessions.ts"; | |
| 12 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 13 | import { quietLogs } from "./helpers/globals.ts"; | |
| 14 | import { expectListKeys, mainOf, navHrefs } from "./helpers/shell-page.ts"; | |
| 15 | ||
| 16 | quietLogs(); | |
| 17 | let dataDir = ""; | |
| 18 | let app: App; | |
| 19 | let http: ForgeHttp; | |
| 20 | let cookie = ""; | |
| 21 | ||
| 22 | beforeAll(async () => { | |
| 23 | dataDir = await mkdtemp(join(tmpdir(), "forge-keys-returns-")); | |
| 24 | app = createApp({ dataDir, port: 0 }); | |
| 25 | http = forgeHttp(app); | |
| 26 | const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 27 | if (!admin.ok) throw new Error(admin.error); | |
| 28 | cookie = `forge_session=${createSession(app.db, admin.value.id).token}`; | |
| 29 | const made = await createRepo(app, admin.value, { name: "mill", visibility: "public", description: "" }); | |
| 30 | if (!made.ok) throw new Error("repo"); | |
| 31 | for (const title of ["First work", "Second work"]) { | |
| 32 | const opened = openIssue(app, admin.value, made.value, { title }); | |
| 33 | if (!opened.ok) throw new Error("issue"); | |
| 34 | const id = createRun(app.db, { repoId: made.value.id, kind: "agent", issueId: opened.value.id, branch: "", actorId: admin.value.id, reason: "assigned" }).id; | |
| 35 | app.db.query("UPDATE run SET state='ok', cost_usd=1, started_at=?2, finished_at=?3 WHERE id=?1").run(id, new Date(Date.now() - 60_000).toISOString(), new Date().toISOString()); | |
| 36 | } | |
| 37 | }); | |
| 38 | ||
| 39 | afterAll(async () => { | |
| 40 | app.close(); | |
| 41 | await rm(dataDir, { recursive: true, force: true }); | |
| 42 | }); | |
| 43 | ||
| 44 | test("each costliest issue is one data-nav link; j/k focus it, Enter opens it, Esc comes back, fields keep their keys", async () => { | |
| 45 | const res = await http.handle(new Request("http://forge.test/board/returns", { headers: { cookie } })); | |
| 46 | const markup = mainOf(await res.text()); | |
| 47 | const hrefs = await navHrefs(markup); | |
| 48 | expect(hrefs.length).toBe(2); | |
| 49 | for (const href of hrefs) expect(href).toMatch(/^\/alice\/mill\/issues\/\d+$/); | |
| 50 | await expectListKeys({ path: "/board/returns", html: markup, hrefs, opened: { path: hrefs[0]!, body: "<main>issue</main>" } }); | |
| 51 | }); |