Skip to content

feat: the visual matchers can wait until the image matches (wait, interval) - #1323

Draft
plum117 wants to merge 4 commits into
webdriverio:mainfrom
plum117:feat/matcher-wait
Draft

plum117 wants to merge 4 commits into
webdriverio:mainfrom
plum117:feat/matcher-wait

Conversation

@plum117

@plum117 plum117 commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #690

Why

A page or an element is not always in its final state when the test checks it, for example during an animation, and another condition to wait for is not always available. The visual matchers checked once, so the test failed. The reporter of #690 proposed to run the check again until it matches, as the other WebdriverIO matchers do.

What

toMatchScreenSnapshot(), toMatchFullPageSnapshot(), toMatchElementSnapshot() and toMatchTabbablePageSnapshot() accept 2 new options, with the same names and meaning as in the expect-webdriverio matchers (but wait is 0 by default, so it is opt-in):

  • wait (milliseconds, default 0): check again until the image matches, or with .not until it does not match;
  • interval (milliseconds, default 100): the time between 2 checks.
await expect($('#chart')).toMatchElementSnapshot('chart', { wait: 3000, interval: 250 })
  • Opt-in: without wait, a matcher checks once, as before (a failing check does not take longer, and the screenshot is not repeated).
  • The matcher always checks at least once. When it checked more than once, the failure message tells how often and how long.
  • .not uses isNot of the matcher context. The Jasmine adapter now gives isNot too (compare and negativeCompare).
  • wait and interval are not passed to the check commands; an invalid value (negative, not a number) throws a clear error.
  • Each attempt is a full check. The actual and diff images and the JSON report of a check have the same file names, so the last attempt stays (no extra report entries). When a later attempt matches, the images that the earlier failed attempts wrote are removed (a file with an older write time, for example of an earlier check, stays).
  • toMatchElementSnapshot(): an element that the page renders again (stale) is found again, and an element that is not in the page yet is waited for, at most for the rest of the wait time (the instances of a multiremote element share that time) (before, it failed at once with Unsupported type: function). At the end of the wait time, the error tells which element is missing.
  • Types: WdioMatcherWaitOptions, added to the options of the 4 matchers. A changeset (minor). The feature ships after 11.0.0, so it is not in the v11 migration guide; the options need a line in the WebdriverIO docs (matchers).

Test

  • Unit tests (tests/matcher.test.ts, with fake timers): one check without wait, check again until it matches, stop at the wait time (4 checks at 0, 100, 200 and 300 ms, with the message), options not passed to the check, .not, element and multiremote (2 instances that match at different attempts), a stale element, an element that is not in the page yet (found, not found in time, no wait), the images of failed attempts with real files, full page and tabbable, invalid values; tests/jasmine.test.ts: isNot for compare and negativeCompare.
  • e2e with a local page (tests/fixtures/v10/delayed-change.html): tests/specs/v10.browsingContexts.spec.ts (Mocha: the box gets its final color later, the page renders the box again, the box is not in the page yet) and tests/specs/v10.jasmine.spec.ts (Jasmine, also .not with wait). Each new e2e test fails without its fix.
  • Matrix through the real expect of the test runner (local Chrome, with alwaysSaveActualImage on and off, 16 cells): the 4 matchers with a page or element that changes after 1.5 s; never matches (message and duration); no wait (one check); .not with wait in Mocha (a match, then no match; always matches); an asymmetric matcher; a number as the expected result; an element that comes after 1.5 s and one that never comes; an invalid value; an interval longer than the wait. All pass, and after a match no diff (and with alwaysSaveActualImage: false no actual image) of a failed attempt stays; the JSON report has the result of the last attempt.
  • All core and visual-service unit tests, lint and types pass. The local Chrome suites (test.local.chrome.v10, .jasmine, .emulation, test.local.desktop.multi) pass.

Known and not changed here: without wait, toMatchElementSnapshot() on an element that is not in the page fails with Unsupported type: function (also on main), which does not tell that the element is missing.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 885cb8e

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 Minor

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

@plum117
plum117 marked this pull request as ready for review October 10, 2026 14:30
@plum117
plum117 marked this pull request as draft October 10, 2026 14:30
@plum117

plum117 commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Keeping draft until @wswebcreation approval

@greptile-apps

greptile-apps Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are addressed and no new blocking issue remains.

Summary

The PR adds opt-in retries through wait and interval to all four visual matchers, including .not through Jasmine.

  • Visual snapshot matchers repeat checks until the expected image result appears.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Start visual matcher] --> B[Run visual check]
    B --> C{Expected result reached?}
    C -->|Yes| D[Clean up earlier retry images]
    D --> E[Return result]
    C -->|No| F{Wait time used up?}
    F -->|Yes| E
    F -->|No| G[Wait for next attempt]
    G --> B
Loading

Reviews (3) · Last reviewed commit: "fix: share the matcher wait time between..." · Reviewed by Greptile

Comment thread packages/visual-service/src/matcher.ts
Comment thread packages/visual-service/src/matcher.ts
Comment thread tests/specs/v10.browsingContexts.spec.ts
Comment thread tests/specs/v10.jasmine.spec.ts
Comment thread packages/visual-service/tests/matcher.test.ts Outdated
plum117 added a commit to plum117/visual-testing that referenced this pull request Oct 10, 2026
…ges of failed attempts

Greptile review of webdriverio#1323:

- toMatchElementSnapshot resolves the element once. When the page renders
  it again, the check command sends the old element id and fails with a
  stale element error. Within the wait time the matcher now calls an element
  command, which WebdriverIO uses to find the element again (it updates the
  element id), and checks again. The new e2e test fails without this with
  "stale element reference: stale element not found in the current frame".
- With alwaysSaveActualImage: false a failed attempt saves the actual and
  diff images. When a later attempt matches, the images that it did not
  write again (same write time) are removed.
- The e2e fixture no longer changes on a timer after the load: the test
  starts the change (setBox), so a slow runner can not miss the first state.
- New tests: .not with wait through the Jasmine adapter (match, then
  mismatch), and a multiremote element whose instances match at different
  attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plum117 and others added 3 commits October 10, 2026 17:14
…erval)

A page or an element is not always in its final state when the test
checks it, for example during an animation, and an other condition to
wait for is not always available (webdriverio#690).

The visual matchers accept `wait` (default 0) and `interval` (default
100) in milliseconds, with the same names and meaning as in the
expect-webdriverio matchers. With `wait`, the matcher runs the check again
until the result is the expected one: the image matches, or with `.not`
it does not match (`isNot` of the matcher context; the Jasmine adapter
now gives it too). It always checks at least once, and a failure message
tells how often and how long it checked. The 2 options are not passed to
the check commands. Each attempt is a full check, and the files and the
JSON report of a check have the same names, so the last attempt stays.

Without `wait`, a matcher checks once, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ges of failed attempts

Greptile review of webdriverio#1323:

- toMatchElementSnapshot resolves the element once. When the page renders
  it again, the check command sends the old element id and fails with a
  stale element error. Within the wait time the matcher now calls an element
  command, which WebdriverIO uses to find the element again (it updates the
  element id), and checks again. The new e2e test fails without this with
  "stale element reference: stale element not found in the current frame".
- With alwaysSaveActualImage: false a failed attempt saves the actual and
  diff images. When a later attempt matches, the images that it did not
  write again (same write time) are removed.
- The e2e fixture no longer changes on a timer after the load: the test
  starts the change (setBox), so a slow runner can not miss the first state.
- New tests: .not with wait through the Jasmine adapter (match, then
  mismatch), and a multiremote element whose instances match at different
  attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in the page yet

`expect($('#chart')).toMatchElementSnapshot('chart', { wait: 3000 })` failed at once with "Unsupported type: function"
when the chart was not in the page yet: WebdriverIO keeps an element that it did not find without an element id, and
the check commands need that id. With `wait`, the matcher now waits for such an element, at most for the rest of the
wait time, and lets an element command of WebdriverIO set its element id (also for each instance of a multiremote
element). At the end of the wait time, the error of `waitForExist` tells which element is missing. Without `wait`,
nothing changes.

The feature is in 11.1.0, not in 11.0.0: the section in the v11 migration guide is removed, and the changeset tells
that `wait` is opt-in (`0` by default), unlike the other WebdriverIO matchers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plum117
plum117 marked this pull request as ready for review October 11, 2026 01:16
Comment thread packages/visual-service/src/matcher.ts Outdated
Comment thread packages/visual-service/src/matcher.ts Outdated
…eep images that a failed attempt did not write

- The missing elements of a multiremote element were each waited for with the full rest of the wait time, one after
  the other, so the matcher could wait longer than `wait`. The instances now share one deadline.
- With `alwaysSaveActualImage: false` and a `saveAboveTolerance` above the threshold, a failed attempt does not save
  an actual image, but the matcher remembered the image that an earlier check had saved under that name and removed
  it after a later match. Only a file that the failed attempt wrote (written after the attempt started) is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plum117
plum117 marked this pull request as draft October 11, 2026 01:42

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.

Wait for Screenshot to Match

1 participant