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

The issue page's sections share one heading style, and a test holds it #9684

Merged opened this

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

Merged

mason/forge:mason/9677-the-issue-page-s-sections-share-one-head → main 2 commits fork

  • Merged. mason/9677-the-issue-page-s-sections-share-one-head was merged into main as afe504e.

Review

Sign in to review this change.
2 files changed +131 −3
src/web/studio/tell.ts +6 −3
@@ -582,11 +582,14 @@ async function studioTell(
582582 ${privateWorkCard(ctx, repo, issue, `${thread}/private`)}
583583 ${hasPicture(db, repo.id, issue.number)
584584 ? card(
585 html`<h2>Picture of the page</h2>
586 <a href="${thread}/picture"
585 html`<a href="${thread}/picture"
587586 ><img src="${thread}/picture" alt="The page the sender was looking at" loading="lazy" class="tell-picture"
588587 /></a>`,
589 { extra: "mt" },
588 {
589 extra: "mt",
590 head: html`${icon("eye")}
591 <h2>Picture of the page</h2>`,
592 },
590593 )
591594 : null}
592595 ${runsCard}
test/issue-page-rows.test.ts added +125
@@ -0,0 +1,125 @@
1 /**
2 * The issue page's sections speak one heading: every `.card-head` on a
3 * rendered issue is an icon then an `<h2>`, and nothing else. A section
4 * that drops its icon, or puts its title in the body, fails here. The
5 * labels and assignee cards are one function for the issue and pull pages,
6 * so the heading held here is the pull page's too.
7 */
8
9 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
10 import { mkdtemp, rm } from "node:fs/promises";
11 import { tmpdir } from "node:os";
12 import { join } from "node:path";
13 import { openIssue } from "../src/actions/issues.ts";
14 import { openPull } from "../src/actions/pulls.ts";
15 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
16 import { createApp, type App } from "../src/app.ts";
17 import { savePicture } from "../src/core/note_pictures.ts";
18 import { createSession } from "../src/core/sessions.ts";
19 import { saveTeam } from "../src/core/teams.ts";
20 import { createRun } from "../src/core/runs.ts";
21 import { createUser, type User } from "../src/core/users.ts";
22 import { getRepoById } from "../src/core/repos.ts";
23 import { serve, type Servers } from "../src/http/server.ts";
24 import { quietLogs } from "./helpers/globals.ts";
25
26 quietLogs();
27
28 /** Every card heading in a page, as `[icon-reference, title]`, or the raw markup when it is not that shape. */
29 function headings(page: string): (string | [string, string])[] {
30 const out: (string | [string, string])[] = [];
31 for (const m of page.matchAll(/<div class="card-head">([\s\S]*?)<\/div>/g)) {
32 const inner = m[1]!.replace(/\s+/g, " ").trim();
33 if (!/<h2[\s>]/.test(inner)) continue; // the byline's head is not a section heading
34 const shape = /^<svg[^>]*><use href="[^"]*#i-([A-Za-z]+)"\/><\/svg> <h2>([^<]+)<\/h2>$/.exec(inner);
35 out.push(shape ? [shape[1]!, shape[2]!] : inner);
36 }
37 return out;
38 }
39
40 describe("the issue page's section headings", () => {
41 let dataDir = "";
42 let app: App;
43 let servers: Servers;
44 let alice: User;
45 let repoId = "";
46 const cookie = () => `forge_session=${createSession(app.db, alice.id).token}`;
47 const get = async (path: string) =>
48 (await fetch(`${servers.url}/alice/garden${path}`, { headers: { cookie: cookie() } })).text();
49
50 beforeAll(async () => {
51 dataDir = await mkdtemp(join(tmpdir(), "forge-issue-page-rows-"));
52 app = createApp({ dataDir, port: 0 });
53 servers = await serve(app);
54 const made = await createUser(app.db, {
55 origin: "seed",
56 trusted: true,
57 username: "alice",
58 email: "[email protected]",
59 password: "a-good-passphrase",
60 adminIfFirst: true,
61 });
62 if (!made.ok) throw new Error(made.error);
63 alice = made.value;
64 const r = await createRepo(app, alice, { name: "garden", visibility: "private" });
65 if (!r.ok) throw new Error(String(r.error));
66 repoId = r.value.id;
67 const seeded = await commitToBranch(app, alice, r.value, {
68 message: "seed",
69 files: [{ path: "README.md", content: "# garden\n" }],
70 });
71 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
72 saveTeam(app.db, "design", { charter: "design", dial: 1, members: [] });
73 }, 30_000);
74
75 afterAll(async () => {
76 await servers.stop();
77 app.close();
78 await rm(dataDir, { recursive: true, force: true });
79 });
80
81 test("a sealed issue's private-work card has the same heading", async () => {
82 const repo = getRepoById(app.db, repoId)!;
83 const o = openIssue(app, alice, repo, { title: "A hole", sealed: true });
84 if (!o.ok) throw new Error(o.error);
85 const found = headings(await get(`/issues/${o.value.number}`));
86 expect(found.filter((h) => typeof h === "string")).toEqual([]);
87 expect(found.map((h) => (typeof h === "string" ? h : h[1]))).toContain("Fix it privately");
88 });
89
90 test("every section carries an icon and an h2, the same way", async () => {
91 const repo = getRepoById(app.db, repoId)!;
92 const o = openIssue(app, alice, repo, { title: "A chart" });
93 if (!o.ok) throw new Error(o.error);
94 const issue = o.value;
95 const branch = "fix";
96 const c = await commitToBranch(app, alice, repo, {
97 branch,
98 base: "main",
99 message: branch,
100 files: [{ path: "fix.md", content: "x\n" }],
101 });
102 if (!c.ok) throw new Error(JSON.stringify(c.error));
103 const p = await openPull(app, alice, repo, {
104 title: branch,
105 body: `Closes #${issue.number}`,
106 baseBranch: "main",
107 headBranch: branch,
108 });
109 if (!p.ok) throw new Error(String(p.error));
110 createRun(app.db, { repoId: repo.id, kind: "agent", issueId: issue.id, env: "main", commitOid: p.value.headOid, reason: "issue" } as never);
111 const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]);
112 app.db
113 .query("INSERT INTO app_note (id, app_id, repo_id, issue_number, visitor, name, hidden, created_at) VALUES ('note1abcdefgh','a',?1,?2,'v','n',0,?3)")
114 .run(repo.id, issue.number, new Date().toISOString());
115 const saved = savePicture(app.db, dataDir, { id: "note1abcdefgh", repoId: repo.id, issueNumber: issue.number }, "png", png);
116 expect(saved).toBe(true);
117
118 const found = headings(await get(`/issues/${issue.number}`));
119 const titles = found.map((h) => (typeof h === "string" ? h : h[1]));
120 for (const want of ["Runs", "Closes with", "Picture of the page", "Depends on", "Labels", "Assignee", "Teams"])
121 expect(titles).toContain(want);
122 // No heading is anything but icon + h2.
123 expect(found.filter((h) => typeof h === "string")).toEqual([]);
124 });
125 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs