Skip to content

fix: take a mobile full page screenshot again when it still shows the frame from before the scroll - #1346

Draft
plum117 wants to merge 2 commits into
webdriverio:mainfrom
plum117:fix/mobile-full-page-stale-frame
Draft

plum117 wants to merge 2 commits into
webdriverio:mainfrom
plum117:fix/mobile-full-page-stale-frame

Conversation

@plum117

@plum117 plum117 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Why

On a busy device (a new emulator or simulator, a CI runner), the native screenshot after a scroll can still show the frame from before the scroll, also after fullPageScrollTimeout. The full page image then has a part of the page two times, and a part is missing. This is the common cause of the mobile e2e flakes in CI: the Android emulator job (for example run 38074791965: the baseline repeated a tile, 0.158 %) and the iOS simulator job (first viewport again, or an older tile again).

What

getMobileFullPageNativeWebScreenshotsData (iOS, and Android with native web screenshots):

  • When the page scrolled, but the part of the new screenshot that goes into the image has exactly the same pixels as in the previous screenshot, the screenshot is the old frame: the service waits (fullPageScrollTimeout, at least 500 ms) and takes it again, up to 3 times.
  • When the part is still the same after 3 new screenshots (for example a page with two equal parts), it keeps the screenshot and logs a warning that tells to increase fullPageScrollTimeout. A whole page takes at most 5 new screenshots, with one warning, so a page that looks the same after each scroll (a fixed overlay that fills the screen) costs at most about 2.5 s.
  • Each screenshot is decoded once: the decoded previous screenshot is kept for the next comparison.
  • An area in one color (a blank part of the page) is not checked: a new screenshot of it can not give another image, so this costs no time and gives no false warning.
  • The comparison uses the same rotation as the full page image (landscape).
  • New helpers hasSameArea and isUniformArea in utils/imageUtils.ts. A changeset (patch).

Android with ChromeDriver screenshots and desktop are not changed: there the browser gives the screenshot from its own compositor.

Test

  • Unit tests: a new screenshot after an old frame, the minimum wait of 500 ms, the warning after 3 new screenshots, no new screenshot when the page did not scroll or when the area is one color, the landscape rotation; hasSameArea and isUniformArea.
  • Local: the iOS web suite on an iOS 26.3 simulator and the Android web suite on an Android 16 emulator pass (baseline and compare runs).
  • CI on the fork (fix only for Android, fix plus a prebuilt WebDriverAgent for iOS, with a log line for each old frame): 6 iOS jobs passed; in one of them the screenshot after the scroll to 4914 px was the old frame two times, and the second new screenshot was right (without the fix, that image has a repeated part). 5 Android jobs passed without a visual failure (one more job failed before the tests: adb could not start a session).
  • pnpm test (lint, types, unit tests) passes.

CI job times

Job (start to end) Other PRs and main (median of 22–24 runs) This PR (2 runs) Change
iOS simulator 22.3 min 24.3 / 17.7 min none measurable
Android emulator 7.6 min 10.2* / 7.5 min none measurable
One mobile full page screenshot — — +0.2 to +0.4 s

* The warm-up hit the known viewport flake and a 5-minute request timeout before any screenshot. The gain of this PR is right images on a busy device, not time.

Battle test: main against this PR on real devices

The same script against both builds (saveFullPageScreen of a page of 20 stripes of 303 px; the image is right when every stripe is there once, at a fixed step), on an Android 16 emulator (native web screenshots, DPR 3) and an iOS 26.3 simulator (iPhone 16 Pro, DPR 3).

Performance (default fullPageScrollTimeout, 2 alternating rounds of 5 runs per build in the same device session, so the order does not change the result):

Device main (median) This PR (median) Cost
Android 16 emulator 15.63 s 15.82 s +0.19 s (+1.2 %)
iOS 26.3 simulator 16.63 s 17.01 s +0.38 s (+2.3 %)
iOS, landscape from the session start 36.6 s 37.1 s +0.45 s

All 40 runs gave a right image, without a new screenshot or a warning: on a device that is not busy, the cost is only the decode and the comparison.

Other scenarios:

Scenario main This PR
fullPageScrollTimeout: 100 (5 runs per device) right images right images
fullPageScrollTimeout: 0, Android (5 runs) right images right images
fullPageScrollTimeout: 0, iOS (5 runs) 5 wrong images 6 old frames found and taken again; the images are still wrong (see below)
A fixed overlay that fills the screen 16.1 s (Android), 16.9 s (iOS) 15.7 s, 17.3 s: no new screenshot and no warning (the scroll indicator of the browser changes the pixels)
Landscape, rotated in the session (Android, 2 runs) right images right images
CI on the fork (see Test) (old frames in earlier runs) one old frame found after the scroll to 4914 px, the second new screenshot was right

Known limit, not changed here: with fullPageScrollTimeout: 0 on iOS, about 96 CSS px of the page are missing at one place, on main and with this PR (not a repeated part, so not an old frame). Safari changes the size of its toolbar during the scroll, and the screenshot is taken during that change. It does not occur with 100 ms.

🤖 Generated with Claude Code

… frame from before the scroll

On a busy device (a new emulator or simulator, a CI runner) the native screenshot after a scroll could still show the
frame from before the scroll, also after `fullPageScrollTimeout`: the full page image then had a part two times and
missed a part. When the page scrolled but its part of the new screenshot has the same pixels as in the previous
screenshot, the screenshot is taken again after a wait (at least 500 ms), up to 3 times, then a warning. An area in
one color (a blank part of the page) is not checked: a new screenshot of it can not give another image. For iOS and
Android with native web screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d739a97

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@wdio/image-comparison-core Patch
@wdio/visual-service Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

… screenshots for a whole page

- The check for a stale frame keeps the decoded previous screenshot, so each screenshot is decoded once (before: twice).
- At most 3 new screenshots for a part of the page and 5 for the whole page, with one warning: a fixed overlay that
  fills the screen looks the same after each scroll, and cost up to 3 new screenshots for each part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plum117
plum117 marked this pull request as ready for review October 11, 2026 03:56
@plum117
plum117 marked this pull request as draft October 11, 2026 03:56
@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Adds retry logic for mobile full-page screenshot capture.

Fix the comparison crop bounds before merging so retries work on affected Android screen sizes.

Findings

  1. P1 Rounding disables screenshot retries ▶

Summary

Adds bounded retries when a mobile native screenshot repeats the previous frame after scrolling.

  • Mobile full-page screenshots retry when a scroll still shows the old frame.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Scroll and take screenshot] --> B{Scroll position changed?}
    B -->|No| F[Store screenshot]
    B -->|Yes| C{Compared pixels identical and not uniform?}
    C -->|No| F
    C -->|Yes| D[Wait and take screenshot again]
    D --> E{Three retries used?}
    E -->|No| C
    E -->|Yes| G[Warn if pixels still match]
    G --> F
Loading

Reviews (1) · Last reviewed commit: "fix: take a mobile full page screenshot ..." · Reviewed by Greptile

Comment on lines +48 to +54
const x = Math.floor(area.imageXPosition)
const y = Math.floor(area.imageYPosition)
const width = Math.floor(area.imageWidth)
const height = Math.floor(area.imageHeight)

return !isUniformArea(image, x, y, width, height)
&& hasSameArea(decodeUpright(previousScreenshot, isLandscape), image, x, y, width, height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rounding disables screenshot retries

isSameFrameArea compares the requested crop without the bounds limits used to build the full-page image. If an Android device has a 1080-pixel viewport and devicePixelRatio of 2.75, the width rounds to 393 CSS pixels and then back to 1081 image pixels. hasSameArea rejects that crop even when both screenshots are identical. No retry or warning happens, so a stale screenshot still repeats part of the page.

Limit the compared crop to the same available pixels used by makeFullPageBase64Image.

Knowledge Base Used:

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant