Skip to content

fix: activate a page in a background tab before a check or save command - #1272

Merged
plum117 merged 5 commits into
webdriverio:mainfrom
dprevost-LMI:fix/activate-hidden-tab
Oct 8, 2026
Merged

plum117 merged 5 commits into
webdriverio:mainfrom
dprevost-LMI:fix/activate-hidden-tab

Conversation

@dprevost-LMI

@dprevost-LMI dprevost-LMI commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

In a WebDriver BiDi session with a Chromium-based browser, a check or save command on a page in a background tab could pass without a comparison, or hang for 180 seconds. The visual service now brings that page to the front before the command, as ChromeDriver does for a WebDriver Classic screenshot.

Replaces #1270 (its test commit is included here).

Problem

In WebdriverIO v10, browser.newWindow() does not switch to the new tab, but Chrome brings the new tab to the front. The page of the browser commands is then in a background tab (document.visibilityState is hidden). On Linux headless Chrome (the CI platform):

  • Wrong file name, no comparison: the hidden page reports outerHeight = innerHeight, and the desktop file name uses the outer size. With autoSaveBaseline (the default), the check saved a new baseline and returned 0. Found in CI: the newWindow() test saved …-1366x625.png and …-1366x768.png for the same page and never compared.
  • Hang: browsingContext.captureScreenshot on the hidden page never returned, until bidiResponseTimeout (180 s by default). Found in CI on refactor: find a stale ignore element again with WebdriverIO #1254.

WebDriver Classic does not have these problems, because ChromeDriver brings the tab to the front before a screenshot (ActivateWebView in ExecuteScreenshot). Our BiDi path did not.

Root cause: a Chromium bug

The hang also happens with the Chrome DevTools Protocol only, without WebDriver: Page.captureScreenshot on a page in a background tab never returns when the page had no new frame for about 300 ms. Pure CDP reproduction (Puppeteer, Chrome 154, 10 runs per mode):

Platform Background tab Foreground tab Background + Page.bringToFront
Linux x64 (Docker) 10/10 hang 0/10 0/10
macOS arm64 0/10 0/10 0/10

Firefox 157 (BiDi) does not hang.

Reported:

Changes

Fix (@wdio/visual-service, patch changeset)

  • New activateHiddenBrowsingContext(): if document.visibilityState is hidden, call browsingContext.activate for the current context, and log it. Any error only logs a warning, so the command never fails because of this step.
  • Scope: BiDi sessions of Chromium-based desktop browsers (Chrome, Chromium, Edge), web context, and only when the page is hidden. Firefox, Safari, mobile, native context and WebDriver Classic sessions do not change.
  • Not limited to Linux: the browser can run on another OS than the test runner (grid, cloud), the hang is also reported on macOS with Chrome 149 (agent-browser#1437), and Windows was not tested. The cost is one execute call per command.
  • Called in wrapWithContext(), so before every check and save command (single browser, multiremote, page and element), before the page size is read and the screenshot is taken.
  • Side effect: the tab of the page comes to the front, the same as with a Classic screenshot.
  • The JSDoc explains why, and links both reports. When Chromium fixes the bug, we can limit the step to older Chrome versions or remove it.

Test

  • The newWindow() e2e test checks that the second check uses the same baseline file as the first one (so a new name fails instead of passing), that the mismatch is 0, and that page A is visible after the check. Page B is blue and page A is red, so a screenshot of the wrong tab fails.
  • The local Chrome v10 config (also used by the Jasmine suite) names the files without {width}x{height}: it runs one fixed Chrome.
  • Unit tests for the helper: hidden, visible, Classic/mobile/native, Chrome/Edge/Chromium names, Firefox, an activation error and an execute error. A unit test for the call order in wrapWithContext(). The old wrapWithContext tests passed the browser with the wrong key (browser instead of browserInstance); corrected.

Test

  • pnpm test (lint, types, unit): pass.
  • Local Chrome v10 suites (Mocha, Jasmine, emulation): pass on macOS.
  • Linux container (Chrome 155), the same platform as CI:
Variant Before After
This branch — 2 passing
Tab B left open hang, 180 s 2 passing in 5 s
{width}x{height} in the file name 2 names (625 / 768), no comparison 1 baseline, a real comparison

Notes

  • bidiResponseTimeout (WebdriverIO, 180 s by default) already ends a hung command, so the service does not add another timeout. This fix removes the cause.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 35f959f

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

This PR includes changesets to release 1 package
Name Type
@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

@dprevost-LMI
dprevost-LMI marked this pull request as ready for review October 8, 2026 11:48
@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Visual service now activates background tabs before screenshots.

The PR appears safe to merge; no actionable defects were found.

What we checked:

  • Every capture reaches activation: Single-browser and multiremote page and element commands all use wrapWithContext, which calls activation before running the command.
  • The test still checks zero: The configuration removes the baseline folder before the run. The first check copies the captured image into the baseline and compares against it, so the second result must also report zero.

Summary

The visual service brings hidden Chromium desktop tabs to the front before BiDi check and save commands.

  • Activation runs before the command reads page sizes and takes a screenshot.
  • Classic, mobile, native, and non-Chromium sessions skip this step.
  • Tests cover activation, skipped cases, errors, and stable comparison filenames.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Check or save command] --> B[Desktop Chromium BiDi session?]
  B -->|No| E[Run existing command]
  B -->|Yes| C[Read page visibility]
  C -->|Visible| E
  C -->|Hidden| D[Activate current tab]
  D --> E
  C -->|Error| W[Log warning]
  D -->|Error| W
  W --> E
  E --> F[Read page sizes and capture image]
Loading

Reviews (2) · Last reviewed commit: "test: compare the 2 results of the newWi..." · Reviewed by Greptile

dprevost-LMI and others added 5 commits October 8, 2026 11:47
After newWindow(), Chrome brings the new tab to the front, so page A is
in a background tab (`document.visibilityState` is `hidden`). A hidden
tab reports `outerHeight` = `innerHeight`, and the file name uses the
outer size on desktop. On Linux CI the second check got another name
(1366x625 instead of 1366x768), saved a new baseline, and passed
without a comparison.

- The local Chrome v10 config (also used by the Jasmine suite) names
  the files without {width}x{height}: it runs one fixed Chrome.
- The test checks that the second check uses the same baseline file as
  the first one, so a different name now fails instead of passing.
  Page B is blue, so a screenshot of the tab in front cannot match the
  red page A.

Checked in a Linux container (Chrome 155): the test passes with one
baseline, and with the old name format it now fails on the file name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a WebDriver BiDi session, a page in a background tab (for example
after `browser.newWindow()`, which in WebdriverIO v10 does not switch
to the new tab) is `hidden`:
- it reports outerHeight = innerHeight, so the desktop file name
  changes, and with autoSaveBaseline the check saves a new baseline and
  returns 0 without comparing;
- its screenshot can hang until the bidiResponseTimeout (180 s).
A WebDriver Classic screenshot does not have these problems, because
ChromeDriver brings the tab to the front.

Before each command (wrapWithContext, so single browser, multiremote,
page and element commands), activate the browsing context of the
browser when `document.visibilityState` is `hidden`. Mobile, native
context and Classic sessions are not changed. A failed activation only
logs a warning.

Tests: unit tests for the helper and the call order; the e2e newWindow
test now also checks that page A is visible after the check. Checked in
a Linux container (Chrome 155): the suite passes; with tab B left open
it passes in 5 s (before: 180 s hang); with {width}x{height} in the
file name, both checks use one baseline (before: 1366x625 vs 1366x768).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on Linux and macOS with the same Chrome 155.0.8059.39, and
with Firefox 157 (BiDi) on Linux:
- Chrome on Linux: a page in a background tab reports outerHeight =
  innerHeight, and after a DOM change `Page.captureScreenshot` never
  returns. The hang also happens with CDP only (Puppeteer), so it is a
  Chromium bug, not a BiDi or WebdriverIO bug.
- Chrome on macOS and Firefox on Linux: neither problem.

So activate the page only in BiDi sessions of Chromium-based browsers
(Chrome, Chromium, Edge). It is not limited to Linux, because Windows
was not tested and on macOS the activation does no harm (ChromeDriver
does the same for a Classic screenshot).

The helper is now fully in try/catch, so it can never make a check or
save command fail, and its JSDoc explains the problem, the evidence,
the scope and the side effect. Unit tests for Edge/Chromium, Firefox
and a failed visibility check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tab activation before a check keeps the window size stable, so the
names with the size stay the same. With the size in the name, the
newWindow() test fails when the activation does not work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first and the second check of the same page return the same compare
data, so toEqual checks the file name, the folders and the mismatch in
one step. The interface, the type guard and the helper are not needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dprevost-LMI
dprevost-LMI force-pushed the fix/activate-hidden-tab branch from 9dcc21f to 35f959f Compare October 8, 2026 15:47
@plum117
plum117 merged commit d32cca2 into webdriverio:main Oct 8, 2026
10 checks passed
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.

2 participants