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

An issue's page fits a 390 px phone, and its pull says so from the forge's own measurement #9394

Merged opened this

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

Merged

scout/forge:scout/9384-an-issue-s-page-fits-a-390-px-phone-and → main 7 commits fork 1 thread · 1 unresolved

  • Merged. scout/9384-an-issue-s-page-fits-a-390-px-phone-and was merged into main as 85cb207.

Review

Sign in to review this change.
5 files changed +116 −2
docs/design/areas/tell.md +3
@@ -10,6 +10,9 @@ shifts and Enter sends it with the page's path as `from`. Any failure falls back
1010 - `.tell-picture` — the picture a sender chose to send with a note, on its issue page: at most the card's width, height
1111 following (no layout shift beyond the image's own load).
1212
13 - `[data-tell]` — the wrapper of an issue's page (#9384): text wraps anywhere, the two columns may shrink, code scrolls
14 inside its own box. It is an attribute hook, not a component class.
15
1316 ## Client home: `client-app-finished`
1417
1518 A muted line on a client's app row, `Finished <time>: <title>`, for the app's
src/spawn/preview-looks.ts +4
@@ -69,6 +69,9 @@ export const PREVIEW_CHANGELOG_PAGE = "/changelog";
6969 /** The next-up page, whose long titles and action buttons must fit a 390 px phone (#9324). */
7070 export const PREVIEW_NEXT_PAGE = `${PREVIEW_REPO_HOME}/next`;
7171
72 /** The sample's first issue, whose long title and comments must wrap at a 390 px phone (#9384). */
73 export const PREVIEW_ISSUE_PAGE = `${PREVIEW_REPO_HOME}/issues/1`;
74
7275 /** The commits list, whose long messages must wrap at a 390 px phone (#9348). */
7376 export const PREVIEW_COMMITS_PAGE = `${PREVIEW_REPO_HOME}/commits`;
7477
@@ -112,6 +115,7 @@ export const PREVIEW_OWNER_LOOKS: readonly OwnerLook[] = [
112115 PREVIEW_NEXT_PAGE,
113116 PREVIEW_COMMITS_PAGE,
114117 PREVIEW_ISSUES_PAGE,
118 PREVIEW_ISSUE_PAGE,
115119 PREVIEW_RESILIENCE_PAGE,
116120 PREVIEW_SEARCH_PAGE,
117121 ].flatMap((page) => (["desktop", "phone"] as const).map((kind) => ({ page, kind })));
src/web/studio/tell.ts +2 −2
@@ -560,7 +560,7 @@ async function studioTell(
560560 { extra: "mt" },
561561 );
562562
563 const body = html`${repoHeader(ctx, repo, "issues", {
563 const body = html`<div data-tell>${repoHeader(ctx, repo, "issues", {
564564 compact: true,
565565 ownTitle: true,
566566 })}
@@ -600,7 +600,7 @@ async function studioTell(
600600 thread: "issue",
601601 number: issue.number,
602602 lastCommentId: talk.lastCommentId,
603 })}`;
603 })}</div>`;
604604
605605 const response = page({
606606 ctx,
src/web/style/content.ts +6
@@ -152,6 +152,12 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
152152 font-variant-numeric: tabular-nums; letter-spacing: 0.14em;
153153 text-align: center; user-select: all; word-break: break-all;
154154 }
155 /* An issue's page (#9384): other people's words wrap, the columns shrink, code scrolls in its box. */
156 [data-tell] { overflow-wrap: anywhere; }
157 [data-tell] .split > * { min-width: 0; }
158 [data-tell] .page-head-sub { flex-wrap: wrap; }
159 [data-tell] .comment-body img { max-width: 100%; height: auto; }

The .page-head-sub and .comment-body img rules have no assertion in test/issue-fit.test.ts. Add them to the test or remove them.

160 [data-tell] pre { max-width: 100%; overflow-x: auto; }
155161 [data-apps], [data-apps-page], [data-bet-card], [data-milestone], [data-now], [data-people] { overflow-wrap: anywhere; }
156162 [data-bet-card] h2, [data-bet-card] ul, [data-bet-card] p { margin-block: 8px; }
157163 [data-bet-card] ul { padding-inline-start: 16px; }
test/issue-fit.test.ts added +101
@@ -0,0 +1,101 @@
1 /**
2 * An issue's page fits a 390 px phone (#9384): seeded long content (an unbroken
3 * 300-character word, a long title, many comments) ships no fixed width over
4 * 390 px, and the long-text container wraps anywhere; code may scroll inside
5 * its own box. Where a Chromium starts, the page is measured at 390x844.
6 */
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createRepo } from "../src/actions/repos.ts";
12 import { addComment, openIssue } from "../src/actions/issues.ts";
13 import { createApp, type App } from "../src/app.ts";
14 import { createSession } from "../src/core/sessions.ts";
15 import { createUser } from "../src/core/users.ts";
16 import { serve, type Servers } from "../src/http/server.ts";
17 import { PREVIEW_ISSUE_PAGE, PREVIEW_OWNER_LOOKS } from "../src/spawn/preview-looks.ts";
18 import { utilities as base } from "../src/web/style/base.ts";
19 import { components } from "../src/web/style/components.ts";
20 import { content } from "../src/web/style/content.ts";
21 import { findChromium } from "../src/looks/capture.ts";
22 import { chromiumRuns, evaluateLoaded } from "./helpers/chrome.ts";
23
24 const WORD = "W".repeat(300);
25 const TITLE = "A very long title ".repeat(10).trim();
26 let dir: string;
27 let app: App;
28 let servers: Servers;
29 let page: string;
30 let cookie: string;
31
32 const must = <T>(r: { ok: true; value: T } | { ok: false; error: unknown }): T => {
33 if (!r.ok) throw new Error(JSON.stringify(r.error));
34 return r.value;
35 };
36
37 beforeAll(async () => {
38 dir = await mkdtemp(join(tmpdir(), "forge-issue-fit-"));
39 app = createApp({ dataDir: dir, port: 0 });
40 servers = await serve(app);
41 const u = must(await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true }));
42 const repo = must(await createRepo(app, u, { name: "fit", visibility: "public" }));
43 const issue = must(openIssue(app, u, repo, { title: TITLE, body: `${WORD}\n\n\`\`\`\n${"code ".repeat(80)}\n\`\`\`` }));
44 for (let i = 0; i < 15; i++) must(addComment(app, u, repo, issue, i === 0 ? WORD : `Comment ${i} ${"words ".repeat(20)}`));
45 cookie = `forge_session=${createSession(app.db, u.id).token}`;
46 page = await (await fetch(`${servers.url}/alice/fit/issues/${issue.number}`, { headers: { cookie } })).text();
47 }, 60_000);
48 afterAll(async () => {
49 servers.stop();
50 app.close();
51 await rm(dir, { recursive: true, force: true });
52 });
53
54 describe("an issue's page at 390 px", () => {
55 test("the long content is on the page, in many comments", () => {
56 expect(page).toContain(WORD);
57 expect(page).toContain("A very long title");
58 expect(page.match(/data-kind="comment"/g)?.length ?? 0).toBeGreaterThanOrEqual(10);
59 });
60
61 test("no inline width and no shipped rule fixes a width over 390 px", () => {
62 expect(page).not.toMatch(/style="[^"]*(?:^|[;\s])(?:min-|max-)?width\s*:/i);
63 for (const m of page.matchAll(/\swidth=["']?(\d+)/gi)) expect(Number(m[1])).toBeLessThanOrEqual(390);
64 const over: string[] = [];
65 for (const css of [base, components, content, ...(page.match(/<style[^>]*>[\s\S]*?<\/style>/g) ?? [])]) {
66 for (const m of css.matchAll(/(?<![-\w(])(?:min-)?width\s*:\s*(-?[\d.]+)(px|rem|em|ch)\b/g)) {
67 if (Number(m[1]) * (m[2] === "px" ? 1 : m[2] === "ch" ? 8 : 16) > 390) over.push(m[0]);
68 }
69 }
70 expect(over).toEqual([]);
71 });
72
73 test("the container wraps anywhere, and code scrolls inside its own box", () => {
74 expect(page).toContain("data-tell");
75 const css = content;
76 expect(css).toMatch(/\[data-tell\]\s*\{[^}]*overflow-wrap:\s*(anywhere|break-word)/);
77 expect(css).toMatch(/\[data-tell\] \.split > \*\s*\{[^}]*min-width:\s*0/);
78 expect(css).toMatch(/\[data-tell\] \.page-head-sub\s*\{[^}]*flex-wrap:\s*wrap/);
79 expect(css).toMatch(/\[data-tell\] \.comment-body img\s*\{[^}]*max-width:\s*100%/);
80 expect(css).toMatch(/\[data-tell\] pre\s*\{[^}]*overflow-x:\s*auto/);
81 });
82 });
83
84 test("the owner's phone look takes an issue's page", () => {
85 expect(PREVIEW_ISSUE_PAGE).toBe("/preview-admin/sample/issues/1");
86 expect(PREVIEW_OWNER_LOOKS).toContainEqual({ page: PREVIEW_ISSUE_PAGE, kind: "phone" });
87 });
88
89 const found = findChromium();
90 const chromium = found !== undefined && chromiumRuns(found) ? found : undefined;
91 if (chromium === undefined) console.log("SKIPPING: no chromium that starts here, so the issue page is not measured");
92
93 describe.skipIf(chromium === undefined)("measured at 390x844", () => {
94 test("no sideways scroll", async () => {
95 const measure = `(() => JSON.stringify({ scrollWidth: document.documentElement.scrollWidth, viewportWidth: document.documentElement.clientWidth,
96 comments: document.querySelectorAll(".comment").length }))()`;
97 const m = JSON.parse(await evaluateLoaded(chromium!, `${servers.url}/alice/fit/issues/1`, { width: 390, height: 844 }, [{ name: "forge_session", value: cookie.split("=")[1]! }], measure));
98 console.log(`issue look 390x844: ${JSON.stringify(m)}`);
99 expect(m.scrollWidth).toBeLessThanOrEqual(m.viewportWidth);
100 }, 30_000);
101 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs