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

The front door is the one studio page with no heading #162

Merged opened this

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

Merged

scout/forge:scout/142-the-front-door-is-the-one-studio-page-wi → main 1 commit fork

  • Merged. scout/142-the-front-door-is-the-one-studio-page-wi was merged into main as e2bd71b.

Review

Sign in to review this change.
2 files changed +158 −1
src/web/studio/home.ts +2 −1
@@ -124,7 +124,8 @@ export async function studioHome(ctx: Ctx, user: User): Promise<Response> {
124124 { head: nowHead },
125125 );
126126
127 const body = html`<div class="dash">
127 const body = html`<h1 class="sr-only">Studio</h1>
128 <div class="dash">
128129 <div class="dash-main stack">
129130 <div id="live-asks" data-live="run.*,pull.*,issue.*,check.*">${asks}</div>
130131 <div
test/studio.test.ts +156
@@ -4448,3 +4448,159 @@ describe("finding things in the studio: /search", () => {
44484448 expect(html).toContain("Issues and pull requests");
44494449 });
44504450 });
4451
4452 describe("every studio page's headings form one outline", () => {
4453 let dataDir: string;
4454 let app: App;
4455 let servers: Servers;
4456 let base: string;
4457 let alice: User;
4458 let repo: Repo;
4459 let cookie: string;
4460 let issueNumber: number;
4461 let pullNumber: number;
4462 let runId: string;
4463
4464 const get = (path: string) =>
4465 fetch(`${base}${path}`, {
4466 headers: { cookie: `${cookie}; forge_mode=studio` },
4467 redirect: "manual",
4468 });
4469
4470 /** Every `<hN>` tag in document order, and whether the outline it forms
4471 * is one a heading-jumping reader can follow: exactly one `<h1>`, and
4472 * never a level deeper than one past the deepest seen so far. */
4473 function headingOutline(html: string): { levels: number[]; ok: boolean } {
4474 const levels = [...html.matchAll(/<h([1-6])(?:[ >])/g)].map((m) =>
4475 Number(m[1]),
4476 );
4477 const h1s = levels.filter((l) => l === 1).length;
4478 let deepest = 0;
4479 let skipped = false;
4480 for (const level of levels) {
4481 if (level > deepest + 1) skipped = true;
4482 deepest = Math.max(deepest, level);
4483 }
4484 return { levels, ok: h1s === 1 && !skipped };
4485 }
4486
4487 beforeAll(async () => {
4488 dataDir = await mkdtemp(join(tmpdir(), "forge-studio-outline-"));
4489 app = createApp({ dataDir, port: 0 });
4490 servers = await serve(app);
4491 base = servers.url;
4492 const a = await createUser(app.db, {
4493 username: "alice",
4494 email: "[email protected]",
4495 password: "a-good-passphrase",
4496 adminIfFirst: true,
4497 });
4498 if (!a.ok) throw new Error(a.error);
4499 alice = a.value;
4500 cookie = (
4501 (
4502 await fetch(`${base}/signin`, {
4503 method: "POST",
4504 body: new URLSearchParams({
4505 username: "alice",
4506 password: "a-good-passphrase",
4507 }),
4508 redirect: "manual",
4509 })
4510 ).headers.get("set-cookie") ?? ""
4511 ).split(";")[0]!;
4512 const made = await createRepo(app, alice, {
4513 name: "outline",
4514 visibility: "private",
4515 description: "for the outline",
4516 });
4517 if (!made.ok) throw new Error(String(made.error));
4518 repo = made.value;
4519 const seeded = await commitToBranch(app, alice, repo, {
4520 message: "Start",
4521 files: [
4522 {
4523 path: "package.json",
4524 content: '{"name":"outline","scripts":{"start":"bun run server.ts"}}\n',
4525 },
4526 {
4527 path: "server.ts",
4528 content:
4529 "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n",
4530 },
4531 ],
4532 });
4533 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
4534 const enabled = await enableApp(app, alice, repo, { slug: "outline" });
4535 if (!enabled.ok) throw new Error(String(enabled.error));
4536
4537 const told = openIssue(app, alice, repo, {
4538 title: "Outline test tell",
4539 body: "for the heading walk",
4540 });
4541 if (!told.ok) throw new Error(String(told.error));
4542 issueNumber = told.value.number;
4543
4544 const branch = await commitToBranch(app, alice, repo, {
4545 branch: "feature/outline",
4546 base: "main",
4547 message: "Outline change",
4548 files: [{ path: "OUTLINE.md", content: "outline\n" }],
4549 });
4550 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
4551 const opened = await openPull(app, alice, repo, {
4552 title: "Outline test change",
4553 body: "",
4554 baseBranch: "main",
4555 headBranch: "feature/outline",
4556 });
4557 if (!opened.ok) throw new Error(String(opened.error));
4558 pullNumber = opened.value.number;
4559 const run = createRun(app.db, {
4560 repoId: repo.id,
4561 kind: "agent",
4562 actorId: alice.id,
4563 reason: "assigned",
4564 });
4565 claimRun(app.db, run.id);
4566 finishRun(app.db, run.id, { state: "ok", summary: "done" });
4567 runId = run.id;
4568 });
4569
4570 afterAll(async () => {
4571 servers.stop();
4572 app.close();
4573 await rm(dataDir, { recursive: true, force: true });
4574 });
4575
4576 test("the studio home has exactly one h1, said in the studio's own words, and no skipped heading level", async () => {
4577 const html = await (await get("/")).text();
4578 const outline = headingOutline(html);
4579 expect(outline.levels.filter((l) => l === 1).length).toBe(1);
4580 expect(outline.ok).toBe(true);
4581 expect(html).toMatch(/<h1 class="sr-only">Studio<\/h1>/);
4582 });
4583
4584 test("the app page, the three lists, a tell, a change and a run each have one outline with no skipped level", async () => {
4585 const pages: Record<string, string> = {
4586 "the app page": "/alice/outline",
4587 "everything told": "/alice/outline/tells",
4588 "every change": "/alice/outline/changes",
4589 "every run": "/alice/outline/runs",
4590 "a tell": `/alice/outline/issues/${issueNumber}`,
4591 "a change": `/alice/outline/pulls/${pullNumber}`,
4592 "a run": `/alice/outline/runs/${runId}`,
4593 "settings": "/alice/outline/settings",
4594 };
4595 for (const [name, path] of Object.entries(pages)) {
4596 const html = await (await get(path)).text();
4597 const outline = headingOutline(html);
4598 if (
4599 outline.levels[0] !== 1 ||
4600 outline.levels.filter((l) => l === 1).length !== 1 ||
4601 !outline.ok
4602 )
4603 throw new Error(`${name} (${path}): ${JSON.stringify(outline)}`);
4604 }
4605 });
4606 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs