Skip to content

Conversation

@dprevost-LMI
Copy link
Contributor

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

Fixes #1507.

Matchers toBeDisplayed and several others have TypeScript signatures that support an array element, even though they don't.

Current situation

Premise

  • From documentation, only toHaveText and toBeElementsArrayOfSize mention $$() and therefore officially support them and must mitigate breaking changes
  • Before enhancing the type in this PR, code's comment was ambiguous on the support but still not official
  • After enhancing types, unfortunately, the code's comment was taken as being true, and support through types only was more predominant since matchers' function used this type, allowing both Element and Elements. Nevertheless, support was not observed elsewhere; it was simply another misinterpretation of an incorrect comment.

Current behaviour

  • toHaveText officially supports an array for both single elements and multiple elements
    • For single elements, it trims by default the actual element's text before comparing with any values in the arrays
    • For multiple elements, it DOES NOT trim by default (🐛) but still it compares with any values in the arrays
    • The expected arrays can have more expected values than the number of elements because comparison on any value of the array is being done.
    • When the elements are empty, even if we provide a single expected or multiple values, the matchers pass with success. This is a bug since the elements do not have Text and pass (🐛)
    • Note: Other matchers like toHaveHTML do use the compareTextWithArray, but after testing, they do not support $$(), so their behaviour today cannot be compared to toHaveText under $$().
  • Only support of awaited $$() (await $$()) is working. Passing non-awaited $$() or $$().filter() throws an error today

Error handling

  • When $$ returns only one element and we have one expected value, the error message is as below, support of this could change (to validate)
Expect $$(`#username`) to have text
Expected: "t"
Received: ""
  • When $$ returns only one element and an array of expectations is passed, the error message is as follows: support of this could change (to validate)
Expect $$(`#username`) to have text

Expected: ["t", "r"]
Received: ""

Official $$() Support

This PR provides official support for most matchers; currently, the targets are the toBe and toHave element matchers.

⚠️ Even though support of $$() will potentially allow expect to work with multi-remote, this is not the target, and any side-effect of it, allowing multi-remote to work, can break at any time. Multi-remote support is tracked here but is not yet officially supported.

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; otherwise, it fails.

expect($$('.items')).toBeDisplayed()
  • For toBe matchers, ALL elements' value must match the boolean pass value, which is most of the time true, except for toBeDisabled matcher
  • For toHave matchers supporting an expected value, when passing an array of elements to expect, both a single value and an array of values can be passed as the expected value, and a strict array comparison is done
  • Options like StringOptions, HTMLOptions, ToBeDisplayedOptions are, for now, kept separately and will apply to the entire array if applicable. No support per array value is planned
  • Only NumberOptions will be allowed to be passed as multiple expected values since it is the only "option" that is really an expected value.

Array Comparison Behaviour

  • When passing a single expected value, by default, ALL elements' array values must strictly match the expected value.
    • When the actual element's value is a text, the default trimmed default will apply; using options { trim: false } will disable that behaviour and do a strict match
  • When passing multiple expected values, by default, the element's array must STRICTLY match the expected provided array
    • Each actual element's value per index is compared to the same index value for the expected value array
    • If the expected array values' length is not the same as the number of elements, the comparison fails
    • element's value will NOT be compared anymore to any of the array's expected values.
      • Only Matcher toHaveText will preserve the "any array value" compare behaviour to not break, but that is deprecated
    • When the actual element's value is a text, the default trimmed default will apply; using options { trim: false } will disable that behaviour and do a strict match
    • When the matcher uses NumberOptions, the strict match cannot be applied thoroughly, but the NumberOptions rules will apply correctly

isNot

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

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

Edge cases

  • When no elements are found, we fail at all times with or without .not, even if the expected is an empty array.
    • If the expected value is an empty array, we could not fail for the above, but until the community needs this case, let's not support it. For now use toBeElementsArrayOfSize(0)

Bugs 🐛 (fixed when checked)

  • Fix non-awaited $$() like in expect($$('el')).toHaveText('text') erroring to Error: Can't call "getText" on element with selector "#username", it is not a function.
  • Array comparison will also trim actual element text by default, same as when passing a single expected value
  • Fix toBeElementsArrayOfSize working with Element[] but not having the typing working (fixed by Make type Element[] work with toBeElementsArrayOfSize #1980)
  • Matcher toHaveElementProperty was supporting an optional value, but using it with an existing or non-existing property always ends up in a test failure. So the typing for it was changed. Later, we could bring it back and have the same behaviour as toHaveAttribute, where it checks for the existence of the property.
  • Fix $$().toHaveText('C' | ['C']) that should fail when there are no elements, since we need to have elements having the text, which is not true in this case.
  • Multiple matchers were reporting in the before and after hooks the wrong matcher name
  • BREAKING Removing the only remaining containing matcher named toHaveClassContaining, deprecated for 2 years now
  • BREAKING Removing toHaveClass deprecated 4 versions ago
  • toBeElementsArrayOfSize await before the waitUntil and use a refetch function, verify to move the await inside the waitUntil and remove refetch since it will be replaced by the await? Or do we need to refetch in all other matchers?

Future consideration

  • For $$(), we could support an array per element, where we can compare if that element contains those values.
    • Not done today since this implies passing an array of arrays, which complicated the typing so that it is allowed only for $$() and not $()
    • Moreover, the same behaviour can be achieved by regex or possibly even with stringContaining
  • Support empty array expected value passing against no elements found. Today, let's rely on toBeElementsArrayOfSize(0)
  • NumberOptions is not an options like the others; it does not convey a side-effect for the expected value, but instead is the expected value. It should be deprecated as an "options" and be an "ExpectedType" instead.
    • Keeping it as an option makes it harder to streamline other options like a custom error message or wait, so deprecating it as an option would clarify that we need a separate option for those.
  • There is no typing, nor unification on how to call before and afterAssertion, something to do.

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?

Waiting on the merge of

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
return expected
}

export const isElementArray = (obj: unknown): obj is WebdriverIO.ElementArray => {
Copy link
Contributor Author

@dprevost-LMI dprevost-LMI Jan 12, 2026

Choose a reason for hiding this comment

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

To review, this is probably not just ElementArray... do we need to check for foundWith = $$? Does a single element have a parent? Maybe checking for getElements would also be good...

values: result.value
elementOrArray: isSingleElement && awaitedElements?.length === 1 ? awaitedElements[0] : awaitedElements,
success: results.length > 0 && results.every((res) => res.result === true),
valueOrArray: isSingleElement && results.length === 1 ? results[0].value : results.map(({ value }) => value),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤔 That could unwrapped an singleton array value expecting to be an array ? To review!

{
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

- 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
- 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
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.

el?.isDisplayed is not a function

1 participant