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

Merged opened this

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

Merged

scout/forge:scout/9904-home-s-now-list-the-checks-reviews-appro → main 3 commits fork 3 threads · 3 unresolved

  • Merged. scout/9904-home-s-now-list-the-checks-reviews-appro was merged into main as d3454b7.

Review

Sign in to review this change.
2 files changed +85 −16
src/web/pages/now.ts +13 −16
@@ -11,7 +11,7 @@ import type { Ctx } from "../../http/context.ts";
1111 import { notFoundPage, signInFirst } from "../chrome.ts";
1212 import { html } from "../html.ts";
1313 import { page } from "../layout.ts";
14 import { card, empty, row, timeAgo } from "../ui.ts";
14 import { row, timeAgo } from "../ui.ts";
1515
1616 /** Where a word of the Now line goes. */
1717 export const nowHref = (stage: NowStage): string => `/now/${stage}`;
@@ -27,21 +27,18 @@ export function nowPage(ctx: Ctx): Response {
2727 const { total, rows } = readNowRows(ctx.app.db, stage);
2828 const body =
2929 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>`
outdated

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.

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>`;
4542 notePhase(ctx.request, "render", performance.now() - reading);
4643 return page({
4744 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

html.indexOf("<data-now") > -1 ? 0 : ... is a dead ternary (the element is <div data-now>). Slice from the first <ul class="list setting-list"> directly, so the row-count test means something.

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 &lt;pull&gt;");
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 class="card empty" is rendered.

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs