fix(scroll): keep saturated scroll gestures out of the status bar; gate Android replays from android/emulator (#1781 A1) - #1820
Conversation
…te Android replays from android/emulator (#1781 A1) `pnpm gate replay-android` failed 4/8 whenever it ran after the full-tier Android E2E (replays-nightly run 32107665052, job 95620294899): 05-app-lifecycle, 06-swipe-gestures and both fixture replays diverged under "A system surface covers the app". The E2E was not the cause. Reproduced on a pixel_7 / API 36 AVD with the same cutout geometry CI's `avdmanager --device pixel_7` produces (status bar 136px, not the 63px of a plain 1080x2400 skin): - `03-scroll-discovery.ad` runs `scroll up 3`. The scroll planner clamps travel to the viewport minus a 5% band, so the touch-down landed at y=120 — inside the 136px status bar — and pulled the notification shade instead of scrolling. On API 36 the app window is edge-to-edge, so the reported viewport starts at y=0 and includes that bar. - The shade then covered every replay until `04`'s `back` closed it. Native readdir order on the runner (03, 05, 06, fixture/02, fixture/01, 04, 01, 02) put four files in that window; the last green run (2026-07-30) had 04 right after 03, so the pull was masked. Fix in the product, not the lane: DEFAULT_EDGE_PADDING_FRACTION 0.05 -> 0.1 in the TS scroll planner and its Swift port. Every real Pixel has a cutout (5.7% of a Pixel 7's height) and an iPhone's Dynamic Island status bar is 6.9%, so any saturated `scroll up` opened the shade / Notification Center for real agents too. Parity vectors updated in both suites plus a Pixel 7 regression vector (1080x2400, amount 3 -> touch-down y=240 > 136). Second contamination the same order exposed once the shade was gone: `fixture/02-selector-routes-covered-diagnosis.ad` is a #1715 reproduction recipe that FAILS BY DESIGN at step 9 (covered-target refusal) and leaves the device in landscape, yet the gate enumerated `test/integration/replays/android` recursively. iOS keeps gate replays in `replays/ios/simulator` and fixture recipes in `replays/ios/fixture`; Android now mirrors that: the six Settings replays move to `replays/android/emulator`, `test:replay:android` points there, and `fixture/` stays E2E-owned (`full:fixture-replays` already runs 01 by path). android.yml and the workflow-evidence fixture follow the path; the replay-compat manifest keeps the historical paths it pins at released tags. Verified live (Pixel 7 geometry, API 36, --retries 0): control run at main head in CI order reproduces exactly CI's 4/8; with the fix, `pnpm gate replay-android` 6/6 in both native and CI order, and `03` leaves Settings on screen (scroll up 3 now touches down at y=240).
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Code-quality review (thermo-nuclear pass). Verdict: approve with two asks — one in-PR, one as a named follow-up. The diff is small (65 lines), the diagnosis is real and reproduced, and it makes the codebase more canonical (Android replays now mirror 1. The constant is a proxy for a fact the platform layer already knows — say so, and name the follow-up
I would not block on this — the inset-aware version needs a viewport contract change across the helper, the runner, and the planner, and this PR unblocks a nightly. But please (a) keep the comment framing the 10% as "must exceed the tallest known bar (Pixel 7 cutout 5.7%, Dynamic Island 6.9%)" — it already does, good — and (b) file the follow-up as an issue: gesture viewport should expose content-safe bounds (app window minus system bar windows / safe-area insets) so the planner clamps to real geometry and the fraction returns to a small generic margin. One sentence in the PR body linking it is enough. Also worth one data point before merge: does 80% max travel change any perf/scroll benchmark expectation ( 2. The TS↔Swift parity vectors are hand-mirrored; the repo already has the canonical pattern for thisThis PR had to update the same three vectors in 3. Root-cause of the masking: gate enumeration order is native readdir orderThe body notes the last green run had Minor
Nothing here changes behaviour further; approve once (1)'s follow-up issue exists and (2) is either done or named. |
…one table (#1820 review) The two suites hand-mirrored the same vectors and #1820 had to edit both by hand — the drift class the repo already closes for the tap-point rule via contracts/fixtures/tap-point-policy.json. The scroll vectors (plus both planner constants, pinned behaviourally on a 1000px axis) now live in contracts/fixtures/scroll-gesture.json; scroll-gesture.test.ts and RunnerTests+ScrollGesture.swift iterate it. Verified: vitest 10/10; the four XCTests run on an iOS 26.2 simulator with the unit flag on (Executed 4 tests, 0 failures). Also: test/ci/android-workflow-evidence.json says what it guards. Follow-up for content-safe viewport bounds + discovery order: #1821.
|
Addressed in 9f4670b:
🤖 Addressed by Claude Code |
|
Reviewed exact head 9f4670b: clean and merge-ready. The 10% scroll edge band is implemented at the shared TS/Swift planning seams, governed by one parity fixture, and directly validated against the Pixel 7 cutout failure. The replay directory split keeps normal Settings replays in the CI lane while leaving fixture recipes E2E-owned. #1821 appropriately tracks the deeper content-safe-bounds follow-up. All exact-head checks and smoke lanes are green. |
Refs #1781 (A1). Follow-up to #1793 / #1794: makes
pnpm gate replay-androidpass after the full-tier Android E2E.Symptom
Replay Nightly run 32107665052 (job 95620294899): the E2E passes 9/9, then the gate reports
4 passed (8), 4 failed—05-app-lifecycle,06-swipe-gestures,fixture/02,fixture/01all diverge with "A system surface (notification shade…) covers the app".Root cause (reproduced live; not an E2E leftover)
03-scroll-discovery.adrunsscroll up 3. The scroll planner clamps travel to the viewport minus a 5 % edge band, so on 1080×2400 the swipe touches down at y = 120. CI'savdmanager --device pixel_7AVD carries the Pixel 7 cutout, whose status bar is 136 px (a plain 1080×2400 skin gives 63 px — which is why a naive local repro doesn't show it). On API 36 the app window is edge-to-edge, so the reported viewport starts at y = 0: the touch-down landed inside the status bar and pulled the notification shade instead of scrolling. The shade then covered every replay until04'sbackclosed it. Native readdir order on the runner (03, 05, 06, fixture/02, fixture/01, 04, 01, 02) put four files in that window; the last green run (07-30) had04right after03, masking it.Evidence: CI's
03screenshots (Settings before/afterscroll down 3, shade afterwards); a control run at main head on a cutout AVD in CI order reproduces exactly CI's 4/8 with the same steps.Fix — in the product, not the lane
Every real Pixel has a cutout (5.7 % of a Pixel 7's height) and an iPhone's Dynamic Island bar is 6.9 %, so any saturated
scroll upopened the shade / Notification Center for real agents too. A "dismiss system surfaces" recovery in the replay lane would have hidden that.packages/contracts/src/scroll-gesture.ts:DEFAULT_EDGE_PADDING_FRACTION0.05 → 0.1, mirrored in the Swift port (RunnerTests+ScrollGesture.swift). Parity vectors updated in both suites plus a Pixel 7 regression vector (1080×2400, amount 3 → touch-down y=240 > 136). Cost: max single-gesture travel 90 % → 80 % of the axis; amounts ≤ 0.8 (default 0.6,scroll top/bottom) are unchanged.Second contamination the same order exposed
Once the shade was gone,
fixture/02-selector-routes-covered-diagnosis.ad— a #1715 reproduction recipe that fails by design at step 9 and leaves the device in landscape — broke the files after it, because the gate enumeratedtest/integration/replays/androidrecursively. iOS keeps gate replays inios/simulatorand recipes inios/fixture; Android now mirrors that: the six Settings replays move totest/integration/replays/android/emulator/,test:replay:androidpoints there, andfixture/stays E2E-owned (full:fixture-replaysalready runsfixture/01by path — this also removes that duplication).android.ymlandtest/ci/android-workflow-evidence.jsonfollow the path; the replay-compat manifest keeps the historical paths it pins at released tags. #1793's files are untouched.Verification
--retries 0:pnpm gate replay-android6/6 in both native and CI order;03leaves Settings on screen (scroll up 3now touches down at y=240).hoverframeworkTier parity) is pre-existing on main.Notes for reviewers
ios/simulator/03-scroll-discovery.adalso doesscroll up 3(possibly relevant to the parked iOS lane — not verified here).-only-testinglist; the nightly runs them.adb shell cmd overlay enable-exclusive --category com.android.internal.display.cutout.emulation.hole; without the IME helper packaged, Gboard stays up and04'sbackonly hides the keyboard, leaving the Settings search activity on top ofopen settings --relaunch— not a CI condition.Review follow-ups (thymikee)
WorkspaceExecutionPlannerusesFiles.walk→unsortedFlowFiles) is on the same issue rather than changed here.scripts/perfuses the default-amountscroll down(0.6) andscroll top/bottomloops the default too; onlyamount ≥ 0.8/ large explicit pixels lose travel (90 % → 80 % max).contracts/fixtures/scroll-gesture.json(vectors + both constants), iterated byscroll-gesture.test.tsandRunnerTests+ScrollGesture.swift(same#filePathpattern asRunnerTapPointPolicy.swift). Verified live: vitest 10/10, and the four XCTests executed on an iOS 26.2 simulator with the unit flag on (4/4).test/ci/android-workflow-evidence.jsonnow carries apurposefield saying what it guards.