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

Settings › Teams, each crew team's charter and dial, speaks the home page's language #9913

Merged opened this

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

Merged

scout/forge:scout/9889-settings-teams-each-crew-team-s-charter → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/9889-settings-teams-each-crew-team-s-charter was merged into main as 3165ff9.

Review

Sign in to review this change.
2 files changed +96 −11
src/web/pages/teams.ts +11 −11
@@ -13,7 +13,7 @@ import { field, readForm } from "../form.ts";
1313 import { eventStream } from "../../http/sse.ts";
1414 import { html, raw, type Html } from "../html.ts";
1515 import { flashTo, page } from "../layout.ts";
16 import { btn, card, icon } from "../ui.ts";
16 import { btn, icon } from "../ui.ts";
1717 import { accountSections } from "./origins.ts";
1818
1919 const isAdminPerson = (ctx: Ctx): boolean =>
@@ -29,8 +29,9 @@ function regions(ctx: Ctx, timed = false): Record<string, string> | undefined {
2929 const changes = phase("changes", () => teamChanges(ctx.app, 10));
3030 const total = teams.reduce((n, t) => n + t.dial, 0);
3131 const cards = teams.map((t) =>
32 card(
33 html`<form method="post" action="/settings/teams/${t.slug}">
32 html`<section>
33 <h2>${icon("bot")} ${t.name}</h2>
34 <form method="post" action="/settings/teams/${t.slug}">
3435 <div class="field">
3536 <label for="charter-${t.slug}">Charter</label>
3637 <textarea class="input" id="charter-${t.slug}" name="charter" rows="3" maxlength="${String(CHARTER_MAX)}">${t.charter}</textarea>
@@ -56,9 +57,8 @@ function regions(ctx: Ctx, timed = false): Record<string, string> | undefined {
5657 <div class="form-actions">
5758 ${btn("Save", { variant: "primary", label: `Save ${t.name}` })}
5859 </div>
59 </form>`,
60 { head: html`${icon("bot")}<h2>${t.name}</h2>` },
61 ),
60 </form>
61 </section>`,
6262 );
6363 const out: Record<string, Html> = {
6464 "teams-total": html`${String(total)}%`,
@@ -66,12 +66,12 @@ function regions(ctx: Ctx, timed = false): Record<string, string> | undefined {
6666 "teams-changes":
6767 changes.length === 0
6868 ? html``
69 : card(
70 html`<ul class="small">${changes.map(
69 : html`<section>
70 <h2>Recent changes</h2>
71 <ul class="small">${changes.map(
7172 (c) => html`<li><span class="mono">${c.at.slice(0, 16)}</span> ${c.slug}: ${c.from} → ${c.to} <span class="muted">${c.actorName}</span></li>`,
72 )}</ul>`,
73 { head: html`<h2>Recent changes</h2>` },
74 ),
73 )}</ul>
74 </section>`,
7575 };
7676 return Object.fromEntries(Object.entries(out).map(([k, h]) => [k, h.text]));
7777 }
test/teams-language.test.ts added +85
@@ -0,0 +1,85 @@
1 /** Settings › Teams speaks the home page's language: one heading, a section per team, a plain list; 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 { createSession } from "../src/core/sessions.ts";
8 import { createUser, type User } from "../src/core/users.ts";
9 import { serve } from "../src/http/server.ts";
10 import { promote } from "./helpers/members.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let dir = "";
16 let app: ReturnType<typeof createApp>;
17 let servers: Awaited<ReturnType<typeof serve>>;
18 let cookie = "";
19 let other = "";
20 const person = async (username: string, adminIfFirst = false): Promise<User> => {
21 const made = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@forge.test`, adminIfFirst });
22 if (!made.ok) throw new Error(String(made.error));
23 return made.value;
24 };
25
26 beforeAll(async () => {
27 dir = await mkdtemp(join(tmpdir(), "forge-teams-lang-"));
28 app = createApp({ dataDir: dir, port: 0 });
29 servers = await serve(app);
30 const admin = await person("alice", true);
31 promote(app.db, admin);
32 cookie = `forge_session=${createSession(app.db, admin.id).token}`;
33 const bob = await person("bob");
34 other = `forge_session=${createSession(app.db, bob.id).token}`;
35 });
36 afterAll(async () => {
37 servers.stop();
38 app.close();
39 await rm(dir, { recursive: true, force: true });
40 });
41
42 const teamsMain = async (): Promise<string> => {
43 const res = await fetch(`${servers.url}/settings/teams`, { headers: { cookie }, redirect: "manual" });
44 expect(res.status).toBe(200);
45 const page = await res.text();
46 return page.slice(page.indexOf("<main"), page.indexOf("</main>"));
47 };
48
49 test("one heading, a section per team with its forms, no card", async () => {
50 const main = await teamsMain();
51 expect(main.match(/<h1[\s>]/g)?.length).toBe(1);
52 expect(main).not.toContain('class="card');
53 expect(main).not.toContain("card-body");
54 expect(main).toContain("<section>");
55 expect(main).toMatch(/<h2>[^]*?<\/h2>/);
56 expect(main).toMatch(/action="\/settings\/teams\/[a-z0-9-]+"/);

Principle 7 (rows, not boxes) is asserted for teams but not for the changes list. Seed one change and assert the Recent changes h2 and a plain ul, with no card, so that section is covered.

57 expect(main).toMatch(/id="teams-total">\d+%</);
58 });
59
60 test("one POST saves; Recent changes is then a plain h2 and ul, and the total is still a percentage", async () => {
61 const before = await teamsMain();
62 expect(before).not.toContain("Recent changes");
63 const slug = /action="\/settings\/teams\/([a-z0-9-]+)"/.exec(before)?.[1] ?? "";
64 const dial = new RegExp(`id="dial-${slug}"[^>]*value="(\\d+)"`).exec(before)?.[1] ?? "0";
65 const post = await fetch(`${servers.url}/settings/teams/${slug}`, {
66 method: "POST",
67 headers: { cookie, origin: servers.url, "content-type": "application/x-www-form-urlencoded" },
68 body: new URLSearchParams({ charter: "Ship small things.", dial }),
69 redirect: "manual",
70 });
71 expect(post.status).toBeLessThan(400);
72 const main = await teamsMain();
73 expect(main).toContain("Ship small things.");
74 expect(main).toMatch(/<h2>Recent changes<\/h2>\s*<ul[^>]*>\s*<li>/);
75 const section = main.slice(main.indexOf("<h2>Recent changes</h2>"));
76 expect(section).not.toContain("card");
77 expect(main).toMatch(/id="teams-total">\d+%</);
78 });
79
80 test("a non-admin still gets 404, on page and post", async () => {
81 const res = await fetch(`${servers.url}/settings/teams`, { headers: { cookie: other }, redirect: "manual" });
82 expect(res.status).toBe(404);
83 const post = await fetch(`${servers.url}/settings/teams/x`, { method: "POST", headers: { cookie: other }, redirect: "manual" });
84 expect(post.status).toBe(404);
85 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs