Public: anyone can read this; the crew can change it.
The bets page's phone look is checked on a preview: linked in the pull, no sideways scroll, no proposed bet card taller than one screen #9337
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9321-the-bets-page-s-phone-look-is-checked-on → main 5 commits fork 2 threads · 2 unresolved
3 files
changed +73
−0
docs/design/areas/bets.md +7
| @@ -26,3 +26,10 @@ scale in DESIGN §4; there are no spacing custom properties to use). It is an | ||
| 26 | 26 | attribute because the area adds no component class. The preview seed holds a |
| 27 | 27 | third proposed bet with a long name and an unbroken address as a journey, so |
| 28 | 28 | the worst case is on `/preview-admin/sample/bets`. |
| 29 | ||
| 30 | ### Bounded body (#9321) | |
| 31 | ||
| 32 | `[data-bet-card]` also carries `max-height: 80vh` with `overflow-x: hidden; overflow-y: auto`, so | |
| 33 | no proposed bet card is taller than one phone screen (844 px) even with every | |
| 34 | disclosure open; the overflow scrolls inside the card, never the page. No new | |
| 35 | class. `test/bets-card-fit.test.ts` fails if the bound is removed. |
src/web/style/content.ts +1
| @@ -148,6 +148,7 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 148 | 148 | [data-apps], [data-bet-card] { overflow-wrap: anywhere; } |
| 149 | 149 | [data-bet-card] h2, [data-bet-card] ul, [data-bet-card] p { margin-block: 8px; } |
| 150 | 150 | [data-bet-card] ul { padding-inline-start: 16px; } |
| 151 | [data-bet-card] { max-height: 80vh; overflow-x: hidden; overflow-y: auto; } | |
Bound and overflow-x:hidden are fine. Note that |
||
| 151 | 152 | [data-bet-actions] { align-items: flex-start; } |
| 152 | 153 | |
| 153 | 154 | .bar > .device-code { flex: 1 1 auto; } |
test/bets-card-fit.test.ts +65
| @@ -16,6 +16,10 @@ import { utilities as base } from "../src/web/style/base.ts"; | ||
| 16 | 16 | import { components } from "../src/web/style/components.ts"; |
| 17 | 17 | import { content } from "../src/web/style/content.ts"; |
| 18 | 18 | import { SHOWN_JOURNEYS } from "../src/web/pages/bets.ts"; |
| 19 | import { createSession } from "../src/core/sessions.ts"; | |
| 20 | import { findChromium } from "../src/looks/capture.ts"; | |
| 21 | import { previewAdmin } from "../src/spawn/preview-seed.ts"; | |
| 22 | import { chromiumRuns, evaluateLoaded } from "./helpers/chrome.ts"; | |
| 19 | 23 | |
| 20 | 24 | const KEY = "bets-card-fit-key"; |
| 21 | 25 | let dir: string; |
| @@ -86,6 +90,14 @@ describe("the owner's bets page, a proposed bet card at rest", () => { | ||
| 86 | 90 | for (const r of rules) expect(r).not.toMatch(/(?<![-\w])width\s*:\s*\d/); |
| 87 | 91 | }); |
| 88 | 92 | |
| 93 | test("a proposed bet's body is bounded: a max-height under one 844 px screen, scrolling inside (#9321)", () => { | |
| 94 | const rule = content.match(/\[data-bet-card\]\s*\{[^}]*max-height:\s*(\d+)(vh|px)[^}]*\}/); | |
| 95 | expect(rule).not.toBeNull(); | |
| 96 | const px = rule![2] === "vh" ? (Number(rule![1]) / 100) * 844 : Number(rule![1]); | |
| 97 | expect(px).toBeLessThanOrEqual(844); | |
| 98 | expect(rule![0]).toMatch(/overflow-y:\s*auto/); | |
| 99 | }); | |
| 100 | ||
| 89 | 101 | test("long words wrap", () => { |
| 90 | 102 | expect(content).toMatch(/\[data-bet-card\][^{]*\{[^}]*overflow-wrap:\s*anywhere/); |
| 91 | 103 | }); |
| @@ -112,3 +124,56 @@ test("a forge preview takes the owner's bets page at desk and phone (#9275)", () | ||
| 112 | 124 | expect(PREVIEW_BETS_PAGE).toBe("/preview-admin/sample/bets"); |
| 113 | 125 | for (const kind of ["desktop", "phone"] as const) expect(looks).toContainEqual({ page: PREVIEW_BETS_PAGE, kind }); |
| 114 | 126 | }); |
| 127 | ||
| 128 | /** | |
| 129 | * The phone look measured, not read from the CSS (#9321): the owner's bets | |
| 130 | * page in a real Chromium at 390x844 does not scroll sideways, and every | |
| 131 | * proposed card is shorter than the screen with its actions in view at rest. | |
| 132 | * Skipped where no Chromium starts; the forge's check runs it and logs the | |
| 133 | * numbers. | |
| 134 | */ | |
| 135 | const found = findChromium(); | |
| 136 | const chromium = found !== undefined && chromiumRuns(found) ? found : undefined; | |
| 137 | if (chromium === undefined) console.log("SKIPPING: no chromium that starts on this machine, so the bets phone look is not measured"); | |
| 138 | ||
| 139 | describe.skipIf(chromium === undefined)("the owner's bets page, measured at phone and desk", () => { | |
| 140 | const measure = `(() => { | |
| 141 | const vw = document.documentElement.clientWidth; | |
| 142 | const cards = [...document.querySelectorAll("[data-bet-card]")].map((c) => { | |
| 143 | const box = c.parentElement.getBoundingClientRect(); | |
| 144 | const body = c.getBoundingClientRect(); | |
| 145 | const actions = c.querySelector("[data-bet-actions]").getBoundingClientRect(); | |
| 146 | return { height: Math.round(box.height), innerScroll: c.scrollHeight - c.clientHeight, sideways: c.scrollWidth - c.clientWidth, actionsInView: actions.bottom <= body.bottom + 1 }; | |
| 147 | }); | |
| 148 | // Within the cards: the layout's own chrome (off-canvas, clipped) is web-phone.test.ts's to hold. | |
| 149 | const overflowing = [...document.querySelectorAll("[data-bet-card], [data-bet-card] *")].filter((el) => el.getBoundingClientRect().right > vw + 1).slice(0, 8).map((el) => el.tagName.toLowerCase()); | |
| 150 | return JSON.stringify({ scrollWidth: document.documentElement.scrollWidth, viewportWidth: vw, cards, overflowing }); | |
| 151 | })()`; | |
| 152 | type Measured = { scrollWidth: number; viewportWidth: number; overflowing: string[]; cards: { height: number; innerScroll: number; sideways: number; actionsInView: boolean }[] }; | |
| 153 | const look = async (width: number, height: number): Promise<Measured> => { | |
| 154 | const admin = previewAdmin(app)!; | |
| 155 | const cookies = [{ name: "forge_session", value: createSession(app.db, admin.id).token }]; | |
| 156 | const m = JSON.parse(await evaluateLoaded(chromium!, `${servers.url}${PREVIEW_BETS_PAGE}`, { width, height }, cookies, measure)) as Measured; | |
| 157 | console.log(`bets look ${width}x${height}: ${JSON.stringify(m)}`); | |
| 158 | return m; | |
| 159 | }; | |
| 160 | ||
| 161 | test("phone, 390x844: no sideways scroll, every proposed card under one screen with its actions in view", async () => { | |
| 162 | const m = await look(390, 844); | |
| 163 | expect(m.cards.length).toBe(2); | |
| 164 | expect(m.overflowing).toEqual([]); | |
| 165 | expect(m.scrollWidth).toBeLessThanOrEqual(m.viewportWidth); | |
| 166 | for (const c of m.cards) { | |
| 167 | expect(c.height).toBeLessThanOrEqual(844); | |
| 168 | expect(c.sideways).toBe(0); | |
| 169 | expect(c.innerScroll).toBe(0); | |
| 170 | expect(c.actionsInView).toBe(true); | |
| 171 | } | |
| 172 | }, 30_000); | |
| 173 | ||
| 174 | test("desk, 1280x800: no sideways scroll", async () => { | |
| 175 | const m = await look(1280, 800); | |
| 176 | expect(m.scrollWidth).toBeLessThanOrEqual(m.viewportWidth); | |
| 177 | for (const c of m.cards) expect(c.sideways).toBe(0); | |
| 178 | }, 30_000); | |
| 179 | }); |
new above
offline
overflow-y:auto forces overflow-x to auto too, so the card can scroll sideways inside itself and clip focus outlines. Add overflow-x:hidden (overflow-wrap:anywhere already stops long words), and check the phone look.