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

A proposed bet card is bounded at rest: a test holds its lines, and the owner's bets look on the preview shows it at 390 px #9253

Merged opened this

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

Merged

scout/forge:scout/9240-a-proposed-bet-card-is-bounded-at-rest-a → main 3 commits fork

  • Merged. scout/9240-a-proposed-bet-card-is-bounded-at-rest-a was merged into main as 55f0405.

Review

Sign in to review this change.
3 files changed +96 −4
src/web/pages/bets.ts +6 −4
@@ -72,7 +72,8 @@ function oneButton(repo: Repo, b: Bet, verb: string, label: string, variant: "da
7272 }
7373
7474 /** Journeys shown at rest; the rest sit behind a disclosure so the card stays one screen tall. */
75 const SHOWN_JOURNEYS = 3;
75 /** Exported for test/bets-card-fit.test.ts, which holds the page to it. */
76 export const SHOWN_JOURNEYS = 3;
7677
7778 /**
7879 * A proposed bet as one card for the owner: name, journeys, its issues as a
@@ -95,13 +96,14 @@ function proposedCard(ctx: Ctx, repo: Repo, b: Bet, backed: readonly Bet[]): Htm
9596 ${journeys.length <= SHOWN_JOURNEYS ? null : html`<details><summary>${journeys.length - SHOWN_JOURNEYS} more ${journeys.length - SHOWN_JOURNEYS === 1 ? "journey" : "journeys"}</summary>
9697 <ul>${journeys.slice(SHOWN_JOURNEYS).map((j) => html`<li>${j}</li>`)}</ul></details>`}
9798 ${issues}
99 <div class="bar mt" data-bet-actions>
98100 <form method="post" action="${post(repo, b, "back")}">
99101 <input type="hidden" name="share" value="${String(b.appetite)}">
100102 ${btn(`Back it at ${b.appetite}%`, { variant: "primary", disabled: why !== null })}
101 ${why === null ? null : html`<p class="muted" role="note">${why[0]!.toUpperCase() + why.slice(1)}.</p>`}
102103 </form>
103 <details class="mt"><summary>Change the share</summary>${backForm(repo, b, "Back it")}</details>
104 <div class="mt">${oneButton(repo, b, "cut", "Cut it", "danger")}</div></div>`,
104 <details><summary>Change the share</summary>${backForm(repo, b, "Back it")}</details>
105 ${oneButton(repo, b, "cut", "Cut it", "danger")}</div>
106 ${why === null ? null : html`<p class="muted" role="note">${why[0]!.toUpperCase() + why.slice(1)}.</p>`}</div>`,
105107 );
106108 }
107109
src/web/style/content.ts +1
@@ -148,6 +148,7 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
148148 [data-apps], [data-bet-card] { overflow-wrap: anywhere; }
149149 [data-bet-card] h2, [data-bet-card] ul, [data-bet-card] p { margin-block: 8px; }
150150 [data-bet-card] ul { padding-inline-start: 16px; }
151 [data-bet-actions] { align-items: flex-start; }
151152
152153 .bar > .device-code { flex: 1 1 auto; }
153154
test/bets-card-fit.test.ts added +89
@@ -0,0 +1,89 @@
1 /**
2 * A proposed bet card is bounded at rest (#9240): the preview seed's proposed
3 * bets, rendered as their owner, hold one title, the journeys the page
4 * allows, one row of actions, no fixed width, and words that wrap.
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { createHmac } from "node:crypto";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { serve, type Servers } from "../src/http/server.ts";
13 import { seedPreview } from "../src/spawn/preview-seed.ts";
14 import { PREVIEW_ADMIN, PREVIEW_REPO } from "../src/spawn/preview-looks.ts";
15 import { utilities as base } from "../src/web/style/base.ts";
16 import { content } from "../src/web/style/content.ts";
17 import { SHOWN_JOURNEYS } from "../src/web/pages/bets.ts";
18
19 const KEY = "bets-card-fit-key";
20 let dir: string;
21 let app: App;
22 let servers: Servers;
23 let cards: string[];
24
25 beforeAll(async () => {
26 dir = await mkdtemp(join(tmpdir(), "forge-bets-fit-"));
27 app = createApp({ dataDir: dir, port: 0, previewIdentityKey: KEY });
28 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
29 if (!seeded.ok) throw new Error(seeded.error);
30 servers = await serve(app);
31 const said = `u1|trevato|admin|${Date.now() + 60_000}`;
32 const headers = { "x-forge-identity": said, "x-forge-signature": createHmac("sha256", KEY).update(said).digest("base64url") };
33 const html = await (await fetch(`${servers.url}/${PREVIEW_ADMIN}/${PREVIEW_REPO}/bets`, { headers })).text();
34 cards = html.split("<div data-bet-card").slice(1).map((c) => c.slice(0, c.indexOf("Cut it") + 40));
35 }, 30_000);
36 afterAll(async () => {
37 servers.stop();
38 app.close();
39 await rm(dir, { recursive: true, force: true });
40 });
41
42 describe("the owner's bets page, a proposed bet card at rest", () => {
43 test("the owner sees both proposed bets of the seed as cards", () => {
44 expect(cards.length).toBe(2);
45 expect(cards.some((c) => c.includes("Price the list"))).toBe(true);
46 expect(cards.some((c) => c.includes("Make every long and winding"))).toBe(true);
47 });
48
49 test("one title line", () => {
50 for (const c of cards) expect(c.match(/<h2[ >]/g)?.length).toBe(1);
51 });
52
53 test("at most the journeys the page allows, the rest behind a disclosure", () => {
54 for (const c of cards) {
55 const [atRest] = c.split("<details>");
56 expect(atRest!.match(/<li>/g)?.length ?? 0).toBeLessThanOrEqual(SHOWN_JOURNEYS);
57 }
58 const long = cards.find((c) => c.includes("Fifth journey"))!;
59 expect(long).toContain("2 more journeys");
60 expect(long.split("<details>")[0]!.match(/<li>/g)?.length).toBe(SHOWN_JOURNEYS);
61 });
62
63 test("one row of actions, holding back, change the share and cut", () => {
64 for (const c of cards) {
65 expect(c.match(/data-bet-actions/g)?.length).toBe(1);
66 const row = c.slice(c.indexOf("data-bet-actions"));
67 expect(row).toContain("Back it at 20%");
68 expect(row).toContain("Change the share");
69 expect(row).toContain("Cut it");
70 expect(c.slice(0, c.indexOf("data-bet-actions"))).not.toContain("<form");
71 }
72 // What keeps the row inside 390 px: .bar is a flex row that wraps.
73 expect(base).toMatch(/^\.bar \{[^}]*display:\s*flex[^}]*flex-wrap:\s*wrap/m);
74 });
75
76 test("no element with a fixed width", () => {
77 for (const c of cards) {
78 expect(c).not.toMatch(/\swidth=/i);
79 expect(c).not.toMatch(/style="[^"]*(?:^|[;\s])(?:min-|max-)?width\s*:/i);
80 }
81 for (const l of base.split("\n").filter((l) => /^\.bar\b/.test(l))) expect(l).not.toMatch(/(?<![-\w])(?:min-|max-)?width\s*:/);
82 const rules = content.split("\n").filter((l) => l.includes("[data-bet-card]"));
83 for (const r of rules) expect(r).not.toMatch(/(?<![-\w])width\s*:\s*\d/);
84 });
85
86 test("long words wrap", () => {
87 expect(content).toMatch(/\[data-bet-card\][^{]*\{[^}]*overflow-wrap:\s*anywhere/);
88 });
89 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs