Skip to content

feat: Support $$() with toBe() element's matchers - #2149

Merged
dprevost-LMI merged 19 commits into
webdriverio:mainfrom
dprevost-LMI:feat/enable-$$-support-for-toBeMatchers
Jul 18, 2026
Merged

feat: Support $$() with toBe() element's matchers#2149
dprevost-LMI merged 19 commits into
webdriverio:mainfrom
dprevost-LMI:feat/enable-$$-support-for-toBeMatchers

Conversation

@dprevost-LMI

@dprevost-LMI dprevost-LMI commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #1507.
Partially fixes #512.
Partially fixes #1717

Summary

Adds official $$() (element array/elements) support to toBe matchers. Previously, TypeScript signatures allowed arrays (by mistake in this PR), but the implementation didn't support them properly.

Example:

        await expect($$('items')).toBeDisplayed();

        // `.not` cases
        await expect($$('items')).not.toBeDisplayed();

Official $$() Support

This PR adds official support for toBe most matchers.

⚠️ While $$() support may incidentally enable expect() to work with multi-remote, this is not intended and may break at any time. Official multi-remote support is tracked here and is not yet available.

Types Support

  • ChainablePromiseArray, the non-awaited case
  • ElementArray, the awaited case
  • Element[], the filtered case

Behavior

The following must pass for all elements to be displayed; otherwise, it fails.

        await expect($$('items')).toBeDisplayed();
  • For toBe matchers, all elements must match the expected boolean (usually true, except for toBeDisabled).

isNot

The following must pass when all elements are not displayed; otherwise, it fails.

        await expect($$('items')).not.toBeDisplayed();

Edge cases

No elements found

When no elements are found, we always fail, with or without .not, even if the expected value is an empty array.

Error handling

Below are examples of colour failures.

  • We can see cases for multiple elements using toBeDisplayed matchers
  • With .not, toBe are handled by adding not in the values

TODO

  • Documentations

Failures Example

image image image

@dprevost-LMI dprevost-LMI changed the title feat: Enable $$ support for to be matchers feat: Support $$() with all element's matchers Jul 14, 2026
@dprevost-LMI dprevost-LMI changed the title feat: Support $$() with all element's matchers feat: Support $$() with toBeX() element's matchers Jul 14, 2026
@dprevost-LMI dprevost-LMI changed the title feat: Support $$() with toBeX() element's matchers feat: Support $$() with toBe() element's matchers Jul 14, 2026
@dprevost-LMI
dprevost-LMI marked this pull request as ready for review July 14, 2026 10:47
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds official $$() (element array) support to toBe* matchers. Previously, TypeScript signatures allowed arrays by mistake but the implementation only handled single elements; now ChainablePromiseArray, ElementArray, and Element[] all work correctly through a new executeCommandWithStrategy router with two strategies: a multipleElementResultsStrategy (new semantics: all elements must pass for success, all must fail for .not success) and a legacyMultipleElementResultsStrategy kept for toHaveText backward compatibility.

  • Replaces executeCommand + manual getElement() in executeCommandBe with executeCommandWithStrategy, enabling arrays without requiring callers to change their matcher signatures.
  • Adds enhanceErrorBe array-aware error formatting showing per-element pass/fail diffs and a dedicated "at least one result" message for empty-array failures.
  • Fixes a latent isDefined(index) bug in getSelectors where element index 0 was treated as falsy, resulting in incorrect selector strings in error messages.

Confidence Score: 5/5

Safe to merge. The new element-array execution path is well-separated from the existing single-element path, toHaveText legacy behavior is explicitly preserved via strategy routing, and edge cases (empty arrays, index-0 elements, toExist with allowEmptyElements) are all covered by dedicated tests.

The core multipleElementResultsStrategy success formula is correct and verified against all test assertions. The legacyMultipleElementResultsStrategy correctly routes toHaveText through the old path, preserving backward compatibility. The enhanceErrorBe signature change is contained to a single call site. No behavioral regressions were found in the changed matchers.

No files require special attention. The most complex logic is in src/util/executeCommand.ts and src/util/formatMessage.ts, both of which have comprehensive test coverage for the new array paths.

Important Files Changed

Filename Overview
src/util/executeCommand.ts Core change: adds multipleElementResultsStrategy and legacyMultipleElementResultsStrategy, with executeCommandWithStrategy routing between them. Logic is correct.
src/utils.ts Refactors executeCommandBe to use executeCommandWithStrategy instead of executeCommand + manual getElement(), enabling element-array support for all toBe* matchers.
src/util/formatMessage.ts Adds array-aware enhanceErrorBe signature with `results: boolean[]
src/matchers/element/toBeExisting.ts Sets this.allowEmptyElements = true so that toExist / toBeExisting / toBePresent behave correctly for empty arrays with .not.
src/matchers/element/toHaveText.ts Adds isNot and strategy: 'LegacyMultipleElements' to preserve existing toHaveText behavior for multi-element / empty-array cases.
test/matchers/beMatchers.test.ts Adds comprehensive multi-element test suite covering ChainablePromiseArray, ElementArray, Element[], edge cases (empty arrays, out-of-bounds, single-element arrays), and .not semantics.
test/matchers/element/toBeDisabled.test.ts Adds multi-element test cases for toBeDisabled, covering both positive and .not scenarios with mocked isEnabled.
test/matchers/element/toBeDisplayed.test.ts Adds comprehensive display-specific multi-element tests including filtered Element[] and edge-case scenarios.
test/util/formatMessage.test.ts Updated to pass new results parameter to enhanceErrorBe; tests now cover per-element diff rendering for array subjects.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["executeCommandBe(received, command, options)"] --> B["waitUntil loop (with isNot, wait, interval)"]
    B --> C["executeCommandWithStrategy(unresolvedElements, singleElementCompare, isNot)"]
    C -->|"strategy = LegacyMultipleElements (toHaveText)"| D["legacyMultipleElementResultsStrategy"]
    C -->|"strategy = NewMultipleElements (toBe* matchers)"| E["multipleElementResultsStrategy"]
    D --> D1{"Empty / no element?"}
    D1 -->|yes| D2["success: false, actual: undefined"]
    D1 -->|"single element"| D3["singleElementCompare(el)"]
    D1 -->|"array"| D4["Promise.allSettled() - success: isAllTrue"]
    E --> E1{"Empty / no element?"}
    E1 -->|yes| E2["success: isNot ? !allowEmptyElements : false"]
    E1 -->|"single element"| E3["singleElementCompare(el)"]
    E1 -->|"array"| E4["Promise.allSettled() - isNot=false: isAllTrue, isNot=true: !isAllFalse"]
    B -->|"breaks or times out"| F["enhanceErrorBe(subject, actual, context, options)"]
    F -->|"isElementArrayLike(subject)"| G["Array diff: expected=per-element expected, actual=per-element pass/fail"]
    F -->|"single element / other"| H["String diff: expected/actual strings"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["executeCommandBe(received, command, options)"] --> B["waitUntil loop (with isNot, wait, interval)"]
    B --> C["executeCommandWithStrategy(unresolvedElements, singleElementCompare, isNot)"]
    C -->|"strategy = LegacyMultipleElements (toHaveText)"| D["legacyMultipleElementResultsStrategy"]
    C -->|"strategy = NewMultipleElements (toBe* matchers)"| E["multipleElementResultsStrategy"]
    D --> D1{"Empty / no element?"}
    D1 -->|yes| D2["success: false, actual: undefined"]
    D1 -->|"single element"| D3["singleElementCompare(el)"]
    D1 -->|"array"| D4["Promise.allSettled() - success: isAllTrue"]
    E --> E1{"Empty / no element?"}
    E1 -->|yes| E2["success: isNot ? !allowEmptyElements : false"]
    E1 -->|"single element"| E3["singleElementCompare(el)"]
    E1 -->|"array"| E4["Promise.allSettled() - isNot=false: isAllTrue, isNot=true: !isAllFalse"]
    B -->|"breaks or times out"| F["enhanceErrorBe(subject, actual, context, options)"]
    F -->|"isElementArrayLike(subject)"| G["Array diff: expected=per-element expected, actual=per-element pass/fail"]
    F -->|"single element / other"| H["String diff: expected/actual strings"]
Loading

Reviews (11): Last reviewed commit: "fix ambiguous title & doc" | Re-trigger Greptile

Comment thread src/util/executeCommand.ts
Comment thread src/util/executeCommand.ts Outdated
Comment thread test/matchers/element/toBeDisplayed.test.ts
@dprevost-LMI
dprevost-LMI marked this pull request as draft July 16, 2026 01:34
@dprevost-LMI
dprevost-LMI marked this pull request as ready for review July 16, 2026 11:30
@dprevost-LMI
dprevost-LMI marked this pull request as draft July 16, 2026 16:00
@dprevost-LMI
dprevost-LMI marked this pull request as ready for review July 17, 2026 22:14
@dprevost-LMI
dprevost-LMI merged commit 958e102 into webdriverio:main Jul 18, 2026
6 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

1 participant