mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
f843dc2df1
* fix(scroll): keep saturated scroll gestures out of the status bar; gate 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). * test(scroll): drive the TS and Swift scroll-plan parity vectors from 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.
65 lines
2.3 KiB
JSON
65 lines
2.3 KiB
JSON
{
|
|
"constants": {
|
|
"defaultScrollAmount": 0.6,
|
|
"defaultEdgePaddingFraction": 0.1
|
|
},
|
|
"cases": [
|
|
{
|
|
"name": "relative amount maps to viewport travel: 400x800 down 0.5 -> (200,600)->(200,200), travel 400",
|
|
"direction": "down",
|
|
"amount": 0.5,
|
|
"referenceWidth": 400,
|
|
"referenceHeight": 800,
|
|
"expected": { "x1": 200, "y1": 600, "x2": 200, "y2": 200, "pixels": 400 }
|
|
},
|
|
{
|
|
"name": "explicit pixels below the safe band cap: 300x600 down 120px -> (150,360)->(150,240)",
|
|
"direction": "down",
|
|
"pixels": 120,
|
|
"referenceWidth": 300,
|
|
"referenceHeight": 600,
|
|
"expected": { "x1": 150, "y1": 360, "x2": 150, "y2": 240, "pixels": 120 }
|
|
},
|
|
{
|
|
"name": "amount above 1 clamps to the safe band: 400x800 down 2 -> requested 1600 clamps to 640, (200,720)->(200,80)",
|
|
"direction": "down",
|
|
"amount": 2,
|
|
"referenceWidth": 400,
|
|
"referenceHeight": 800,
|
|
"expected": { "x1": 200, "y1": 720, "x2": 200, "y2": 80, "pixels": 640 }
|
|
},
|
|
{
|
|
"name": "explicit pixels clamp to the vertical safe band: 400x800 down 1000px -> 640, (200,720)->(200,80)",
|
|
"direction": "down",
|
|
"pixels": 1000,
|
|
"referenceWidth": 400,
|
|
"referenceHeight": 800,
|
|
"expected": { "x1": 200, "y1": 720, "x2": 200, "y2": 80, "pixels": 640 }
|
|
},
|
|
{
|
|
"name": "tiny frame engages every max(1, ...) floor and the .5 rounding both ports must agree on: 2x2 down 10px -> (1,2)->(1,0), travel 1",
|
|
"direction": "down",
|
|
"pixels": 10,
|
|
"referenceWidth": 2,
|
|
"referenceHeight": 2,
|
|
"expected": { "x1": 1, "y1": 2, "x2": 1, "y2": 0, "pixels": 1 }
|
|
},
|
|
{
|
|
"name": "explicit pixels clamp to the horizontal safe band: 300x600 right 500px -> 240, (270,300)->(30,300)",
|
|
"direction": "right",
|
|
"pixels": 500,
|
|
"referenceWidth": 300,
|
|
"referenceHeight": 600,
|
|
"expected": { "x1": 270, "y1": 300, "x2": 30, "y2": 300, "pixels": 240 }
|
|
},
|
|
{
|
|
"name": "#1781 A1: a saturated scroll up on a Pixel 7 (1080x2400) touches down at y=240, clear of its 136px cutout status bar; the 5% band started at y=120 and pulled the notification shade",
|
|
"direction": "up",
|
|
"amount": 3,
|
|
"referenceWidth": 1080,
|
|
"referenceHeight": 2400,
|
|
"expected": { "x1": 540, "y1": 240, "x2": 540, "y2": 2160, "pixels": 1920 }
|
|
}
|
|
]
|
|
}
|