Public: anyone can read this; the crew can change it.
Home's Now list, the checks, reviews, approved pulls and live deploys it counts, speaks the home page's language #9906
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9904-home-s-now-list-the-checks-reviews-appro → main 3 commits fork 3 threads · 3 unresolved
2 files
changed +85
−16
src/web/pages/now.ts +13 −16
| @@ -11,7 +11,7 @@ import type { Ctx } from "../../http/context.ts"; | ||
| 11 | 11 | import { notFoundPage, signInFirst } from "../chrome.ts"; |
| 12 | 12 | import { html } from "../html.ts"; |
| 13 | 13 | import { page } from "../layout.ts"; |
| 14 | import { card, empty, row, timeAgo } from "../ui.ts"; | |
| 14 | import { row, timeAgo } from "../ui.ts"; | |
| 15 | 15 | |
| 16 | 16 | /** Where a word of the Now line goes. */ |
| 17 | 17 | export const nowHref = (stage: NowStage): string => `/now/${stage}`; |
| @@ -27,21 +27,18 @@ export function nowPage(ctx: Ctx): Response { | ||
| 27 | 27 | const { total, rows } = readNowRows(ctx.app.db, stage); |
| 28 | 28 | const body = |
| 29 | 29 | total === 0 |
| 30 | ? empty({ icon: "activity", title: `Nothing in ${NOW_WORDS[stage].toLowerCase()} right now`, note: NOW_HOLDS[stage] }) | |
| 31 | : card( | |
| 32 | html`<ul class="list setting-list"> | |
| 33 | ${rows.map((r) => | |
| 34 | row({ | |
| 35 | href: r.href, | |
| 36 | nav: true, | |
| 37 | title: html`${r.title}`, | |
| 38 | meta: html`${r.repo} · ${r.note}`, | |
| 39 | trail: html`<span class="muted small">${timeAgo(r.at)}</span>`, | |
| 40 | }), | |
| 41 | )} | |
| 42 | </ul>`, | |
| 43 | { pad: false }, | |
| 44 | ); | |
| 30 | ? html`<p class="muted">Nothing in ${NOW_WORDS[stage].toLowerCase()} right now.</p>` | |
| 31 | : html`<ul class="list setting-list"> | |
| 32 | ${rows.map((r) => | |
| 33 | row({ | |
| 34 | href: r.href, | |
| 35 | nav: true, | |
| 36 | title: html`${r.title}`, | |
| 37 | meta: html`${r.repo} · ${r.note}`, | |
| 38 | trail: html`<span class="muted small">${timeAgo(r.at)}</span>`, | |
| 39 | }), | |
| 40 | )} | |
| 41 | </ul>`; | |
| 45 | 42 | notePhase(ctx.request, "render", performance.now() - reading); |
| 46 | 43 | return page({ |
| 47 | 44 | ctx, |
test/now-language.test.ts added +72
| @@ -0,0 +1,72 @@ | ||
| 1 | /** Home's Now pages speak the home page's language (#9904): one heading, a plain list, no card boxes. */ | |
| 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 { createIssue } from "../src/core/issues.ts"; | |
| 9 | import { createSession } from "../src/core/sessions.ts"; | |
| 10 | import { createUser } from "../src/core/users.ts"; | |
| 11 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 12 | import { NOW_HOLDS, NOW_STAGES, NOW_WORDS, readNowRows } from "../src/flow/now_rows.ts"; | |
| 13 | import { promote } from "./helpers/members.ts"; | |
| 14 | ||
| 15 | let dir: string; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | let cookie: string; | |
| 19 | const get = (path: string) => fetch(servers.url + path, { headers: { cookie }, redirect: "manual" }); | |
| 20 | ||
| 21 | beforeAll(async () => { | |
| 22 | dir = await mkdtemp(join(tmpdir(), "forge-now-lang-")); | |
| 23 | app = createApp({ dataDir: dir, port: 0 }); | |
| 24 | servers = await serve(app); | |
| 25 | const r = await createUser(app.db, { origin: "seed", trusted: true, username: "root", allowReserved: true, email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true }); | |
| 26 | if (!r.ok) throw new Error(r.error); | |
| 27 | promote(app.db, r.value); | |
| 28 | cookie = `forge_session=${createSession(app.db, r.value.id).token}`; | |
| 29 | const repo = await createRepo(app, r.value, { name: "notes", visibility: "public" }); | |
| 30 | if (!repo.ok) throw new Error("repo"); | |
| 31 | const made = createIssue(app.db, { repoId: repo.value.id, authorId: r.value.id, title: "Checked <pull>", kind: "pull" }); | |
| 32 | if (!made.ok) throw new Error(made.error); | |
| 33 | app.db.query("INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid) VALUES (?, 'main', 'h', 'b', 'h')").run(made.value.id); | |
| 34 | app.db | |
| 35 | .query("INSERT INTO run (id, repo_id, kind, issue_id, env, state, reason) VALUES (?, ?, 'check', ?, '', 'running', '')") | |
| 36 | .run(crypto.randomUUID(), repo.value.id, made.value.id); | |
| 37 | }); | |
| 38 | afterAll(async () => { | |
| 39 | servers.stop(); | |
| 40 | app.close(); | |
| 41 | await rm(dir, { recursive: true, force: true }); | |
| 42 | }); | |
| 43 | ||
| 44 | test("every Now page has one h1 and no card", async () => { | |
| 45 | for (const stage of NOW_STAGES) { | |
| 46 | const html = await (await get(`/now/${stage}`)).text(); | |
| 47 | expect(html.match(/<h1[ >]/g)?.length, stage).toBe(1); | |
| 48 | expect(html, stage).not.toContain('class="card'); | |
| 49 | expect(html, stage).not.toContain("card-body"); | |
| 50 | } | |
| 51 | }); | |
| 52 | ||
| 53 | test("a stage with rows lists them plainly, as many as home counts, escaped", async () => { | |
| 54 | const html = await (await get("/now/check")).text(); | |
| 55 | const total = readNowRows(app.db, "check").total; | |
| 56 | expect(total).toBeGreaterThan(0); | |
| 57 | const start = html.indexOf('<ul class="list setting-list">'); | |
outdated
|
||
| 58 | expect(start).toBeGreaterThan(-1); | |
| 59 | const mine = html.slice(start, html.indexOf("</ul>", start)); | |
| 60 | expect(mine.match(/<li/g)?.length).toBe(total); | |
| 61 | expect(html).toContain("Checked <pull>"); | |
| 62 | }); | |
| 63 | ||
| 64 | test("an empty stage says so in one line", async () => { | |
| 65 | const stage = NOW_STAGES.find((s) => readNowRows(app.db, s).total === 0)!; | |
| 66 | expect(stage).toBeDefined(); | |
| 67 | const html = await (await get(`/now/${stage}`)).text(); | |
outdated
Weak: "right now." also matches other text. Assert the exact one-line sentence, that NOW_HOLDS appears once on the page, and that no |
||
| 68 | expect(html).toContain(`<p class="muted">Nothing in ${NOW_WORDS[stage].toLowerCase()} right now.</p>`); | |
| 69 | expect(html.split(NOW_HOLDS[stage]).length - 1).toBe(1); | |
| 70 | expect(html).not.toContain('class="card empty"'); | |
| 71 | expect(html).not.toContain("<ul class=\"list"); | |
| 72 | }); |
Principle 1: NOW_HOLDS[stage] is already printed in the #now-count line above, so this line explains the stage twice. Make it just: Nothing in ${NOW_WORDS[stage].toLowerCase()} right now.