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

The New forge page at /forges/new, where the owner spawns a forge for a client, speaks the home page's language #9876

Merged opened this

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

Merged

mason/forge:mason/9864-the-new-forge-page-at-forges-new-where-t → main 4 commits fork 3 threads · 3 unresolved

  • Merged. mason/9864-the-new-forge-page-at-forges-new-where-t was merged into main as 221edc5.

Review

Sign in to review this change.
3 files changed +131 −21
docs/design/areas/lists.md +1 −1
@@ -58,4 +58,4 @@ Disks and network, Processes, Main-thread stalls, Telemetry readers, Code page c
5858 nothing to show a part says so in one plain sentence. The live regions (`summary`, `cores`, `mem`, `temps`, `sys-disks`,
5959 `sys-procs`, …) keep their ids, so `system-live.ts` still patches them. `test/system-parts.test.ts` holds the shape.
6060
61 `/forges` (#9798) speaks the home page's language and adds no class: "Side by side" and "Forges" are each a `<section class="settings-block">` under an `<h2>` (the list is the same `row()`s in a `.list.setting-list`); with none sent, one headed section ("No forges sent yet") says so in a plain sentence with the New forge button. The fresh-link page, the remove confirm and the stop/remove receipts are a `.settings-block` too, no `.card`; the claim link (`claimLinkBody`) is a bare row list. Those pages stay `cache-control: no-store`. `test/forges-language.test.ts` holds the shape.
61 `/forges` (#9798) and `/forges/new` (the form, the plan and the answer with the claim link, each a headed section) speak the home page's language and add no class: "Side by side" and "Forges" are each a `<section class="settings-block">` under an `<h2>` (the list is the same `row()`s in a `.list.setting-list`); with none sent, one headed section ("No forges sent yet") says so in a plain sentence with the New forge button. The fresh-link page, the remove confirm and the stop/remove receipts are a `.settings-block` too, no `.card`; the claim link (`claimLinkBody`) is a bare row list. Those pages stay `cache-control: no-store`. `test/forges-language.test.ts` holds the shape.
outdated

Grammar: two subjects now, so "speak the home page language and add no class", not "adds".

src/web/pages/new-forge.ts +23 −20
@@ -31,7 +31,7 @@ import { field, readForm, type Form } from "../form.ts";
3131 import { html, type Html } from "../html.ts";
3232 import { page } from "../layout.ts";
3333 import { pickList } from "../pick.ts";
34 import { btn, card, field as formField, row, selectField } from "../ui.ts";
34 import { btn, field as formField, row, selectField } from "../ui.ts";
3535
3636 interface Values {
3737 readonly name: string;
@@ -144,12 +144,13 @@ function formView(
144144 if (full)
145145 return html`<div class="narrow-wide" data-forges>
146146 ${pageHead("New forge")}
147 ${card(
148 html`<p role="status">This forge cannot hold another child: ${full}.</p>
149 ${ctx.auth.user?.isAdmin
150 ? html`<p><a href="/settings/capacity">Raise the room in settings</a></p>`
151 : null}`,
152 )}
147 <section class="settings-block">
148 <h2>No room</h2>
149 <p role="status">This forge cannot hold another child: ${full}.</p>
150 ${ctx.auth.user?.isAdmin
151 ? html`<p><a href="/settings/capacity">Raise the room in settings</a></p>`
152 : null}
153 </section>
153154 </div>`;
154155 const anon = user === undefined;
155156 return html`<div class="narrow-wide" data-forges>
@@ -165,8 +166,9 @@ function formView(
165166 ? html`<p class="error mb" role="alert">${errors["form"]}</p>`
166167 : null}
167168 ${plan}
168 ${card(
169 html`<form method="post" action="/forges/new" class="stack" id="new-forge">
169 <section class="settings-block">
170 <h2>Its name and apps</h2>
171 <form method="post" action="/forges/new" class="stack" id="new-forge">
170172 ${formField({
171173 label: "Name",
172174 name: "name",
@@ -322,8 +324,8 @@ function formView(
322324 </button>
323325 ${btn("Spawn", { variant: "primary" })}
324326 </div>
325 </form>`,
326 )}
327 </form>
328 </section>
327329 </div>`;
328330 }
329331
@@ -339,8 +341,9 @@ function planView(ctx: Ctx, user: User | undefined, values: Values, body: Record
339341 const plan = body["plan"] as { budgetUsd?: number | null; template?: string } | undefined;
340342 const budget = plan?.budgetUsd ?? null;
341343 const address = childUrlOf(ctx.app, values.name);
342 return card(
343 html`<ul class="mb">
344 return html`<section class="settings-block" id="plan">
345 <h2>The plan</h2>
346 <ul class="mb">
344347 <li>A forge at <b>${address}</b>.</li>
345348 <li>Whoever opens the link becomes its admin.</li>
346349 ${user === undefined
@@ -366,9 +369,8 @@ function planView(ctx: Ctx, user: User | undefined, values: Values, body: Record
366369 you press Spawn.
367370 </p>
368371 ${typeof note === "string" ? html`<p class="small mt">${note}</p>` : null}
369 <p class="mt">${btn("Spawn", { variant: "primary", form: "new-forge" })}</p>`,
370 { id: "plan", head: html`<h2>The plan</h2>` },
371 );
372 <p class="mt">${btn("Spawn", { variant: "primary", form: "new-forge" })}</p>
373 </section>`;
372374 }
373375
374376 /** The answer to a spawn: the link, once. */
@@ -399,14 +401,15 @@ function madeView(body: Record<string, unknown>): Html {
399401 ${pageHead(
400402 body["existed"] === true ? `${name} already exists` : `${name} is ready`,
401403 )}
402 ${card(
403 typeof link === "string"
404 <section class="settings-block">
405 <h2>${typeof link === "string" ? "The link to send" : "Its link"}</h2>

This heading and the link/no-store behaviour of the answer have no test (principle: every behaviour added is tested).

406 ${typeof link === "string"
404407 ? claimLinkBody(link, typeof note === "string" ? note : undefined)
405408 : html`<p>
406409 It was made before, so its link is not shown again. Ask for a new
407410 one if it was lost.
408 </p>`,
409 )}
411 </p>`}
412 </section>
410413 </div>`;
411414 }
412415
test/new-forge-language.test.ts added +107
@@ -0,0 +1,107 @@
1 /** /forges/new speaks the home page's language: headed sections, no card. */
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 } from "../src/app.ts";
7 import { createToken } from "../src/core/tokens.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve } from "../src/http/server.ts";
10 import { usePlacement, useProbe } from "../src/spawn/door.ts";
11 import { bootHostedChild } from "../src/spawn/host.ts";
12 import { ok } from "../src/kernel/result.ts";
13
14 let dir = "";
15 let app: ReturnType<typeof createApp>;
16 let servers: Awaited<ReturnType<typeof serve>>;
17 let auth: Record<string, string> = {};
18 let roomLeft = true;
19 const spawned: string[] = [];
20
21 beforeAll(async () => {
22 dir = await mkdtemp(join(tmpdir(), "forge-newforge-lang-"));
23 app = createApp({ dataDir: dir, port: 0 });
24 useProbe(app, () => async () => true);
25 usePlacement(app, {
26 place: async (plan: { name: string }) => ok({ url: `https://${plan.name}.fake.test`, bodyId: `fake-${plan.name}` }),
27 retire: async () => ok(),
28 room: () => (roomLeft ? undefined : { message: "the test host is full" }),
29 } as never);
30 servers = await serve(app);
31 const made = await createUser(app.db, {
32 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
33 password: "a-good-passphrase", adminIfFirst: true,
34 });
35 if (!made.ok) throw new Error(made.error);
36 auth = { authorization: `Bearer ${createToken(app.db, made.value.id, "t", "admin").token}` };
37 });
38 afterAll(async () => {
39 for (const name of spawned) await bootHostedChild(app, name);
40 servers.stop();
41 app.close();
42 await rm(dir, { recursive: true, force: true });
43 });
44
45 const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
46 const boxed = ['class="card', "card-body"];
47
48 test("the form is a headed section, not a card", async () => {
49 const page = main(await (await fetch(servers.url + "/forges/new", { headers: auth })).text());
50 expect(page).toContain('<section class="settings-block">');
51 expect(page).toContain("<h2>Its name and apps</h2>");
52 for (const b of boxed) expect(page).not.toContain(b);
53 });

Add tests for the spawn answer (claim link in a settings-block, no card, cache-control: no-store, shown only in that body) and the already-exists answer; the headings added in madeView are otherwise untested and would not fail if reverted. Also cover the No room branch.

54
55 test("the plan is a headed section", async () => {
56 const res = await fetch(servers.url + "/forges/new", {
57 method: "POST",
58 headers: { ...auth, "content-type": "application/x-www-form-urlencoded", origin: servers.url },
59 body: new URLSearchParams({ name: "plan-lang", do: "plan" }),
60 });
61 const page = main(await res.text());
62 expect(page).toContain('<section class="settings-block" id="plan">');
63 expect(page).toContain("<h2>The plan</h2>");
64 for (const b of boxed) expect(page).not.toContain(b);
65 });
66
67 const spawn = (name: string) =>
68 fetch(servers.url + "/forges/new", {
69 method: "POST",
70 headers: { ...auth, "content-type": "application/x-www-form-urlencoded", origin: servers.url },
71 body: new URLSearchParams({ name }),
72 });
73
74 test("the answer holds the claim link in a headed section, once, never cached", async () => {
75 const res = await spawn("made-lang");
76 spawned.push("made-lang");
77 expect(res.status).toBe(201);
78 expect(res.headers.get("cache-control")).toBe("no-store");
79 const page = main(await res.text());
80 expect(page).toContain('<section class="settings-block">');
81 expect(page).toContain("<h2>The link to send</h2>");
82 expect(page).toContain('id="claim-link"');
83 for (const b of boxed) expect(page).not.toContain(b);
84 });
85
86 test("a forge that already exists says its link is not shown again", async () => {
87 const res = await spawn("made-lang");
88 expect(res.status).toBe(200);
89 const page = main(await res.text());
90 expect(page).toContain("<h2>Its link</h2>");
91 expect(page).toContain("made-lang already exists");
92 expect(page).not.toContain('id="claim-link"');
93 for (const b of boxed) expect(page).not.toContain(b);
94 });
95
96 test("with no room, one headed section says so and there is no form", async () => {
97 roomLeft = false;
98 try {
99 const page = main(await (await fetch(servers.url + "/forges/new", { headers: auth })).text());
100 expect(page).toContain("<h2>No room</h2>");
101 expect(page).toContain("the test host is full");
102 expect(page).not.toContain('id="new-forge"');
103 for (const b of boxed) expect(page).not.toContain(b);
104 } finally {
105 roomLeft = true;
106 }
107 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs