Skip to content

feat: Support $$() with all element's matchers#1990

Draft
dprevost-LMI wants to merge 7 commits intowebdriverio:mainfrom
dprevost-LMI:fix-matchers-with-$$
Draft

feat: Support $$() with all element's matchers#1990
dprevost-LMI wants to merge 7 commits intowebdriverio:mainfrom
dprevost-LMI:fix-matchers-with-$$

Conversation

@dprevost-LMI
Copy link
Contributor

@dprevost-LMI dprevost-LMI commented Jan 8, 2026

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

Summary

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

Example:

 await expect($$('items')).toBeDisplayed(); await expect($$('items')).toHaveHTML('<div/>'); await expect($$('items')).toHaveHTML(['<div/>', '<label/>']); // `.not` cases await expect($$('items')).not.toBeDisplayed(); await expect($$('items')).not.toHaveHTML('<div/>'); await expect($$('items')).not.toHaveHTML(['<div/>', '<label/>']);

Current Behaviour Issues

  • Only toHaveText and toBeElementsArrayOfSize officially support arrays
  • Non-awaited $$() or filtered $$().filter() throws errors
  • toHaveText with empty elements incorrectly passes
  • toHaveText doesn't trim text for multiple elements (inconsistent with single element behaviour)
  • toHaveText doesn't do strict and index-based comparison but only loose comparison (kept)
  • Failure message not shown all elements values (changed)

Error handling

  • When $$ returns only one element and we have one expected value, the error message (CHANGED)
Expect $$(`#username`) to have text Expected: "t" Received: "" 
  • When $$ returns only one element and an array of expectations is passed, the error message (CHANGED)
Expect $$(`#username`) to have text Expected: ["t", "r"] Received: "" 
  • When having multiple elements and multiple expected values, we see the following (CHANGED)
Expect $$(`label`) to have text - Expected - 3 + Received + 1 Array [ - Array [ "Username", - "Password1", - ], + "Password", ] 

Note: All the above have been changed to show all the elements' values and not just those not matching

Official $$() Support

This PR adds official support for most matchers, with a focus on the toBe and toHave element 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 when all elements are displayed/have the HTML; otherwise, it fails.

 await expect($$('items')).toBeDisplayed(); await expect($$('items')).toHaveHTML('<div/>'); await expect($$('items')).toHaveHTML(['<div/>', '<label/>']);
  • For toBe matchers, all elements must match the expected boolean (usually true, except for toBeDisabled).
  • For toHave matchers, you can provide a single expected value or an array; strict array comparison is used.
  • Options like StringOptions, HTMLOptions, ToBeDisplayedOptions apply to the whole array (not per element).
  • Only NumberOptions can be provided as an array since it's the only "option" treated as expected values.

Array Comparison Behaviour

  • With a single expected value, all elements must strictly match (for text, trimming is the default unless { trim: false }).
  • With an array of expected values, each element is compared by index; differing array lengths or mismatches cause failure.
  • Except for toHaveText (deprecated), elements are not compared to any value in the expected array—only by index.
  • For number options, strict matching still applies according to the NumberOptions rules.

isNot

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

 await expect($$('items')).not.toBeDisplayed(); await expect($$('items')).not.toHaveHTML('<div/>'); await expect($$('items')).not.toHaveHTML(['<div/>', '<label/>']);

Edge cases

No elements found

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

  • Only toBeElementsArrayOfSize(0) support empty array element

expect.arrayContaining

Only toHaveText will do a containing array behaviour with the following

 await expect(await $$('label')).toHaveText(['Username', 'Password']);

We should consider deprecating the above for expect.arrayContaining and supporting it, which is not the case at all

 await expect(await $$('label')).toHaveText(expect.arrayContaining(['Username', 'Password']));

Error handling

Below are examples of failures with colours.

  • We can see cases for multiple elements for the toHaveText and toBeDisplayed matchers
  • With .not
    • toBe are handled by adding not in the values
    • For toHave matchers, a more complex method was used to highlight those actuall matching (red highlight)
image

Previous Releases Bugs 🐛

Fixed bugs from previous releases (fixed when checked)

  • Non-awaited $$() calls like expect($$('el')).toHaveText('text') no longer cause function errors.
  • Array comparisons now trim actual text values by default.
  • toBeElementsArrayOfSize typings now work correctly with Element[].
  • Typings for toHaveElementProperty adjusted—using an optional value always failed; future support for existence checks like toHaveAttribute may be added.
  • $$().toHaveText('C' | ['C']) now fails when there are no elements (e.g., expect([]).toHaveText('test')); all matchers fail gracefully on empty arrays.
  • Multiple matchers now report the correct names in before and after hooks.
  • BREAKING Removed deprecated toHaveClassContaining and toHaveClass matchers.
  • Non-element parameters to expect() (e.g., expect(undefined)) now fail gracefully with clear messages.

Future Considerations

  • Consider supporting arrays-per-element in $$() (compare each element against multiple values), but this is complex, limited to $$(), and alternatives exist (e.g., regex, stringContaining).
  • Deprecate NumberOptions as an "option"—treat as ExpectedType for clarity and easier handling of other options (e.g., error messages, wait).
  • Show failure instead of throwing for missing elements or even index out of bounds from $()[x]; POC done and working.
  • Support Promise<Element|Elements> in typings.
  • Enhance support and typings for expect.arrayContaining() across all matchers and remove custom “containing” logic from toHaveText.
  • Improve TypeScript typing for single element vs. array of elements, reflecting matcher support for array values and elements (overloaded functions)
  • For toBeElementsArrayOfSize.ts, consider updating the array of the non-awaited case by awaiting it
  • For toHaveElementProperty
    • Use deep-equality to support type like object & Arrays
    • As toHaveAttribute support properly optional expected value for property existence

TODO

  • Documentations
  • Finish error handling cases validations
  • Finish code review
  • Finish writing multiple elements & multiple expected values for each matcher
  • Add more UTs case got toHaveElement & unknown? And review the null case?
  • Test multiple elements with assymetric matcher a bit more
  • Look to support strict matching with toHaveText somehow now or later?
  • Ensure toHaveText legacy behaviour is kept, including failure reporting!
  • Get approval and document the fact that awaiting element/elements is now always in the wait time of waitUntil
  • Test it out in the playground (when merged)
  • Get approval on the documented behaviour in MultipleElements.md (Christian?)
  • Review .not in waitUntil following regression in main
  • Assert if we need to refreshElements (now or later) as we do for singleElement when they are not found
  • Review localStorageItem once it's merged (if needed)
  • Review forgotten snapshot matchers

Blocked and waiting on the merge of

Not blockers, but good to have dependent PR

@dprevost-LMI dprevost-LMI force-pushed the fix-matchers-with-$$ branch 2 times, most recently from 811216d to 62a200b Compare January 10, 2026 03:23
Comment on lines +24 to +25
- name: Run All Checks
run: npm run checks:all
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Waiting on merge of #1991

return aliasFn.call(this, toExist, { verb: 'be', expectation: 'existing' }, el, options)
this.verb = 'be'
this.expectation = 'existing'
this.matcherName = 'toBeExisting'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed bad matcher name

@dprevost-LMI dprevost-LMI changed the title fix: Fix matchers not working with $$ aka ElementArray feat: Support $$ with all matchers Jan 11, 2026
@dprevost-LMI dprevost-LMI changed the title feat: Support $$ with all matchers feat: Support $$() with all matchers Jan 11, 2026
@dprevost-LMI dprevost-LMI changed the title feat: Support $$() with all matchers feat: Support $$() with all element's matchers Jan 11, 2026
{
ignoreCase = false,
trim = false,
trim = true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed issue where single expect value vs multiple expected value behavior differently by default

Comment on lines -15 to -26
// If no options passed in + children exists
if (
typeof options.lte !== 'number' &&
typeof options.gte !== 'number' &&
typeof options.eq !== 'number'
) {
return {
result: children.length > 0,
value: children?.length
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced below by ?? { gte: 1 } + test added

const numberOptions = validateNumberOptionsArray(expectedValue ?? { gte: 1 })
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming from toHaveClass

@dprevost-LMI dprevost-LMI force-pushed the fix-matchers-with-$$ branch 14 times, most recently from f8f6ba0 to 78a6cf0 Compare January 20, 2026 23:18
@dprevost-LMI dprevost-LMI force-pushed the fix-matchers-with-$$ branch 4 times, most recently from 8991b28 to 2c40ce5 Compare January 24, 2026 19:23
@christian-bromann
Copy link
Member

What is the status of this?

@dprevost-LMI
Copy link
Contributor Author

dprevost-LMI commented Feb 1, 2026

I was waiting for at least some feedback on the defined behaviour here: https://github.com/webdriverio/expect-webdriverio/blob/860dedcc78c9ede976eae187fdd6a44cdf8b2c4e/docs/MultipleElements.md
I suspect you will be the best person to do so?

I moved back to draft and update the list of tasks

  • Get approval and document the fact that awaiting element/elements is now always in the wait time of waitUntil
  • Test it out in the playground (when merged)
  • Get approval on the documented behaviour in MultipleElements.md (Christian?)
  • Review .not in waitUntil following regression in main
  • Assert if we need to refreshElements (now or later) as we do for singleElement when they are not found
@dprevost-LMI dprevost-LMI marked this pull request as draft February 1, 2026 13:17
- Fix double negation in `Expected` with `isNot` when using `enhanceErrorBe` - Fix incorrect Received value with `isNot` when using `enhanceErrorBe` - Code refactoring simplify to better understand the code
Use Promise.all for ElementArray instead of waiting one by one Add UTs for ElementArray and Element[] Review after rebase + add awaited element case fix regression while trying to support Element[] Code review Add assertions of `executeCommandBeWithArray` and `waitUntil` All to be matchers now compliant with multiple elements + UTs fix rebase Fix ChainablePromiseElement/Array not correctly considered Even if the typing was passing using `'getElement' in received` was not considering Chainable cases Add coverage Enhance toHaveText test cases Working cases of toHaveWidth and toHaveHTML with $$() Ensure all test dans can run fast with `wait: 1` Speed-up unit tests runs Speed-up test execution Add coverage on `executeCommandWithArray` and support edge case Review error message assertions and discover a problem Have failure messages better asserted toHaveAttribute supporting $$() now Make all `toHave` matcher follow same code patterns Align code to use same code pattern and fix case of `Element[]` not working Remove `executeCommand` and simplify executeCommandWithArray + coverage Align typing with expected being an array + trim by default for arrays Support of `toHaveHeigth`, `toHaveSize` & `toHaveWidth`` fix UTs Working support of `$$()` in all element matchers Deprecate `compareTextWithArray` & remove toHaveClassContaining fix possible regression around NumberOptions wait & internal not considered Forget toHaveStyle + code review Code review better function naming Code review Code review + remove `toHaveClass` deprecated since v1, 4 versions ago Support unknown type for `toHaveElementProperty` since example existed doc: official support of `$$()` Review & add coverage + discover problem with isNot fix for other PR, waitunitl + toBeerror - to revert later? fix `.not` cases - Ensure isNot is correct following the backport of other PR fixes - Ensure for multiple elements the not is apply on each element and fails if any case fails - Fix/consider empty elements as an error at all times Finish matchers UT's refactor to be mocked and type safe - Review all matcher UTs to call there real implementation with a this object ensuring the implementation has the right type - Use vi.mocked, to ensure we mock with the proper type Missed some bind Review waitUntil + coverage Better documentation Code review
Code review + make Be/Browser matchers test type safe Mock default option to speed up tests Better way to speed-up tests Add tests around default options Fix $$ mock Ensure matcherName is corectly passed + toExists test correctly Reinforce and test `isElementArray` Add missing coverage for `executeCommand` Migrate test of toBeArraySize + add more robust util - Migrate + add coverage for `toBeElementsArrayOfSize` matcher - Fix wait not correctly considered in `toBeElementsArrayOfSize` - Reinforce isElementArray and similar + add more coverage - By default for `DEFAULT_OPTIONS`use a non 0 wait time - Fix global mock missing element.parent Add note on element not found + add potential case to support Add edge cases Add alternative with parametrized in aPI doc Gracefully fails on invalid element types With selector fixed add back Promise of elements case Code review Code review + add unsupported type to toBe matchers Code review Add unsupported type test coverage Code review + add coverage + better awaitElement mechanism Add asymmetric integration tests Code review & coverage Add more tests for toHaveAttribute Array of array is not supported so adapting test for today Review some TODOs Support better failure msg for multiple results in toBe Matchers Properly handle failure colored message for `.no` for multiple elementst Properly support equal for NumberOptions and .not multiple values failure Code review Review coverage for formatMessage + numberOptions Fix 0 not stringily correctly Add .not elements integration tests Add coverage Review docs Finalize `executeCommandBe` tests Increase coverage More stable tests test Add `toHaveText` non-indexed + non-strict length legacy behavior Test more unknown expected type, but maybe some bug? Use supported type instead of unknown for `toHaveElementProperty` Add case of element not found which throws for single element - Note that for multiple element so ElementArray, there is no exception but an empty array Add missing element case and index out of bound from `$()[x]` Review refresh test after rebase - Ensure we return non modified args elements
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants