Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8d487cb
Ensure mocks used represented wdio framework reality
dprevost-LMI Jan 9, 2026
33349a8
fix bad command
dprevost-LMI Jan 10, 2026
3e5d3c3
Working case of $$ aka ElementArray with `toBeDisplayed`
dprevost-LMI Jan 8, 2026
0a75715
Use Promise.all for ElementArray instead of waiting one by one
dprevost-LMI Jan 8, 2026
dbc3d80
Add UTs for ElementArray and Element[]
dprevost-LMI Jan 8, 2026
0696f38
Review after rebase + add awaited element case
dprevost-LMI Jan 8, 2026
31e20ae
fix regression while trying to support Element[]
dprevost-LMI Jan 8, 2026
7eae5be
Code review
dprevost-LMI Jan 8, 2026
f37d9da
Add assertions of `executeCommandBeWithArray` and `waitUntil`
dprevost-LMI Jan 8, 2026
96667f5
All to be matchers now compliant with multiple elements + UTs
dprevost-LMI Jan 9, 2026
5587393
fix rebase
dprevost-LMI Jan 10, 2026
f89d340
Fix ChainablePromiseElement/Array not correctly considered
dprevost-LMI Jan 10, 2026
62a200b
Add coverage
dprevost-LMI Jan 10, 2026
dd1107e
Enhance toHaveText test cases
dprevost-LMI Jan 10, 2026
c058c1d
Working cases of toHaveWidth and toHaveHTML with $$()
dprevost-LMI Jan 10, 2026
5048457
Ensure all test dans can run fast with `wait: 1`
dprevost-LMI Jan 10, 2026
b9a1846
Add coverage on `executeCommandWithArray` and support edge case
dprevost-LMI Jan 10, 2026
6daeaa6
Review error message assertions and discover a problem
dprevost-LMI Jan 10, 2026
1b9d738
Have failure messages better asserted
dprevost-LMI Jan 10, 2026
e0b5003
toHaveAttribute supporting $$() now
dprevost-LMI Jan 10, 2026
485ac1a
Make all `toHave` matcher follow same code patterns
dprevost-LMI Jan 10, 2026
69469dd
Remove `executeCommand` and simplify executeCommandWithArray + coverage
dprevost-LMI Jan 10, 2026
fc1bae9
Align typing with expected being an array + trim by default for arrays
dprevost-LMI Jan 11, 2026
4fc12d6
Support of `toHaveHeigth`, `toHaveSize` & `toHaveWidth``
dprevost-LMI Jan 11, 2026
4bea20d
Working support of `$$()` in all element matchers
dprevost-LMI Jan 12, 2026
4932e4f
Deprecate `compareTextWithArray` & remove toHaveClassContaining
dprevost-LMI Jan 12, 2026
e32619a
fix possible regression around NumberOptions wait & internal not cons…
dprevost-LMI Jan 12, 2026
7d6acee
Forget toHaveStyle + code review
dprevost-LMI Jan 12, 2026
eec4b23
Code review
dprevost-LMI Jan 12, 2026
e7123c1
better function naming
dprevost-LMI Jan 12, 2026
60abeb1
Code review
dprevost-LMI Jan 12, 2026
38f0a0b
Code review + remove `toHaveClass` deprecated since v1, 4 versions ago
dprevost-LMI Jan 12, 2026
0817e3b
Support unknown type for `toHaveElementProperty` since example existed
dprevost-LMI Jan 12, 2026
3878980
doc: official support of `$$()`
dprevost-LMI Jan 13, 2026
d890d7e
Review & add coverage + discover problem with isNot
dprevost-LMI Jan 13, 2026
f58f42c
fix for other PR, waitunitl + toBeerror - to revert later?
dprevost-LMI Jan 13, 2026
086dfe6
fix `.not` cases
dprevost-LMI Jan 14, 2026
31eec83
Finish matchers UT's refactor to be mocked and type safe
dprevost-LMI Jan 14, 2026
25edfab
Missed some bind
dprevost-LMI Jan 14, 2026
6d6414a
Review waitUntil + coverage
dprevost-LMI Jan 14, 2026
1ab8f3d
Better documentation
dprevost-LMI Jan 15, 2026
5a1ffe9
Code review
dprevost-LMI Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,5 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm install --force
- name: Build
run: npm run build
- name: Run Tests
run: npm test
- name: Run All Checks
run: npm run checks:all
Comment on lines +24 to +25
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

63 changes: 43 additions & 20 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

When you're writing tests, you often need to check that values meet certain conditions. `expect` gives you access to a number of "matchers" that let you validate different things on the `browser`, an `element` or `mock` object.

**Note:** Multi-remote is not yet supported; any working case is coincidental and could break or change until fully supported.

## Soft Assertions

Soft assertions allow you to continue test execution even when an assertion fails. This is useful when you want to check multiple conditions in a test and collect all failures rather than stopping at the first failure. Failures are collected and reported at the end of the test.
Expand Down Expand Up @@ -252,15 +254,43 @@ await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard tex

## Element Matchers

### Multiples Elements Support

All element matchers work with arrays of elements (e.g., `$$()` results).
- In short, matchers is applied on each elements and must pass for the entire assertion to succeed, so if one fails, the assertions fails.
- See [MutipleElements.md](MultipleElements.md) for more information.

#### Usage

```ts
await expect($$('#someElem')).toBeDisplayed()
await expect(await $$('#someElem')).toBeDisplayed()
```

```ts
const elements = await $$('#someElem')

// Single expected value compare with each element's value
await expect(elements).toHaveAttribute('class', 'form-control')

// Multiple expected values for exactly 2 elements having exactly 'control1' & 'control2' as values
await expect(elements).toHaveAttribute('class', ['control1', 'control2'])

// Multiple expected values for exactly 2 elements but with more flexibility for the first element's value
await expect(elements).toHaveAttribute('class', [expect.stringContaining('control1'), 'control2'])

// Filtered array also works
await expect($$('#someElem').filter(el => el.isDisplayed())).toHaveAttribute('class', ['control1', 'control2'])
```

### toBeDisplayed

Calls [`isDisplayed`](https://webdriver.io/docs/api/element/isDisplayed/) on given element.

##### Usage

```js
const elem = await $('#someElem')
await expect(elem).toBeDisplayed()
await expect($('#someElem')).toBeDisplayed()
```

### toExist
Expand All @@ -270,8 +300,7 @@ Calls [`isExisting`](https://webdriver.io/docs/api/element/isExisting) on given
##### Usage

```js
const elem = await $('#someElem')
await expect(elem).toExist()
await expect($('#someElem')).toExist()
```

### toBePresent
Expand All @@ -281,8 +310,7 @@ Same as `toExist`.
##### Usage

```js
const elem = await $('#someElem')
await expect(elem).toBePresent()
await expect($('#someElem')).toBePresent()
```

### toBeExisting
Expand Down Expand Up @@ -375,8 +403,7 @@ Checks if an element can be clicked by calling [`isClickable`](https://webdriver
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toBeClickable()
await expect($('#elem')).toBeClickable()
```

### toBeDisabled
Expand Down Expand Up @@ -412,8 +439,7 @@ Checks if an element is enabled by calling [`isSelected`](https://webdriver.io/d
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toBeSelected()
await expect($('#elem')).toBeSelected()
```

### toBeChecked
Expand All @@ -423,8 +449,7 @@ Same as `toBeSelected`.
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toBeChecked()
await expect($('#elem')).toBeChecked()
```

### toHaveComputedLabel
Expand Down Expand Up @@ -502,8 +527,7 @@ Checks if element has a specific `id` attribute.
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toHaveId('elem')
await expect($('#elem')).toHaveId('elem')
```

### toHaveStyle
Expand All @@ -513,8 +537,7 @@ Checks if an element has specific `CSS` properties. By default, values must matc
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toHaveStyle({
await expect($('#elem')).toHaveStyle({
'font-family': 'Faktum',
'font-weight': '500',
'font-size': '12px',
Expand Down Expand Up @@ -549,10 +572,11 @@ In case there is a list of elements in the div below:
You can assert them using an array:

```js
const elem = await $$('ul > li')
await expect(elem).toHaveText(['Coffee', 'Tea', 'Milk'])
await expect($$('ul > li')).toHaveText(['Coffee', 'Tea', 'Milk'])
```

**Note:** Assertion with multiple elements will pass if the element text's matches any of the text in the arrays. Strict array matching is not yet supported.

### toHaveHTML

Checks if element has a specific text. Can also be called with an array as parameter in the case where the element can have different texts.
Expand Down Expand Up @@ -583,8 +607,7 @@ Checks if an element is within the viewport by calling [`isDisplayedInViewport`]
##### Usage

```js
const elem = await $('#elem')
await expect(elem).toBeDisplayedInViewport()
await expect($('#elem')).toBeDisplayedInViewport()
```

### toHaveChildren
Expand Down
24 changes: 24 additions & 0 deletions docs/MultipleElements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Multiple Elements Support

All element matchers work with arrays of elements (e.g., `$$()` results).
- **Strict Length Matching**: If you provide an array of expected values, the number of values must match the number of elements found. A failure occurs if the lengths differ.
- **Index-based Matching**: When using an array of expected values, each element is compared to the value at the corresponding index.
- **Single Value Matching**: If you provide a single expected value, it is compared against *every* element in the array.
- **Asymmetric Matchers**: Asymmetric matchers can be used within the expected values array for more matching flexibility.
- If no elements exist, a failure occurs (except with `toBeElementsArrayOfSize`).
- Options like `StringOptions` or `HTMLOptions` apply to the entire array (except `NumberOptions`).
- The assertion passes only if **all** elements match the expected value(s).
- Using `.not` applies the negation to each element (e.g., *all* elements must *not* display).

**Note:** Strict length matching does not apply on `toHaveText` to preserve existing behavior.

## Limitations
- An alternative to using `StringOptions` (like `ignoreCase` or `containing`) for a single expected value is to use RegEx (`/MyExample/i`) or Asymmetric Matchers (`expect.stringContaining('Example')`).
- Passing an array of "containing" values, as previously supported by `toHaveText`, is deprecated and not supported for other matchers.

## Supported types

Any of the below element types can be passed to `expect`:
- `ChainablePromiseArray` (the non-awaited case)
- `ElementArray` (the awaited case)
- `Element[]` (the filtered case)
9 changes: 8 additions & 1 deletion src/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Browser matchers
export * from './matchers/browser/toHaveClipboardText.js'
export * from './matchers/browser/toHaveTitle.js'
export * from './matchers/browser/toHaveUrl.js'

// Element matchers
export * from './matchers/element/toBeClickable.js'
export * from './matchers/element/toBeDisabled.js'
export * from './matchers/element/toBeDisplayed.js'
Expand All @@ -11,9 +14,9 @@ export * from './matchers/element/toBeFocused.js'
export * from './matchers/element/toBeSelected.js'
export * from './matchers/element/toHaveAttribute.js'
export * from './matchers/element/toHaveChildren.js'
export * from './matchers/element/toHaveClass.js'
export * from './matchers/element/toHaveComputedLabel.js'
export * from './matchers/element/toHaveComputedRole.js'
export * from './matchers/element/toHaveElementClass.js'
export * from './matchers/element/toHaveElementProperty.js'
export * from './matchers/element/toHaveHeight.js'
export * from './matchers/element/toHaveHref.js'
Expand All @@ -25,7 +28,11 @@ export * from './matchers/element/toHaveText.js'
export * from './matchers/element/toHaveValue.js'
export * from './matchers/element/toHaveWidth.js'
export * from './matchers/elements/toBeElementsArrayOfSize.js'

// Mock matchers
export * from './matchers/mock/toBeRequested.js'
export * from './matchers/mock/toBeRequestedTimes.js'
export * from './matchers/mock/toBeRequestedWith.js'

// Snapshot matchers
export * from './matchers/snapshot.js'
4 changes: 2 additions & 2 deletions src/matchers/element/toBeClickable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeClickable(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'clickable'
Expand Down
4 changes: 2 additions & 2 deletions src/matchers/element/toBeDisabled.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeDisabled(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'disabled'
Expand Down
4 changes: 2 additions & 2 deletions src/matchers/element/toBeDisplayed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = {
...DEFAULT_OPTIONS,
Expand All @@ -11,7 +11,7 @@ const DEFAULT_OPTIONS_DISPLAYED: ExpectWebdriverIO.ToBeDisplayedOptions = {
}

export async function toBeDisplayed(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.ToBeDisplayedOptions = DEFAULT_OPTIONS_DISPLAYED,
) {
this.expectation = this.expectation || 'displayed'
Expand Down
4 changes: 2 additions & 2 deletions src/matchers/element/toBeDisplayedInViewport.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeDisplayedInViewport(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'displayed in viewport'
Expand Down
4 changes: 2 additions & 2 deletions src/matchers/element/toBeEnabled.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeEnabled(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'enabled'
Expand Down
21 changes: 14 additions & 7 deletions src/matchers/element/toBeExisting.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { executeCommandBe, aliasFn } from '../../utils.js'
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementMaybePromise, WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toExist(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'exist'
this.verb = this.verb || ''
this.matcherName = this.matcherName || 'toExist'

await options.beforeAssertion?.({
matcherName: 'toExist',
matcherName: this.matcherName,
options,
})

const result = await executeCommandBe.call(this, received, el => el?.isExisting(), options)

await options.afterAssertion?.({
matcherName: 'toExist',
matcherName: this.matcherName,
options,
result
})
Expand All @@ -26,8 +27,14 @@ export async function toExist(
}

export function toBeExisting(el: WdioElementMaybePromise, options?: ExpectWebdriverIO.CommandOptions) {
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

return toExist.call(this, el, options)
}
export function toBePresent(el: WdioElementMaybePromise, options?: ExpectWebdriverIO.CommandOptions) {
return aliasFn.call(this, toExist, { verb: 'be', expectation: 'present' }, el, options)
this.verb = 'be'
this.expectation = 'present'
this.matcherName = 'toBePresent'
return toExist.call(this, el, options)
}
4 changes: 2 additions & 2 deletions src/matchers/element/toBeFocused.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeFocused(
received: WdioElementMaybePromise,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.expectation = this.expectation || 'focused'
Expand Down
26 changes: 10 additions & 16 deletions src/matchers/element/toBeSelected.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
import { executeCommandBe } from '../../utils.js'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementOrArrayMaybePromise } from '../../types.js'

export async function toBeSelected(
received: ChainablePromiseElement | WebdriverIO.Element,
received: WdioElementOrArrayMaybePromise,
options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS
) {
this.verb = this.verb || 'be'
this.expectation = this.expectation || 'selected'
this.matcherName = this.matcherName || 'toBeSelected'

await options.beforeAssertion?.({
matcherName: 'toBeSelected',
matcherName: this.matcherName,
options,
})

const result = await executeCommandBe.call(this, received, el => el?.isSelected(), options)

await options.afterAssertion?.({
matcherName: 'toBeSelected',
matcherName: this.matcherName,
options,
result
})

return result
}

export async function toBeChecked (el: WebdriverIO.Element, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) {
export async function toBeChecked (received: WdioElementOrArrayMaybePromise, options: ExpectWebdriverIO.CommandOptions = DEFAULT_OPTIONS) {
this.verb = 'be'
this.expectation = 'checked'
this.matcherName = 'toBeChecked'

await options.beforeAssertion?.({
matcherName: 'toBeChecked',
options,
})

const result = await toBeSelected.call(this, el, options)

await options.afterAssertion?.({
matcherName: 'toBeChecked',
options,
result
})
const result = await toBeSelected.call(this, received, options)

return result
}
Loading
Loading