# Migration Guide: v7 to v8 ## Node.js 22.23.4, 24.22.1 or 26.12.1 v8 requires Node.js `22.23.4`, `24.22.1` or `26.12.1`, or a later release of these lines, the same as WebdriverIO v10. These are the first releases with the [updated CA store](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) for the eMudhra intermediate CA compromise. Node.js 20, 23 and 25 are not supported. ## Peer dependencies v8.0.0 requires WebdriverIO v10: `webdriverio` `10.0.0` or higher. For WebdriverIO v9.31.5 or higher, use expect-webdriverio v7. The types need TypeScript `6.0.3` or higher, the same as WebdriverIO v10. `@wdio/globals` and `@wdio/logger` are no longer peer dependencies: only `webdriverio` and `@wdio/types` are. You can remove them from your `package.json` if you do not use them yourself. `webdriverio` installs `@wdio/types`, so our types use the same copy as WebdriverIO. With Yarn Plug'n'Play, also add `@wdio/types` to your `package.json` (Yarn warns with `YN0002`). If you do not, the types of `SnapshotService` and `SoftAssertionService` become `any`. When `toHaveClipboardText` cannot set the clipboard permissions, its warning now goes to `console.warn`, with the prefix `expect-webdriverio:`, once per worker. Before, it went to the WebdriverIO logger at each try, also at each retry of the wait. ## Strict `$()` In WebdriverIO v10, `$()` rejects with a `StrictSelectorError` when the selector matches several elements. An assertion on that `$()` rejects with this error, not with a failed assertion, also with `.not`: ```ts // The page has 2 `h1` await expect($('h1')).toBeDisplayed() // rejects: strict mode violation: `$("h1")` resolved to 2 elements, expected 1. await expect($$('h1')).toBeDisplayed() // every `h1` await expect($('h1', { strict: false })).toBeDisplayed() // the first `h1` ``` ## Matcher names in `beforeAssertion` and `afterAssertion` The `matcherName` given to the `beforeAssertion` and `afterAssertion` hooks is now the name of the matcher that you called. Before, some aliases sent the name of the matcher that they use: | Matcher | Before | Now | | ------- | ------ | --- | | `toBeExisting`, `toBePresent` | `toExist` | its own name | | `toHaveLink` | `toHaveHref` | `toHaveLink` | | `toHaveValue` | `toHaveElementProperty` | `toHaveValue` | | `toBeRequested` | `toBeRequestedTimes` | `toBeRequested` | The `expectedValue` given to `afterAssertion` is now the value that you gave, the same as for `beforeAssertion`. Before, `toHaveText`, `toHaveComputedLabel`, `toHaveComputedRole` and `toHaveElementProperty` gave it after the string options changed it, e.g. `ignoringCaseOneOf<"a", "b">` for `expect.oneOf('a', 'b')` with `ignoreCase`. The hooks also get no internal argument of the matcher anymore: | Assertion | `expectedValue` before | Now | | --------- | ---------------------- | --- | | `toHaveValue('Hello')` | `['value', 'Hello']` | `'Hello'`, as `toHaveId('main')` gives `'main'` | | `toHaveElementProperty('checked')` | `['checked', Anything]` | `['checked', undefined]`, as `toHaveAttribute('checked')` | | `toHaveLocalStorageItem('key')` | `'key'` | `['key', undefined]`, as `toHaveAttribute('key')` | | `toHaveLocalStorageItem('key', '')` | `'key'` | `['key', '']` | ## `toHaveText` on multiple elements The strict strategy of the `useToHaveTextStrictMultiElementsCompareStrategy` feature flag is now the only one. The flag, `setFeatureFlags()`, the `featureFlags` option and the `ExpectWebdriverIO.FeatureFlags` type are removed. The internal `utils.compareTextWithArray()` and `utils.getFeatureFlagValue()` are also removed. With `$$()`, an array of expected values is index-based: its length must equal the element count, and each element must match the value at its index, as with every other matcher. Before, each element could match any value of the array. ```diff //
  • Coffee
  • Tea
  • // Inverted order - await expect($$('li')).toHaveText(['Tea', 'Coffee']) // Same order as the elements + await expect($$('li')).toHaveText(['Coffee', 'Tea']) // Each element has one of the texts; a text can be missing (as before) + await expect($$('li')).toHaveText(expect.oneOf('Tea', 'Coffee')) // Each text is on at least one element; elements with other texts are allowed + await expect($$('li')).toHaveText(expect.arrayContaining(['Tea', 'Coffee'])) ``` See [Choosing the expected value](MultipleElements.md#choosing-the-expected-value) for the difference between the options. `some()`, `expect.oneOf()` inside an expected array and multi-remote elements work without the flag. With `.not`, every element must not match. An array of expected values on a single element fails the assertion with `toHaveText`, `toHaveHTML`, `toHaveComputedLabel` and `toHaveComputedRole`: use `expect.oneOf()`. ```diff - await expect($('h1')).toHaveText(['Welcome', 'Bienvenue']) + await expect($('h1')).toHaveText(expect.oneOf('Welcome', 'Bienvenue')) ``` `expect.oneOf()` now trims the actual value by default (`trim: true`), as a single expected value does. Pass `{ trim: false }` to compare the text as is. ## `toHaveElementClass` compares each class `toHaveElementClass` compares each class of the element, also with an asymmetric matcher. Before, an asymmetric matcher was compared with the full `class` attribute. To compare the full attribute, use `toHaveAttribute('class', ...)`. The classes are now split on ASCII whitespace (space, tab, new line, form feed and carriage return), as in HTML, and the string options (`ignoreCase`, `trim`, `containing`...) also apply to `expect.oneOf()`. An array of expected values on a single element fails the assertion, as with the other matchers. Before, it meant "has any of these classes". On `$$()`, an array stays one expected value for each element. ```diff //