Repository navigation
Description
What
tools/simulation/ui/playwright.config.ts declares one project — chromium, spreading devices["Desktop Chrome"] and nothing else. devices["Desktop Chrome"] carries a 1280×720 viewport. So every spec in the smoke suite, on every pull request since 2026-08-08, has only ever rendered the app on a desktop-sized window.
The app's primary context is a phone in a hen house. #674 chose direction B — field-first phone — on exactly that reasoning. Nothing automated has ever looked at it.
What this let through
#740. A Confirm order button whose label wraps at phone width, making the box taller than it is wide; border-radius: 999px then resolves to an ellipse the label renders outside of. Measured on the running sim stack:
| width | height | |
|---|---|---|
| en @420 | 100.4 | 103.2 |
| tl @420 | 115.7 | 103.2 |
| en @1280 | 285.7 | 40.1 |
| tl @1280 | 390.4 | 40.1 |
At 1280 both are a single 40px line and render correctly. That is why the suite never saw it.
The detail that makes this worth fixing rather than filing under i18n: #740 was filed as a Tagalog/Spanish bug, and reproducing it showed English is broken too — en at 420px has the worst aspect ratio of the three. So this is not a gap in locale coverage that a translated run would have closed. It is a gap in viewport coverage, and an English-only phone-width run would have caught it on day one.
Scope
- A second project in
playwright.config.tsat a phone viewport.devices["Pixel 7"]or an explicit{ width: 420, height: 900 }— 420px is the width web: long tl/es strings overflow fixed-width controls on narrow screens #740 reproduces at and is a realistic low end. - Decide which specs run there. Running all of them doubles a suite that is on the pull-request critical path (~3 minutes, 10-minute cap). A
grep-selected subset — the screens with action bars and dialogs — is probably right, and mirrors howplaywright.screenshots.config.tsalready splits achromium-dashboardproject out bygrep. - Note the shell differs below 901px: the sidebar gives way to
BottomNav.fixtures.ts'ssignInasserts ongetByRole("complementary"), which is the sidebar, so a spec that signs in at phone width needs either a resize after sign-in or a different shell assertion. Reproducing web: long tl/es strings overflow fixed-width controls on narrow screens #740 hit exactly this — worth knowing before the first spec is written.
Out of scope
Fixing #740. That is moving to the #674 revamp epic, because the remedy is a decision about how action buttons lay out on a phone. This issue is the missing gate, not the fix — and it is worth landing independently, because the gate is what stops the next one.
Acceptance
- The suite renders at least one phone-width project on every pull request that already triggers
e2e-smoke.yml. - A deliberately over-long button label at 420px fails the new project and passes the desktop one — prove it with a mutation, not by assertion (
AGENTS.md: mutation first, claim second). - The pull-request run stays inside its existing timeout.
Activity
mforce commentedon Sep 13, 2026
Handoff — #814, phone viewport in the E2E suite
Written 2026-09-13 so this can be picked up cold. Everything below is read from the repo or measured on a running stack, not recalled. Nothing here is started.
The one-line problem
tools/simulation/ui/playwright.config.ts:69-84declares one project. It spreadsdevices["Desktop Chrome"]and overrides nothing butlaunchOptions. That device carries a 1280×720 viewport, so every spec, on every pull request since 2026-08-08, has only ever rendered the app in a desktop window.Why it matters more here than in most apps
The product's primary context is a phone in a hen house — that is the stated reason #674 chose direction B, field-first phone. The one context the app is designed around is the one nothing automated has ever looked at.
The proof it let something through
#740, measured on the sim stack at
127.0.0.1:8081:At 1280 both locales are a single 40px line and render correctly. At 420 the button is taller than it is wide,
border-radius: 999pxclamps into an ellipse, and the label renders outside its own background. English is the worst aspect ratio of the three, so a translated test run would not have caught it either — this is a viewport gap, not a locale gap. Screenshots are on #740.Where to make the change
tools/simulation/ui/playwright.config.ts, theprojectsarray. The pattern to copy already exists in this repo:playwright.screenshots.config.tssplits a second project out bygrep/grepInvertand gives it its own viewport, with a comment explaining why the viewport override must come after thedevices[...]spread — a project'susebeats the top-level one, and setting the frame above the spread silently captures at the device's size instead. Same trap applies here.Four things that will bite, in the order they will bite
1.
signInasserts on the sidebar, which does not exist at phone width.src/fixtures.ts'ssignInends withawait expect(page.getByRole("complementary")).toBeVisible()— the<aside>.web/src/styles.css:2798is@media (max-width: 900px)and sets.sidebar { display: none }, replacing it withBottomNav. So a spec that signs in at 420px hangs and then fails at "waiting for complementary", which reads like a broken login. Two ways out: sign in at desktop width and resize afterwards (what the #740 reproduction did, and it works), or give the fixture a shell assertion that holds at both widths. Prefer the second — the first leaves every phone spec carrying a desktop step it does not want.2.
shellNavis sidebar-scoped and will match nothing.shellNavscopesprimaryto the sidebar'snavigationlandmark precisely becauseBottomNavrenders the same destinations and an unscopedgetByRole("link")is strict-mode-ambiguous. At phone width the sidebar is gone, sonav.link(...)finds nothing. Any spec usingnavneeds a phone-aware variant. NotesignOuthas the same problem from the other side: it lives in the sidebar foot on desktop and insideBottomNav's More sheet on a phone, which is a dialog portalled to<body>and only exists while open.3. The pull-request run has a 10-minute cap, and the suite is ~3 minutes.
.github/workflows/e2e-smoke.yml:83setstimeout-minutes: 10onpull_request(45 on dispatch). The measured job is ~3 minutes wall for image build + boot + migrate + seed + 28 specs. Running all 14 spec files at a second viewport roughly doubles the spec portion, which is the part inside the cap. Select a subset bygreprather than running everything. The specs worth having at phone width are the ones with action bars, dialogs and tables —sales,worker,manager,owner,named-entity-picker,pagination.session-refreshandsession-racesare about token lifetime and have nothing viewport-dependent;i18npersists a language change server-side and is the slowest to make safe.4.
i18n.spec.tspersists language against a dedicated persona for a reason.cast.ts'si18nPersona()exists becauseLanguageSelectorcallsPUT /me/language, so switching language is durable against that user. If a phone spec switches language, it must use that persona and restore English in afinally, or it poisons later runs of the persona specs. This is documented at length incast.ts— read it before adding any language handling.Acceptance, and the one part that is not optional
e2e-smoke.yml.AGENTS.mdis explicit — mutation first, claim second, and never write "this catches X" before running the mutation that makes it go red. This repo has the harness for it:mutation-check.sh, with mutants insrc/mutants.ts, and its three-phase shape (baseline green → mutant red → restore green) is what a new guard is expected to pass through. A mutant that lengthens a button label, or shrinks the.actionsflex basis, should turn the phone project red and leave the desktop project green. A phone project that cannot demonstrate that is a wrong guard, and a wrong guard reads as safety.Out of scope
Fixing #740. That moved to the #674 revamp epic because the remedy is a decision about how action buttons lay out on a phone. This issue is the gate, not the fix, and it is worth landing first — the gate is what stops the next one.
test(e2e): catch a wrapped action label at phone width (#814)
docs(e2e): give the wrapped-label measurement one home (#814)
test(e2e): drop the layout-viewport assertion that could not fail (#814)
test(e2e): close the vacuity holes an adversarial pass found (#814)