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

Settings › Move to another owner fits a 390 px phone, and the pull quotes its measured width #10321

Merged opened this

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

Merged

torque/forge:torque/10320-settings-move-to-another-owner-fits-a-39 → main 3 commits fork 4 threads · 4 unresolved

  • Merged. torque/10320-settings-move-to-another-owner-fits-a-39 was merged into main as cc97054.

Review

Sign in to review this change.
1 file changed +150 −0
test/settings-move-fit.test.ts added +150
@@ -0,0 +1,150 @@
1 /**

The header claims the refused owner and its refusal sentence are covered. The test never triggers a refusal; make the test do it or fix the comment.

2 * Settings › Move to another owner fits a 390 px phone (#10320). Three holds:
3 * the refused state (a 64-character unknown owner and its refusal sentence) is
4 * rendered inside the #move section; the stylesheet rules that lay out the
5 * field, its error and the actions row fix no width over 390 px and forbid no
6 * wrapping (rules inside @media/@supports/@container blocks included); and,
7 * where a Chromium runs, the page is measured at 390x844 both as served and
8 * with the refused section swapped in.
9 *
10 * Limit: the stylesheet holds read the source text of base, components and
11 * content, not computed layout; the Chromium measurement is what catches an
12 * overflow, and it is skipped where no Chromium starts.
13 */
14 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
15 import { mkdtemp, rm } from "node:fs/promises";
16 import { tmpdir } from "node:os";
17 import { join } from "node:path";
18 import { createApp, type App } from "../src/app.ts";
19 import { serve, type Servers } from "../src/http/server.ts";
20 import { utilities as base } from "../src/web/style/base.ts";
21 import { components } from "../src/web/style/components.ts";
22 import { content } from "../src/web/style/content.ts";
23 import { findChromium } from "../src/looks/capture.ts";
24 import { adminCookie } from "./helpers/admin.ts";
25 import { chromiumRuns, evaluateLoaded } from "./helpers/chrome.ts";
26
27 const LONG = "o".repeat(64);
28 let dir: string;
29 let app: App;
30 let servers: Servers;
31 let cookie: string;
32 let refused: string;
33
34 beforeAll(async () => {
35 dir = await mkdtemp(join(tmpdir(), "forge-move-fit-"));
36 app = createApp({ dataDir: dir, port: 0 });
37 servers = await serve(app);
38 cookie = await adminCookie(app, { username: "olive", email: "[email protected]", password: "settings-password" });
39 await fetch(`${servers.url}/new`, {
40 method: "POST",
41 headers: { cookie },
42 body: new URLSearchParams({ name: "proj", visibility: "private", description: "x" }),
43 redirect: "manual",
44 });
45 const res = await fetch(`${servers.url}/olive/proj/settings/transfer`, {
46 method: "POST",
outdated

The regexes only scan the section's HTML, not the CSS that gives the field its width, so they pass whatever the stylesheet does and cannot catch an overflow. Assert against the stylesheet rules for the form field and actions classes (no fixed width, no nowrap), or rely on the measured phone look.

47 headers: { cookie, origin: servers.url },
48 body: new URLSearchParams({ owner: LONG }),
49 redirect: "manual",
50 });
51 expect(res.status).toBe(422);
52 refused = await res.text();
53 });
54
55 afterAll(async () => {
56 servers.stop();
57 app.close();
58 await rm(dir, { recursive: true, force: true });
59 });
60
61 function moveSection(page: string): string {
62 const from = page.indexOf('id="move"');
63 expect(from).toBeGreaterThan(-1);
64 return page.slice(from, page.indexOf("</section>", from));
65 }
66
67 describe("Move to another owner, refused", () => {
68 test("the long owner and its refusal sentence render inside the section", () => {
outdated

rulesFor matches '[^{}@]+{...}' so rules nested in @media are mis-parsed or skipped; a width over 390 px added in a media query would pass this test. Cover media blocks or state the limit here.

69 const section = moveSection(refused);
70 expect(section).toContain(`value="${LONG}"`);
71 expect(section).toMatch(/class="error"[^>]*>[^<]+/);
72 expect(section).not.toMatch(/\b(?:min-)?width\s*[:=]|nowrap|white-space|\bsize="/i);
73 });
74 });
75
76 /**
77 * Every `selector { body }` in `css`, with conditional group rules unwrapped
78 * first so a rule inside `@media (…) { … }` is read like a top-level one; the
79 * group's closing brace is left behind as a separator.
80 */
81 function rules(css: string): { selector: string; body: string }[] {
82 const flat = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/@(?:media|supports|container|layer)[^{;]*\{/g, "");
83 return [...flat.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({ selector: m[1]!.trim(), body: m[2]! }));
84 }
85
86 describe("the rules that lay the section out", () => {
87 const css = [base, components, content].join("\n");
88 const rulesFor = (sel: RegExp, from = css) => rules(from).filter((r) => sel.test(r.selector)).map((r) => r.body);
89 const selectors = /(?:^|[\s,])(?:\.field\b|\.input\b|\.form-actions\b|\.settings-block\b|\.muted\b)/;
90
91 test("they exist", () => {
92 expect(rulesFor(selectors).length).toBeGreaterThan(5);
93 expect(rulesFor(/\.form-actions\s*$/).join("")).toContain("flex-wrap: wrap");
94 expect(rulesFor(/\.field .error/).join("")).toContain("overflow-wrap: anywhere");
95 });
96
outdated

This is the only assertion that can fail on a real overflow and it is skipped where Chromium does not start. Say in the pull whether it ran in the check, and quote the logged measurement in the description (box 1).

97 test("a rule inside a media block is read", () => {
98 const nested = "@media (max-width: 600px) {\n .x { color: red; }\n .field .input { width: 480px; }\n}\n.y { margin: 0; }";
99 expect(rulesFor(selectors, nested)).toEqual([" width: 480px; "]);
100 expect(rulesFor(/\.y$/, nested)).toEqual([" margin: 0; "]);
101 });
102
103 test("none fixes a width over 390 px or forbids wrapping", () => {
104 const over: string[] = [];
105 for (const body of rulesFor(selectors)) {
106 expect(body).not.toMatch(/white-space:\s*(?:nowrap|pre)\b/);
107 for (const m of body.matchAll(/(?<![-\w(])(?:min-)?width\s*:\s*(-?[\d.]+)(px|rem|em|ch)\b/g)) {
108 const px = Number(m[1]) * (m[2] === "px" ? 1 : m[2] === "ch" ? 8 : 16);
109 if (px > 390) over.push(m[0]);
110 }
111 }
112 expect(over).toEqual([]);
113 });
114 });
115
116 const found = findChromium();
117 const chromium = found !== undefined && chromiumRuns(found) ? found : undefined;
118 if (chromium === undefined) console.log("SKIPPING: no chromium that starts on this machine, so the Move phone look is not measured");
119
120 describe.skipIf(chromium === undefined)("Settings › Move, measured at 390x844", () => {
121 const measureWith = (section: string | null) => `(() => {
122 const swap = ${JSON.stringify(section)};
123 const el = document.getElementById("move");
124 if (swap !== null && el) el.outerHTML = swap;
125 const move = document.getElementById("move");
126 const vw = window.innerWidth;
127 const scope = move ? [move, ...move.querySelectorAll("*")] : [];
128 const overflowing = scope.filter((e) => e.getBoundingClientRect().right > vw + 1).map((e) => e.tagName.toLowerCase());
129 return JSON.stringify({ scrollWidth: document.documentElement.scrollWidth, innerWidth: vw, scoped: scope.length, error: !!move?.querySelector(".error"), overflowing });
130 })()`;
131 const run = async (label: string, section: string | null) => {
132 const name = cookie.slice(0, cookie.indexOf("="));
133 const session = { name, value: cookie.slice(name.length + 1) };
134 const m = JSON.parse(await evaluateLoaded(chromium!, `${servers.url}/olive/proj/settings#move`, { width: 390, height: 844 }, [session], measureWith(section))) as { scrollWidth: number; innerWidth: number; scoped: number; error: boolean; overflowing: string[] };
135 console.log(`settings#move ${label} 390x844: ${JSON.stringify(m)}`);
136 expect(m.scoped).toBeGreaterThan(0);
137 expect(m.overflowing).toEqual([]);
138 expect(m.scrollWidth).toBeLessThanOrEqual(m.innerWidth);
139 return m;
140 };
141 test("as served, no sideways scroll", async () => {
142 await run("served", null);
143 }, 30_000);
144 test("refused with a 64-character owner, no sideways scroll", async () => {
145 const section = refused.slice(refused.lastIndexOf("<section", refused.indexOf('id="move"')), refused.indexOf("</section>", refused.indexOf('id="move"')) + "</section>".length);
146 expect(section.startsWith('<section class="settings-block" id="move"')).toBe(true);
147 const m = await run("refused", section);
148 expect(m.error).toBe(true);
149 }, 30_000);
150 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs