Expect
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: Browser, element, network and snapshot matchers also support multi-remote, checking every browser instance with a single expected value or, for browser, element and network matchers, one per instance.
Examples that do not open a page or show their HTML assume the guinea pig test page is open:
await browser.url('https://guinea-pig.webdriver.io/')
// or, with multi-remote
await multiRemoteBrowser.url('https://guinea-pig.webdriver.io/')
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.
Usage
// Mocha example
it('test page smoke', async () => {
// These won't throw immediately if they fail
await expect.soft(await $('header h1').getText()).toEqual('WebdriverJS Testpage');
await expect.soft(await $('.sometext').getText()).toMatch(/some/);
// Also work with basic matcher
const h1Text = await $('header h1').getText()
expect.soft(h1Text).toEqual('WebdriverJS Testpage');
// Regular assertions still throw immediately
await expect(await $('.sendBtn').isClickable()).toBe(true);
});
// At the end of the test, all soft assertion failures
// will be reported together with their details
Soft Assertion API
expect.soft()
Creates a soft assertion that collects failures instead of immediately throwing errors.
await expect.soft(actual).toBeDisplayed();
await expect.soft(actual).not.toHaveText('Wrong text');
expect.getSoftFailures()
Get all collected soft assertion failures for the current test.
const failures = expect.getSoftFailures();
console.log(`There are ${failures.length} soft assertion failures`);
expect.assertSoftFailures()
Manually assert all collected soft failures. This will throw an aggregated error if any soft assertions have failed.
// Manually throw if any soft assertions have failed
expect.assertSoftFailures();
expect.clearSoftFailures()
Clear all collected soft assertion failures for the current test.
// Clear all collected failures
expect.clearSoftFailures();
Integration with Test Frameworks
The soft assertions feature integrates with WebdriverIO's test runner automatically. By default, it will report all soft assertion failures at the end of each test (Mocha) or step (Cucumber).
To use with WebdriverIO, add the SoftAssertionService to your services list:
// wdio.conf.js
import { SoftAssertionService } from 'expect-webdriverio'
export const config = {
// ...
services: [
// ...other services
[SoftAssertionService, {}]
],
// ...
}
Configuration Options
The SoftAssertionService can be configured with options to control its behavior:
// wdio.conf.js
import { SoftAssertionService } from 'expect-webdriverio'
export const config = {
// ...
services: [
// ...other services
[SoftAssertionService, {
// Disable automatic assertion at the end of tests (default: true)
autoAssertOnTestEnd: false
}]
],
// ...
}
autoAssertOnTestEnd
- Type:
boolean - Default:
true
When set to true (default), the service will automatically assert all soft assertions at the end of each test and throw an aggregated error if any failures are found. When set to false, you must manually call expect.assertSoftFailures() to verify soft assertions.
This is useful if you want full control over when soft assertions are verified or if you want to handle soft assertion failures in a custom way.
Known limitations
The soft assertions service is not supported under Jasmine (e.g. @wdio/jasmine-framework) using the global import because Jasmine is already designed to provide similar behavior out of the box.
Default Options
These default options below are connected to the waitforTimeout and waitforInterval options set in the config.
Only set the options below if you want to wait for specific timeouts for your assertions.
{
wait: 2000, // ms to wait for expectation to succeed
interval: 100, // interval between attempts
}
If you like to pick different timeouts and intervals, set these options like this:
// wdio.conf.js
import { setDefaultOptions } from 'expect-webdriverio'
export const config = {
// ...
before () {
setDefaultOptions({ wait: 5000 })
},
// ...
}
Matcher Options
Every matcher can take several options that allows you to modify the assertion:
Command Options
| Name | Type | Details |
|---|---|---|
wait | number | time in ms to wait for expectation to succeed. Default: 2000 |
interval | number | interval between attempts. Default: 100 |
beforeAssertion | function | function to be called before assertion is made |
afterAssertion | function | function to be called after assertion is made containing assertion results, with the same expected value as beforeAssertion |
message | string | user message to prepend before assertion error |
The hooks get { matcherName, expectedValue, options }, and afterAssertion also gets result ({ pass, message }):
matcherNameis the matcher that you called, also for an alias, e.g.toBeChecked, nottoBeSelected.expectedValueis the value that you gave, before the string options change it. The matchers with a name give the name and the value:[name, value]fortoHaveAttribute,toHaveElementProperty,toHaveCookie,toHaveLocalStorageItemandtoHaveSessionStorageItem, e.g.['title', undefined]fortoHaveAttribute('title'). The boolean matchers (toBeDisplayed,toExist…) give noexpectedValue.
String Options
This option can be applied in addition to the command options when strings are being asserted.
| Name | Type | Details |
|---|---|---|
ignoreCase | boolean | apply toLowerCase to both actual and expected values. A RegExp expected value is matched case-insensitively instead (the i flag is applied), since a pattern cannot be lowercased safely. |
trim | boolean | apply trim to actual value. Default: true |
replace | Replacer | Replacer[] | replace parts of the actual value that match the string/RegExp. The replacer can be a string or a function. |
containing | boolean | expect actual value to contain expected value, otherwise strict equal. |
asString | boolean | might be helpful to force converting property value to string |
atStart | boolean | expect actual value to start with the expected value |
atEnd | boolean | expect actual value to end with the expected value |
atIndex | number | expect actual value to have the expected value at the given index |
Use one position option only: containing, atStart, atEnd or atIndex. With 2 or more, the matcher throws at once, also with .not: The string options containing and atStart cannot be used together: use only one of containing, atStart, atEnd and atIndex. replace applies before the position option.
In a failure message, Received shows the actual value as is (not trimmed, lowercased or replaced), and Expected names the string options that alter the actual value before the comparison. The position comes first (containing, startingWith, endingWith, matchingAtIndex<n>), then trimmed, ignoringCase and replacing. trim is on by default, so trimmed is named only when the actual value had spaces at the start or the end; trim: false alters nothing, so it is not named. When no option applies, the expected value is printed as is. For one string value, the name is in the label (Expected (ignoringCase)), so that Jest's diff still shows the changed characters, or the changed lines of a multiline value. In a list or per-instance values, each expected value has the name (ignoringCase<"Foo">). A RegExp is printed with its flags (ignoreCase adds i), and expect.oneOf() adds OneOf to the name. An element or a multi-remote instance that passed, also only because of the options, is a line with no change in the diff. When the options changed the actual value, the message also shows the value that the matcher compared: after Received for one value, and after each received value that failed in the diff of a list or per-instance values. It is not shown for a multiline value, or when the same received value was compared in 2 ways (e.g. with a string and with a RegExp).
await expect($('h1')).toHaveText('Other', { ignoreCase: true, containing: true })
// Expected (containingTrimmedIgnoringCase): "Other"
// Received: " Hello World " (compared as "hello world")
await expect($$('li')).toHaveText(['Foo', 'Bar'], { ignoreCase: true })
// texts: "foo", " Baz "
// Array [
// ignoringCase<"Foo">,
// - trimmedIgnoringCase<"Bar">,
// + " Baz ", (compared as "baz")
// ]
Number Matcher
Number matchers (toHaveWidth, toHaveHeight, toHaveChildren, toBeElementsArrayOfSize, toBeRequestedTimes, and each field of toHaveSize) take a number, a NumberMatcher, expect.oneOf() with numbers, or an asymmetric matcher as the expected value, and the command options as the next argument, e.g. toHaveWidth({ gte: 32 }, { wait: 0 }).
| Name | Type | Details |
|---|---|---|
eq | number | equals |
lte | number | less then equals |
gte | number | greater than or equals |
Give eq alone, or a range with gte, lte or both. The types reject {} and eq with gte or lte.
expect.oneOf() with numbers is one of these numbers, which a range cannot say: toHaveWidth(expect.oneOf(100, 200)) passes for 100 or 200, and fails for 150.
An asymmetric matcher compares the number as in toEqual, e.g. expect.closeTo(150.4, 0) for a size that the browser rounds, and expect.not.closeTo(). A list matcher (expect.arrayContaining()…) throws, and expect.multiRemote() is only for the multi-remote elements and mocks.
Deep Equality
Objects are compared with the deep equality of Jest's toEqual, the same in every matcher: toHaveSize, toHaveElementProperty (a property that is not a string), expect.multiRemote(), and the asymmetric matchers such as expect.objectContaining().
- An asymmetric matcher works anywhere in the value, e.g.
toHaveSize({ width: expect.any(Number), height: 32 }). - A property that is
undefinedis ignored:{ a: 1, b: undefined }equals{ a: 1 }. For a strict comparison, useexpect(await elem.getProperty('dataset')).toStrictEqual({ a: 1 }). - A class instance equals a plain object with the same properties.
- URLs, sets, maps, dates and binary data are compared by their content, see Deep equality of URLs, sets, maps and binary data.
NaNequalsNaN.+0and-0are not equal.
Handling HTML Entities
An HTML entity is a piece of text (“string”) that begins with an ampersand (&) and ends with a semicolon (;). Entities are frequently used to display reserved characters (which would otherwise be interpreted as HTML code), and invisible characters (like non-breaking spaces, e.g. ).
To find or interact with such element use unicode equivalent of the entity. e.g.:
<div data="Some Value">Some Text</div>
const myElem = await $('div[data="Some\u00a0Value"]')
await expect(myElem).toHaveAttribute('data', 'div[Some\u00a0Value')
await expect(myElem).toHaveText('Some\u00a0Text')
You can find all unicode references in the HTML spec.
Note: unicode is case-insensitive hence both \u00a0 and \u00A0 works. To find element in browser inspect, remove u from unicode e.g.: div[data="Some\00a0Value"]
Browser Matchers
Browser matchers support the multi-remote browser, with a single expected value or one per instance, see Multi-remote Support.
Browsing contexts (tab, window, frame)
The browser matchers also accept a WebdriverIO v10 browsing context: a tab, a window or a frame that browser.url(), browser.newWindow(), browser.browsingContexts() or context.frame() give in a WebDriver BiDi session. They read the document of that context, not the one of the current context of the session.
const page = await browser.url('https://webdriver.io/')
await expect(page).toHaveTitle(expect.stringContaining('WebdriverIO'))
const frame = await page.frame(page.$('iframe'))
await expect(frame).toHaveUrl(expect.stringContaining('/embed'))
await expect(frame).toHaveLocalStorageItem('key') // the local storage of the frame's origin
- A failure message names the browser, the window or the frame, and its URL, e.g.
Expect chrome's frame (https://webdriver.io/embed) to have title. It is the URL of the last navigation of the context (context.url). A frame found by its element has no URL untilnavigate()orgetUrl(), so the message shows no URL for it.toHaveUrldoes not show it, because theReceivedline already shows the URL. - The title of a frame document without
<title>is''. toHaveClipboardTextsets the clipboard permission for the whole session. In a cross-origin frame, the permissions policy of the page can blockclipboard-read.- A browsing context is of one browser: there are no per-instance values.
toHaveUrl
Checks if browser is on a specific page.
Usage
await browser.url('https://webdriver.io/')
await expect(browser).toHaveUrl('https://webdriver.io')
Usage
await browser.url('https://webdriver.io/')
await expect(browser).toHaveUrl(expect.stringContaining('webdriver'))
toHaveTitle
Checks if website has a specific title.
Usage
await browser.url('https://webdriver.io/')
await expect(browser).toHaveTitle('WebdriverIO · Next-gen browser and mobile automation test framework for Node.js')
await expect(browser).toHaveTitle(expect.stringContaining('WebdriverIO'))
toHaveClipboardText
Checks if the browser has a specific text stored in its clipboard.
Usage
import { Key } from 'webdriverio'
await browser.keys([Key.Ctrl, 'a'])
await browser.keys([Key.Ctrl, 'c'])
await expect(browser).toHaveClipboardText('some clipboard text')
await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard text'))
toHaveCookie
Checks if the browser has a cookie with this name, and with an optional value. The matcher reads getCookies({ name }) and compares the value of the cookie as in toHaveText, with the string options. With no value, it checks that the cookie exists; with .not, that it does not exist. When the cookie does not exist, the failure message shows Received: no cookie. It works on a browser, a browsing context and a multi-remote browser (one value for all the instances, or one per instance with expect.multiRemote()).
Usage
await expect(browser).toHaveCookie('session')
await expect(browser).toHaveCookie('lang', 'en')
await expect(browser).toHaveCookie('lang', expect.oneOf('en', 'fr'), { ignoreCase: true })
await expect(browser).not.toHaveCookie('tracking')
toHaveLocalStorageItem
Checks if browser has a specific item in localStorage with an optional value. With no value, it checks that the item exists; with .not, that it does not exist. A missing item never matches, also not a matcher that accepts no value, and the failure message shows Received: no item.
Usage
await browser.url('https://webdriver.io/')
// Check if localStorage item exists
await expect(browser).toHaveLocalStorageItem('existingKey')
// Check localStorage item with exact value
await expect(browser).toHaveLocalStorageItem('someLocalStorageKey', 'someLocalStorageValue')
// Check with case insensitive
await expect(browser).toHaveLocalStorageItem('key', 'uppercase', { ignoreCase: true })
// Check with trim
await expect(browser).toHaveLocalStorageItem('key', 'value', { trim: true })
// Check with containing
await expect(browser).toHaveLocalStorageItem('key', 'long', { containing: true })
// Check with regex
await expect(browser).toHaveLocalStorageItem('userId', /^user_\d+$/)
toHaveSessionStorageItem
Checks if browser has a specific item in sessionStorage with an optional value, as toHaveLocalStorageItem. With no value, it checks that the item exists; with .not, that it does not exist. A missing item never matches, also not a matcher that accepts no value, and the failure message shows Received: no item. The session storage is of the page of the browser or of the browsing context, and of its origin.
Usage
await browser.url('https://webdriver.io/')
// Check if sessionStorage item exists
await expect(browser).toHaveSessionStorageItem('cartId')
// Check sessionStorage item with a value and string options
await expect(browser).toHaveSessionStorageItem('theme', 'dark', { ignoreCase: true })
await expect(browser).not.toHaveSessionStorageItem('token')
toHaveWindowCount
Checks the number of the windows and tabs of the session (getWindowHandles), e.g. after a link opens a new tab. The expected value is a number, a number range ({ gte, lte }), expect.oneOf() with numbers, or an asymmetric matcher. The assertion waits until the count matches, as the other matchers. On a browsing context, it counts the windows of its session.
Usage
await $('a[target=_blank]').click()
await expect(browser).toHaveWindowCount(2)
await expect(browser).toHaveWindowCount({ gte: 2 })
await browser.closeWindow()
await expect(browser).toHaveWindowCount(1)
Element Matchers
Matching a subset of element values
Use expect.arrayContaining() to match values from an element collection in any order, allowing extra elements. This works with toHaveText, toHaveHTML, toHaveAttribute, toHaveElementProperty, toHaveValue, toHaveElementClass, toHaveComputedLabel, toHaveComputedRole, toHaveId, and toHaveHref (including their aliases). To compare with expect.oneOf(), see Choosing the expected value.
await expect($$('header a')).toHaveText(expect.arrayContaining(['2', '1']))
await expect($$('form input')).toHaveValue(expect.arrayContaining(['b', 'a']))
await expect($$('a')).toHaveAttribute('href', expect.arrayContaining([expect.stringContaining('two.html')]))
await expect($$('button')).not.toHaveComputedLabel(expect.arrayContaining(['Delete']))
The other list matchers also compare the complete array: expect.arrayOf() (every value matches) and Jasmine's jasmine.arrayWithExactContents() (the same count, and each value in any order; Jasmine does not count a repeated expected value). See the comparison tables for Jest and Jasmine.
await expect($$('ul > li')).toHaveText(expect.arrayOf(expect.stringMatching(/^(Tea|Coffee)$/)))
await expect($$('ul > li')).toHaveText(jasmine.arrayWithExactContents(['Coffee', 'Tea']))
Each attempt reads one value per element concurrently and applies the asymmetric matcher once to the complete array. Selector-backed collections are refetched on retries. Nested matchers, .not, and expect.not.arrayContaining() retain their normal matching rules. An empty collection matches arrayContaining([]); static empty arrays cannot be retried into a non-empty result.
String comparison options such as trim, ignoreCase, and containing do not transform the collected values or nested matchers. Getter options still apply, such as includeSelectorTag for HTML and asString for properties. Class matching collects each element's complete class attribute, not individual class tokens.
This collection comparison does not change boolean assertions or style assertions. With some(), a list matcher throws an error, also with .not (see some()). A list matcher as the expected value of one element also throws an error, also with .not: in an array of expected values ([expect.arrayContaining(['Home']), 'About']), or in the values of expect.multiRemote() on a multi-remote $(). toHaveElementProperty still compares it with the property of each element, because a property value can be an array. Give the list matcher alone to compare the values of all the elements. On a multi-remote $$(), a list matcher in the values of expect.multiRemote() compares the values of all the elements of that instance, see Multi-remote Support.
On a single element $(), a list matcher throws an error, also with .not: toHaveHTML with a list matcher (arrayContaining, arrayWithExactContents or arrayOf) requires an array of elements. The value of one element is a string, so a list matcher can never match it. Only toHaveElementProperty compares a list matcher with the property of a single element, because a property value can be an array.
toBeDisplayed
Calls isDisplayed on given element.
Usage
await expect($('#someElem')).toBeDisplayed()
toExist
Calls isExisting on given element.
Usage
await expect($('#someElem')).toExist()
toBePresent
Same as toExist.
Usage
await expect($('#someElem')).toBePresent()
toBeExisting
Same as toExist.
Usage
const elem = await $('#someElem')
await expect(elem).toBeExisting()
toBeFocused
Checks if element has focus. This assertion only works in a web context.
Usage
const elem = await $('#someElem')
await expect(elem).toBeFocused()
toBeStable
Checks if an element is stable: it does not move, e.g. at the end of a CSS animation. The matcher reads isStable() and waits until the element is stable, as long as the wait option allows. It works in a browser, not in a native mobile app. In a tab that is not active, isStable() throws an error, because the browser stops the animations there.
Usage
const menu = await $('.menu')
await $('.open-menu').click()
await expect(menu).toBeStable()
await menu.$('a').click()
toBeRequired
Checks if a form field (<input />, <select>, <textarea>) is required: the matcher reads its required property with getProperty('required'). Another element has no such property, so it is not required. For an ARIA widget, use toHaveAttribute('aria-required', 'true').
Usage
await expect($('#email')).toBeRequired()
await expect($('#nickname')).not.toBeRequired()
toBeReadOnly
Checks if a form field (<input />, <textarea>) is read only: the matcher reads its readOnly property with getProperty('readOnly'). Another element has no such property, so it is not read only. For an ARIA widget, use toHaveAttribute('aria-readonly', 'true').
Usage
await expect($('#order-id')).toBeReadOnly()
await expect($('#comment')).not.toBeReadOnly()
toHaveAttribute
Checks if an element has a certain attribute with a specific value. A missing attribute never matches, also not a matcher that accepts no value; the failure message shows Received: no attribute. With no value, it checks that the attribute exists.
Usage
const myInput = await $('.searchinput')
await expect(myInput).toHaveAttribute('name', 'searchinput')
await expect(myInput).toHaveAttribute('name', expect.stringContaining('search'))
Checks if an element has a specific attribute.
Usage
const myInput = await $('.searchinput')
await expect(myInput).toHaveAttribute('name')
// With options
await expect(myInput).toHaveAttribute('name', expect.anything(), { wait: 1000 })
Checks if an element does not have the specified attribute.
Usage
const myInput = await $('.searchinput')
await expect(myInput).not.toHaveAttribute('disabled')
// With options
await expect(myInput).not.toHaveAttribute('disabled', expect.anything(), { wait: 1000 })
toHaveElementClass
Checks if one of the classes of an element matches the expected value: a class name, a regular expression or an asymmetric matcher. Each class is compared, also with an asymmetric matcher. To compare the full class attribute, use toHaveAttribute('class', ...).
Usage
const box = await $('#purplebox')
await expect(box).toHaveElementClass('box', { message: 'Not a box!' })
await expect(box).toHaveElementClass(expect.stringContaining('purp'), { message: 'Not a purple box!' })
// has any of the classes
await expect(box).toHaveElementClass(expect.oneOf('purple', 'red'))
// has all the classes: 1 assertion for each class
await expect(box).toHaveElementClass('box')
await expect(box).toHaveElementClass('purple')
An array of expected values works only with $$(): one expected value for each element. On a single element, it fails the assertion.
toHaveElementProperty
Checks if an element has a certain property and value. A string property is compared with the string options. Another property (a number, a boolean, an object) is compared with deep equality, also with an asymmetric matcher in it, or with expect.oneOf() with numbers. A missing property (null or undefined) never matches, also not a matcher that accepts no value, except an expected null for one element of $$(); the failure message shows Received: no property.
Usage
const elem = await $('#elem')
await expect(elem).toHaveElementProperty('height', 23)
await expect(elem).not.toHaveElementProperty('height', 0)
await expect(elem).toHaveElementProperty('checked', true)
await expect(elem).toHaveElementProperty('dataset', { id: '1', count: expect.any(String) })
Checks if an element has a certain property.
Usage
const elem = await $('#elem')
await expect(elem).toHaveElementProperty('height')
// With options
await expect(elem).toHaveElementProperty('height', expect.anything(), { wait : 1 })
// Does not have height property
await expect(elem).not.toHaveElementProperty('height')
// With options
await expect(elem).not.toHaveElementProperty('height', expect.anything(), { wait : 1 })
toHaveValue
Checks if an input element has a certain value.
Usage
const myInput = await $('.waitForValueEnabled')
await expect(myInput).toHaveValue('Some Content', { ignoreCase: true })
await expect(myInput).toHaveValue(expect.stringContaining('Content'), { ignoreCase: true })
toBeClickable
Checks if an element can be clicked by calling isClickable on the element.
Usage
await expect($('#elem')).toBeClickable()
toBeDisabled
Checks if an element is disabled by calling isEnabled on the element.
Usage
const elem = await $('#elem')
await expect(elem).toBeDisabled()
// same as
await expect(elem).not.toBeEnabled()
toBeEnabled
Checks if an element is enabled by calling isEnabled on the element.
Usage
const elem = await $('#elem')
await expect(elem).toBeEnabled()
// same as
await expect(elem).not.toBeDisabled()
toBeSelected
Checks if an element is enabled by calling isSelected on the element.
Usage
await expect($('#elem')).toBeSelected()
toBeChecked
Same as toBeSelected.
Usage
await expect($('#elem')).toBeChecked()
toHaveComputedLabel
Checks if element has a specific computed WAI-ARIA label. Use expect.oneOf() when the element can have different labels.
Usage
await browser.url('https://webdriver.io/')
const elem = await $('a.navbar__link.header-github-link')
await expect(elem).toHaveComputedLabel('GitHub repository')
await expect(elem).toHaveComputedLabel(expect.stringContaining('repository'))
Usage
await browser.url('https://webdriver.io/')
const elem = await $('a.navbar__link.header-github-link')
await expect(elem).toHaveComputedLabel(expect.oneOf('GitHub repository', 'Private repository'))
await expect(elem).toHaveComputedLabel(expect.oneOf(expect.stringContaining('GitHub'), expect.stringContaining('Private')))
toHaveComputedRole
Checks if element has a specific computed WAI-ARIA role. Use expect.oneOf() when the element can have different roles.
Usage
await browser.url('https://webdriver.io/')
const elem = await $('[aria-label="Skip to main content"]')
await expect(elem).toHaveComputedRole('region')
await expect(elem).toHaveComputedRole(expect.stringContaining('ion'))
Usage
await browser.url('https://webdriver.io/')
const elem = await $('[aria-label="Skip to main content"]')
await expect(elem).toHaveComputedRole(expect.oneOf('region', 'section'))
await expect(elem).toHaveComputedRole(expect.oneOf(expect.stringContaining('reg'), expect.stringContaining('sec')))
toHaveTagName
Checks if element has a specific tag name, the value of getTagName(). The value is compared as in toHaveText, with the string options. In an HTML page, the browser gives the tag name in lowercase, e.g. button; use ignoreCase for another case.
Usage
const elem = await $('#submit')
await expect(elem).toHaveTagName('button')
await expect(elem).toHaveTagName(expect.oneOf('button', 'input'))
await expect($$('nav > *')).toHaveTagName(expect.arrayContaining(['a']))
toHaveHref
Checks if link element has a specific link target.
Usage
const link = await $('#githubRepo')
await expect(link).toHaveHref('https://github.com')
await expect(link).toHaveHref(expect.stringContaining('github.com'))
toHaveLink
Same as toHaveHref.
Usage
const link = await $('#githubRepo')
await expect(link).toHaveLink('https://github.com')
await expect(link).toHaveLink(expect.stringContaining('github.com'))
toHaveId
Checks if element has a specific id attribute.
Usage
await expect($('#elem')).toHaveId('elem')
toHaveStyle
Checks if an element has specific CSS properties. By default, values must match exactly. Only the CSS properties you specify are validated; other properties on the element are ignored. Each value is compared as in toHaveText: the string options apply to each value (trim removes surrounding spaces from the actual value only and leaves the expected value unchanged), and a value can be a RegExp, an asymmetric matcher or expect.oneOf(). The failure message shows each CSS value as is, and the value that the matcher compared.
The actual value is the one of getCSSProperty(), which WebdriverIO normalizes: lowercase and trimmed, a color as rgba(r,g,b,a) with no spaces (not white, #fff or rgb(255, 255, 255)), and only the first font of font-family. A value without unit is a number, compared as its text: font-weight: '700', opacity: '0'. Write the expected value in this form, or use ignoreCase for the case. As in toHaveText, replace applies first, then ignoreCase and the position option.
On $$(), give one style for all the elements, or an array with one style for each element. A list matcher (expect.arrayContaining(), expect.arrayOf(), jasmine.arrayWithExactContents()) throws an error, also with .not: the matcher reads only the CSS properties that the expected value names, and a list matcher names none.
Usage
// The <h1> of a page: color `rgb(255, 255, 255)`, font-family `"Helvetica Neue", Helvetica, Arial`
await expect($('h1')).toHaveStyle({
'color': 'rgba(255,255,255,1)',
'font-family': 'helvetica neue',
'font-weight': '700',
'font-size': '16px',
})
await expect($('h1')).toHaveStyle({ 'font-family': 'Helvetica Neue' }, { ignoreCase: true })
await expect($('h1')).toHaveStyle({
'color': expect.oneOf('rgba(0,0,0,1)', 'rgba(255,255,255,1)'),
'font-size': /^1[0-9]px$/,
})
toHaveText
Checks if an element matches a specific text exactly. You can also pass an asymmetric matcher like expect.stringContaining() for partial matches, or use expect.oneOf() if the element can have different possible texts.
Note: An array of expected values is for multiple elements $$() only, one value per element. On a single element, it fails the assertion: use expect.oneOf().
Usage
await browser.url('https://webdriver.io/')
const elem = await $('.hero__subtitle')
// Exact match assertion
await expect(elem).toHaveText('Next-gen browser and mobile automation test framework for Node.js')
// Partial match assertion using stringContaining
await expect(elem).toHaveText(expect.stringContaining('test framework for Node.js'))
// Succeeds if one of the text options matches (v6.0.0+)
await expect(elem).toHaveText(expect.oneOf('Next-gen browser and mobile automation test framework for Node.js', 'Get Started'))
await expect(elem).toHaveText(expect.oneOf(expect.stringContaining('test framework for Node.js'), expect.stringContaining('Started')))
If you have a list of elements like the HTML structure below:
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
You can assert all of them at once using an array:
// Index-based: each element must match the value at its index
await expect($$('ul > li')).toHaveText(['Coffee', 'Tea', 'Milk'])
Use expect.arrayContaining() to check for a subset of texts in any order. Extra elements are allowed. See Matching a subset of element values for retry behavior, options, and other supported matchers.
await expect($$('ul > li')).toHaveText(expect.arrayContaining(['Tea', 'Coffee']))
await expect($$('ul > li')).toHaveText(expect.arrayContaining([expect.stringContaining('Coff')]))
await expect($$('ul > li')).not.toHaveText(expect.arrayContaining(['Juice']))
toHaveHTML
Checks if an element matches a specific text exactly. You can also pass an asymmetric matcher like expect.stringContaining() for partial matches, or use expect.oneOf() if the element can have different possible texts.
Note: An array of expected values is for multiple elements $$() only, one value per element. On a single element, it fails the assertion: use expect.oneOf().
Usage
await browser.url('https://webdriver.io/')
const elem = await $('.hero__subtitle')
await expect(elem).toHaveHTML('<p class="hero__subtitle">Next-gen browser and mobile automation test framework for Node.js</p>')
await expect(elem).toHaveHTML(expect.stringContaining('Next-gen browser and mobile automation test framework for Node.js'))
await expect(elem).toHaveHTML('Next-gen browser and mobile automation test framework for Node.js', { includeSelectorTag: false })
Usage
await browser.url('https://webdriver.io/')
const elem = await $('.hero__subtitle')
await expect(elem).toHaveHTML(expect.oneof('Next-gen browser and mobile automation test framework for Node.js', 'Get Started'), { includeSelectorTag: false })
await expect(elem).toHaveHTML(expect.oneof(expect.stringContaining('automation test framework for Node.js'), expect.stringContaining('Started')), { includeSelectorTag: false })
toBeDisplayedInViewport
Checks if an element is within the viewport by calling isDisplayedInViewport on the element.
Usage
await expect($('#elem')).toBeDisplayedInViewport()
toHaveChildren
Checks amount of the fetched element's children by calling element.$('./*') command.
Usage
const list = await $('#selectbox')
await expect(list).toHaveChildren() // the list has at least one option
// same as
await expect(list).toHaveChildren({ gte: 1 })
await expect(list).toHaveChildren(3) // the list has 3 options
// same as
await expect(list).toHaveChildren({ eq: 3 })
toHaveWidth
Checks if element has a specific width.
Usage
await browser.url('http://github.com')
const logo = await $('[aria-label="Homepage"] .octicon-mark-github')
await expect(logo).toHaveWidth(32)
// Same as
await expect(logo).toHaveWidth({ eq: 32 })
// Greater/Less than equals or in between
await expect(logo).toHaveWidth({ gte: 32 })
await expect(logo).toHaveWidth({ lte: 34 })
await expect(logo).toHaveWidth({ gte: 32, lte: 34 })
// One of these widths, and nothing between
await expect(logo).toHaveWidth(expect.oneOf(32, 64))
// A width that the browser rounds
await expect(logo).toHaveWidth(expect.closeTo(32.4, 0))
toHaveHeight
Checks if element has a specific height.
Usage
await browser.url('http://github.com')
const logo = await $('[aria-label="Homepage"] .octicon-mark-github')
await expect(logo).toHaveHeight(32)
// Same as
await expect(logo).toHaveHeight({ eq: 32 })
// Greater/Less than equals or in between
await expect(logo).toHaveHeight({ gte: 32 })
await expect(logo).toHaveHeight({ lte: 34 })
await expect(logo).toHaveHeight({ gte: 32, lte: 34 })
toHaveSize
Checks if element has a specific size, with deep equality. Each field is a number matcher value: a number, a NumberMatcher ({ gte: 30 }), expect.oneOf() with numbers, or an asymmetric matcher (expect.closeTo(150.4, 0)). The size can also be an asymmetric matcher, e.g. expect.objectContaining() to check one field only. An invalid field value throws, as in toHaveWidth, e.g. {}, gte greater than lte, a string, NaN or a list matcher. On $$(), a list matcher compares the list of the sizes, e.g. expect.arrayContaining([{ width: 100, height: 50 }]), as toHaveText compares the list of the texts; on one element, it throws. A range is converted only in the fields of the size, not inside an asymmetric matcher: for one field with a range, write { width: { gte: 30 }, height: expect.any(Number) }.
Usage
await browser.url('http://github.com')
const logo = await $('[aria-label="Homepage"] .octicon-mark-github')
await expect(logo).toHaveSize({ width: 32, height: 32 })
await expect(logo).toHaveSize({ width: { gte: 30, lte: 34 }, height: expect.oneOf(32, 64) })
await expect(logo).toHaveSize(expect.objectContaining({ width: 32 }))
toBeElementsArrayOfSize
Checks amount of fetched elements using $$ command.
Note: This matcher will update the passed array with the latest elements if the assertion passes. However, if you've reassigned the variable, you'll need to fetch the elements again.
Usage
const listItems = await $$('.box')
await expect(listItems).toBeElementsArrayOfSize(5) // 5 boxes
// Greater/Less then
await expect(listItems).toBeElementsArrayOfSize({ lte: 10 })
// same as
assert.ok(listItems.length <= 10)
await expect(listItems).toBeElementsArrayOfSize({ gte: 5 })
// In between
await expect(listItems).toBeElementsArrayOfSize({ gte: 5, lte: 5 })
With multi-remote, the size is checked per browser instance. Pass a single size that every instance must match, or one size per instance (every instance must be listed):
const listItems = await multiRemoteBrowser.$$('.box')
await expect(listItems).toBeElementsArrayOfSize(5) // 5 boxes in every browser
await expect(listItems).toBeElementsArrayOfSize(expect.multiRemote({ chrome: 5, firefox: { gte: 3 } }))
Multiple Elements Support
All element matchers support arrays of elements returned from $$():
- Standard Behavior: Every element must pass. One failure fails the assertion.
- Using
.not: Every element must not meet the matcher condition. One match fails the assertion. - Empty Arrays: Empty element arrays will fail the assertion by default.
- Note: Only the
toExist,toBeExisting, andtoBePresentmatchers succeed when using.noton an empty element array.
- Note: Only the
- Retry / Array Refresh: On failure or stale references, the element array is automatically re-fetched until the matcher passes or times out.
- Multi-remote: The same rules apply per browser instance, each on its own elements, see Multi-remote Support.
- See MultipleElements.md for more details.
Usage
toBe matchers
// Elements awaited selector syntax
const elements = await $$('#someElements')
await expect(elements).toBeDisplayed()
// Elements non-awaited
await expect($$('#someElements')).toBeDisplayed()
// Works with filtered elemnts array
await expect($$('#someElements').filter((t) => t.isExisting())).toBeDisplayed()
// Using the .not modifier (Asserts NO elements are displayed)
await expect($$('#someElements')).not.toBeDisplayed()
toHave matchers
const elements = await $$('#someElem')
// Single value: checked against every element
await expect(elements).toHaveAttribute('class', 'form-control')
// One of: checked against every element and succeed if one of the expected text matches
await expect(elements).toHaveAttribute('class', expect.oneOf('form-control1', 'form-control2'))
// Array: each value checked at corresponding element index (must match length)
await expect(elements).toHaveAttribute('class', ['control1', 'control2'])
// Use asymmetric matchers for flexible matching
await expect(elements).toHaveAttribute('class', [expect.stringContaining('control1'), 'control2'])
// Use RegEx `i` for case insensitive
await expect(elements).toHaveAttribute('class', [/Control1/i, 'control2'])
// Use RegEx with `|` to mimic contains
await expect(elements).toHaveAttribute('class', /Control1|Control2/)
await expect(elements).toHaveAttribute('class', [/Control1|Control2/, 'control3'])
// Elements non-awaited
await expect($$('#someElem')).toHaveAttribute('class', 'form-control')
// Works with filtered elements array too
await expect($$('#someElem').filter(el => el.isExisting())).toHaveAttribute('class', ['control1', 'control2'])
Network Matchers
Network matchers also support the mocks of a multi-remote mock(), with one expected value for every browser instance or, with expect.multiRemote(), one per instance, see Multi-remote Support.
toBeRequested
Checks that mock was called
Usage
const mock = browser.mock('**/api/todo*')
await expect(mock).toBeRequested()
toBeRequestedTimes
Checks that mock was called for the expected amount of times
Usage
const mock = browser.mock('**/api/todo*')
await expect(mock).toBeRequestedTimes(2)
// same as
await expect(mock).toBeRequestedTimes({ eq: 2 })
// request called at least 5 times but less than 11
await expect(mock).toBeRequestedTimes({ gte: 5, lte: 10 })
toBeRequestedWith
Checks that mock was called according to the expected options.
Most of the options supports expect/jasmine partial matchers like expect.objectContaining
Usage
const mock = browser.mock('**/api/todo*', { method: 'POST' })
await expect(mock).toBeRequestedWith({
url: 'http://localhost:8080/api/todo', // [optional] string | function | custom matcher
method: 'POST', // [optional] string | array
statusCode: 200, // [optional] number | array
requestHeaders: { Authorization: 'foo' }, // [optional] object | function | custom matcher
responseHeaders: { Authorization: 'bar' }, // [optional] object | function | custom matcher
postData: { title: 'foo', description: 'bar' }, // [optional] object | function | custom matcher
response: { success: true }, // [optional] object | function | custom matcher
})
await expect(mock).toBeRequestedWith({
url: expect.stringMatching(/.*\/api\/.*/i),
method: ['POST', 'PUT'], // either POST or PUT
statusCode: [401, 403], // either 401 or 403
requestHeaders: headers => headers.Authorization.startsWith('Bearer '),
postData: expect.objectContaining({ released: true, title: expect.stringContaining('foobar') }),
response: r => Array.isArray(r) && r.data.items.length === 20
})
Note on
postData/responsetiming: unlike the other options, the request/response body is collected asynchronously (an extra round-trip after the request/response headers are already available), so it may not be attached to a call yet at the very first check. Regular assertions retry within theirwaittimeout and pick it up once it arrives..not.toBeRequestedWith({ postData / response })also retries withinwait- but only while there's a call that already matches every other criterion and is just waiting on its body to attach; if nothing matches at all (wrong URL, no call made, etc.) it still resolves immediately, same as any other.notassertion. The one residual case this can't close: if the body genuinely takes longer to arrive than your configuredwait, a.notassertion can still report a false pass. If you rely on.notwithpostData/responseand see intermittent false passes, increasewait(or await a signal that the request has fully completed) before asserting.
Snapshot Matcher
WebdriverIO supports basic snapshot tests as well as DOM snapshot testing.
The snapshot matchers need the snapshot service: it knows the current test, and it writes the snapshot files. The WebdriverIO testrunner runs it by itself. Without the testrunner, for example in a Node.js script with remote(), the matchers throw Snapshot service is not initialized. Then call the hooks of the service yourself: beforeTest() before each test, with the file of the snapshots and the name of the test, and after() after the tests of the file, to write the snapshot file.
import { fileURLToPath } from 'node:url'
import { remote } from 'webdriverio'
import { expect, SnapshotService } from 'expect-webdriverio'
const snapshotService = SnapshotService.initiate()
const browser = await remote({ capabilities: { browserName: 'chrome' } })
try {
// The snapshots go to `__snapshots__/<file name>.snap`, next to the file, under the name `My page > has a button 1`
await snapshotService.beforeTest({ file: fileURLToPath(import.meta.url), parent: 'My page', title: 'has a button' })
await browser.url('https://example.com')
await expect(browser.$('h1')).toMatchSnapshot()
} finally {
// Also when an assertion fails: write the snapshot file, then close the browser
try {
await snapshotService.after()
} finally {
await browser.deleteSession()
}
}
SnapshotService.initiate() takes the options of the service, e.g. { updateState: 'all' } to update the snapshots, or resolveSnapshotPath. With Jest or Mocha, call beforeTest() in beforeEach, and after() in afterAll (or after).
toMatchSnapshot
Checks if any arbitrary object matches a certain value. If you pass in an WebdriverIO.Element it will automatically snapshot the outerHTML state of it.
Usage
// snapshot arbitrary objects (no "await" needed here)
expect({ foo: 'bar' }).toMatchSnapshot()
// snapshot `outerHTML` of WebdriverIO.Element (DOM snapshot, requires "await")
await expect($('elem')).toMatchSnapshot()
// snapshot `outerHTML` of multi-remote elements, $() or $$(): shared by every instance, else keyed by instance name (requires "await")
await expect(multiRemoteBrowser.$('elem')).toMatchSnapshot()
await expect(multiRemoteBrowser.$$('.box')).toMatchSnapshot()
// snapshot `outerHTML` of every element of $$() as an array (requires "await")
await expect($$('.box')).toMatchSnapshot()
// snapshot result of element command
await expect($('elem').getCSSProperty('background-color')).toMatchSnapshot()
toMatchInlineSnapshot
Similarly, you can use the toMatchInlineSnapshot() to store the snapshot inline within the test file. For example, given:
await expect($('#secondPageLink')).toMatchInlineSnapshot()
Instead of creating a snapshot file, WebdriverIO will modify the test file directly to update the snapshot as a string:
await expect($('#secondPageLink')).toMatchInlineSnapshot(`"<a href="./two.html" id="secondPageLink">two</a>"`)
With $$(), the snapshot is the outerHTML of every element as an array:
await expect($$('header a')).toMatchInlineSnapshot(`
[
"<a href="pointer.html">3</a>",
"<a href="gestureTest.html">2</a>",
"<a href="index.html">1</a>",
]
`)
Visual Snapshot Matchers
The following matcher are implemented as part of the @wdio/visual-service plugin and only available when the service is set up. Make sure you follow the set-up instructions accordingly.
toMatchElementSnapshot
Checks that if given element matches with snapshot of baseline.
Usage
await expect($('#purplebox')).toMatchElementSnapshot('purpleBox', 0, {
// options
})
The expected result is by default 0, so you can write the same assertion as:
await expect($('#purplebox')).toMatchElementSnapshot('purpleBox', {
// options
})
or not pass in any options at all:
await expect($('#purplebox')).toMatchElementSnapshot()
toMatchScreenSnapshot
Checks that if current screen matches with snapshot of baseline.
Usage
await expect(browser).toMatchScreenSnapshot('partialPage', 0, {
// options
})
The expected result is by default 0, so you can write the same assertion as:
await expect(browser).toMatchScreenSnapshot('partialPage', {
// options
})
or not pass in any options at all:
await expect(browser).toMatchScreenSnapshot('partialPage')
toMatchFullPageSnapshot
Checks that if the full page screenshot matches with snapshot of baseline.
Usage
await expect(browser).toMatchFullPageSnapshot('fullPage', 0, {
// options
})
The expected result is by default 0, so you can write the same assertion as:
await expect(browser).toMatchFullPageSnapshot('fullPage', {
// options
})
or not pass in any options at all:
await expect(browser).toMatchFullPageSnapshot('fullPage')
toMatchTabbablePageSnapshot
Checks that if the full page screenshot including tab marks matches with snapshot of baseline.
Usage
await expect(browser).toMatchTabbablePageSnapshot('tabbable', 0, {
// options
})
The expected result is by default 0, so you can write the same assertion as:
await expect(browser).toMatchTabbablePageSnapshot('tabbable', {
// options
})
or not pass in any options at all:
await expect(browser).toMatchTabbablePageSnapshot('tabbable')
Using regular expressions
You can also directly use regular expressions for all matchers that do text comparison.
Usage
await browser.url('https://webdriver.io/')
const elem = await $('.hero__subtitle')
await expect(elem).toHaveText(/node\.js/i)
await expect(elem).toHaveText(expect.oneOf(/node\.js/i, 'Get Started'))
await expect(browser).toHaveTitle(/webdriverio/i)
await expect(browser).toHaveUrl(/webdriver\.io/)
await expect(elem).toHaveElementClass(/Hero__Subtitle/i)
Default Matchers
In addition to the WebdriverIO matchers, expect-webdriverio also provides basic matchers from Jest's expect library.
// Equality
expect(2 + 2).toBe(4);
expect({a: 1}).toEqual({a: 1});
expect([1, 2, 3]).toStrictEqual([1, 2, 3]);
expect(2 + 2).not.toBe(5);
// Truthiness
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(0).toBeFalsy();
expect(1).toBeTruthy();
expect(NaN).toBeNaN();
// Numbers
expect(4).toBeGreaterThan(3);
expect(4).toBeGreaterThanOrEqual(4);
expect(4).toBeLessThan(5);
expect(4).toBeLessThanOrEqual(4);
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
// Strings
expect('team').toMatch(/team/);
expect('Christoph').toContain('stop');
// Arrays and iterables
expect([1, 2, 3]).toContain(2);
expect([{a: 1}, {b: 2}]).toContainEqual({a: 1});
expect([1, 2, 3]).toHaveLength(3);
// Objects
expect({a: 1, b: 2}).toHaveProperty('a');
expect({a: {b: 2}}).toHaveProperty('a.b', 2);
// Errors
expect(() => { throw new Error('error!') }).toThrow('error!');
expect(() => { throw new TypeError('wrong type') }).toThrow(TypeError);
// Asymmetric matchers
expect({foo: 'bar', baz: 1}).toEqual(expect.objectContaining({foo: expect.any(String)}));
expect([1, 2, 3]).toEqual(expect.arrayContaining([2]));
expect('abc').toEqual(expect.stringContaining('b'));
expect('abc').toEqual(expect.stringMatching(/b/));
expect(123).toEqual(expect.any(Number));
// Others
expect(new Set([1, 2, 3])).toContain(2);
// .resolves / .rejects (async)
await expect(Promise.resolve(42)).resolves.toBe(42);
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
Jasmine
For Jasmine, see the official documentation for expect/expectAsync, matchers, and async-matchers.
Note:
- With the global
expectof@wdio/jasmine-framework, the Jasmine synchronous matchers stay synchronous. The WebdriverIO matchers and the Jasmine async matchers return a promise. See Jasmine. - Default matchers are still available if you import
expectdirectly fromexpect-webdriverioinstead of using the global.
Modifiers
.not
WebdriverIO supports usage of modifiers as .not and it will wait until the reverse condition is meet
// Wait until the element is no longer present
await expect($('element')).not.toBeDisplayed()
// Wait until the text is no more 'some title'
await expect(browser).not.toHaveTitle('some title')
In case immediate assertion is required, use { wait: 0 }
// Ensure element is not present right now
await expect($('element')).not.toBeDisplayed({ wait: 0 })
// Ensure the text is not 'some title' right now
await expect(browser).not.toHaveTitle('some title', { wait: 0 })
Note: You can pair .not with asymmetric matchers, but to enable the wait-until behavior, .not must be used directly on the expect() call.
some()
since: v6.0.0
The custom some() function (distinct syntax compared to .not) allows you to assert that at least one element from multiple elements meets the assertion.
import { some } from 'expect-webdriverio/api'
// Allow to succeed only if one element matches (or not match)
// Succeeds if at least one element matches either 'optionA' or 'optionB'
await expect(some($$('elements'))).toHaveText(expect.oneOf('optionA', 'optionB'));
// Succeeds if at least one element does not matches either 'forbiddenTextA' or 'forbiddenTextB'
await expect(some($$('elements'))).not.toHaveText(/forbiddenTextA|forbiddenTextB/);
// Succeeds if the first element matches 'valueForIndex0' OR the second matches 'valueForIndex1'
await expect(some($$('elements'))).toHaveText(['valueForIndex0', 'valueForIndex1']);
A list matcher (expect.arrayContaining(), expect.arrayOf(), jasmine.arrayWithExactContents()) throws an error with some(), also with .not, and also in an array of expected values or in the values of expect.multiRemote(): some() checks each element alone, and the value of one element is not a list. Use the list matcher without some(): on $$(), it compares the values of all the elements, so expect.arrayContaining(['Home']) passes when one element has the value Home. Only toHaveElementProperty compares a list matcher with the property of each element, because a property value can be an array.
Note: With multi-remote, some() requires at least one matching element in every browser instance.
Asymmetric Matchers
WebdriverIO supports usage of asymmetric matchers wherever you compare text values, e.g.:
await expect(browser).toHaveTitle(expect.stringContaining('some title'))
or
await expect(browser).toHaveTitle(expect.not.stringContaining('some title'))
expect.multiRemote()
With multi-remote, passes one expected value per browser instance, keyed by instance name. Every instance must be listed.
await expect(multiRemoteBrowser).toHaveTitle(expect.multiRemote({ chrome: 'WebdriverJS Testpage', firefox: expect.stringContaining('Testpage') }))
await expect(multiRemoteBrowser.$('header h1')).toHaveStyle(expect.multiRemote({ chrome: { color: 'red' }, firefox: { color: 'blue' } }))
See Expected Values for the plain object shorthand.
Jasmine
Under @wdio/jasmine-framework, some Jasmine asymmetric matchers now work with WebdriverIO matchers and the global import.
// Jasmine's stringContaining works just like the one from expect
await expect(browser).toHaveTitle(jasmine.stringContaining('some title'))
await expect(browser).toHaveTitle(expect.stringContaining('some title'))
// Jasmine's stringMatching works
await expect(browser).toHaveUrl(jasmine.stringMatching('/WebdriverIO/'))
// Jasmine's any & anything works
await expect(browser).toHaveUrl(jasmine.any(String))
await expect(browser).toHaveUrl(jasmine.anything())
// Jasmine's objectContaining works with Network Matchers
await expect(mock).toBeRequestedWith({
method: 'POST',
requestHeaders: expect.objectContaining({
Authorization: 'foo'
}),
})
// Jasmine's any & anything works with Network Matchers
await expect(mock).toBeRequestedWith({
method: 'POST',
url: jasmine.any(String),
requestHeaders: jasmine.anything(),
})
jasmine.arrayContaining() and jasmine.arrayWithExactContents() are also supported for element collection values, including nested Jasmine matchers. Limitations with Jasmine collection and object matchers may still apply in other assertion contexts.
Types Definition
TypeScript
If you are using the WDIO Testrunner everything will be automatically setup. Just follow the setup guide from the docs. However if you run WebdriverIO with a different testrunner or in a simple Node.js script you will need to add expect-webdriverio to types in the tsconfig.json.
"expect-webdriverio"for everyone except Jasmine/Jest users."expect-webdriverio/jasmine"for Jasmine without@wdio/jasmine-framework"expect-webdriverio/jest"for Jest"expect-webdriverio/expect-global"// Optional, if you wish to use expect ofexpect-webdriverioglobally without explicit import
JavaScript (VSCode)
It's required to create jsconfig.json in project root and refer to the type definitions to make autocompletion work in vanilla js.
{
"include": [
"**/*.js",
"**/*.json",
"node_modules/expect-webdriverio"
]
}
Jasmine
- With
@wdio/jasmine-framework: add"@wdio/jasmine-framework"totypes, after"@wdio/globals/types". It types its hybrid globalexpect: the Jasmine matchers stay synchronous, and the WebdriverIO matchers return a promise. Do not add"expect-webdriverio/jasmine". - With Jasmine alone: add
"expect-webdriverio/jasmine". It types the WebdriverIO matchers onexpectAsync, which you register yourself.
See Framework.md for the details.
Custom matchers
See TypeScript in Custom Matchers to type your own matchers.
Framework Integration
expect-webdriverio extends Jest's expect API with WebDriverIO-specific enhancements. It can be used standalone or within other testing environments.
Compatibility
It is highly recommended to use this package with the WDIO Testrunner and a compatible framework adapter, which together provide a plug-and-play experience.
Pair it with your preferred framework using the appropriate adapter:
- Mocha: Use
@wdio/mocha-framework - Cucumber: Use
@wdio/cucumber-framework - Jasmine: Use
@wdio/jasmine-framework - Jest: Works (no framework exists; requires manual configuration—see note below)
Note: When using Jest, or when running outside of the WDIO Testrunner without a compatible framework adapter, additional manual configuration may be required, such as adding types to your
tsconfig.jsonand configuring WDIO matchers, soft assertions, and the snapshot service.
Playgrounds
Example playgrounds are available, though their tsconfig.json files may use modified configurations for development purposes.
- See Mocha, Jasmine, and Jest examples here.
Mocha
When pairing with Mocha, you can use expect-webdriverio directly or combine it with chai (or any other assertion library).
- It is strongly recommended to leverage
@wdio/mocha-frameworkfor automatic configuration and a plug-and-play experience. - See Mocha playground example here
Standalone
No import is required; everything is set globally.
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await expect(browser).toHaveUrl('https://example.com')
})
})
Minimum types expected in tsconfig.json:
When depending on @wdio/mocha-framework
{
"compilerOptions": {
"types": [
"@wdio/mocha-framework",
"@wdio/globals/types"
]
}
}
When not depending on @wdio/mocha-framework
{
"compilerOptions": {
"types": [
"@types/mocha",
"expect-webdriverio/expect-global"
]
}
}
Chai
expect-webdriverio can coexist with the Chai assertion library by importing both libraries explicitly.
See also this documentation.
Jest
You can use expect-webdriverio with Jest by leveraging either @types/jest (which provides global ambient support) or @jest/globals alone.
- Note: Jest maintainers do not officially support
@types/jest. Should this package become outdated or experience issues, support may be dropped. - Note: With Jest, the matchers
toMatchSnapshotandtoMatchInlineSnapshotare overloaded. To resolve the types correctly,expect-webdriverio/jestmust be listed last. - Note: WebdriverIO does not provide a compatible framework adapter for Jest; manual configuration is required.
With @types/jest
When paired with @types/jest, no imports are required in your test files. Global ambient types are already defined correctly, allowing you to use Jest's expect directly after some manual configuration.
- Note:
jestandts-jestare also required. - See the Jest with
@types/jestplayground example.
Since no WDIO Testrunner and framework adapter are used, additional prerequisite configuration is required.
Option 1: Replace the global expect with the expect-webdriverio instance:
import { expect } from "expect-webdriverio";
(globalThis as any).expect = expect;
Option 2: Extend Jest's global expect with custom matchers and soft assertions:
If not already set, define a file path for setupFilesAfterEnv in your Jest configuration:
setupFilesAfterEnv: ['./jest.setup.after-env.ts'],
Then, add the following configuration to your jest.setup.after-env.ts file:
import { expect } from "@jest/globals";
import { wdioCustomMatchers } from "expect-webdriverio";
beforeAll(async () => {
// Extend the imported Jest expect instance with WDIO matchers
expect.extend(wdioCustomMatchers);
});
Optional: For soft assertions, copy them from the expect-webdriverio instance:
import { expect } from "@jest/globals";
import { expect as wdioExpect } from "expect-webdriverio";
beforeAll(async () => {
for (const name of ["soft", "getSoftFailures", "assertSoftFailures", "clearSoftFailures"] as const) {
Object.defineProperty(expect, name, { value: wdioExpect[name] });
}
});
Then, as shown below, no imports are required and we can use WDIO matchers directly on Jest's expect:
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await expect(browser).toHaveUrl('https://example.com')
})
})
Expected in tsconfig.json:
{
"compilerOptions": {
"types": [
"@types/jest",
"expect-webdriverio/jest" // Must be last for overloaded matchers `toMatchSnapshot` and `toMatchInlineSnapshot`
]
}
}
With Only @jest/globals
When using @jest/globals directly instead of global ambient types, you explicitly import Jest's utilities. To use expect-webdriverio you have two approaches:
- Note: No example playground available
Option 1: Explicit Imports
import { expect } from 'expect-webdriverio'
import { describe, it, expect as jestExpect } from '@jest/globals'
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await expect(browser).toHaveUrl('https://example.com')
// Jest's native expect is still usable
jestExpect(myFunction).toHaveBeenCalled()
})
})
No types are expected in tsconfig.json.
Option 2: Global Type Definition
To avoid explicitly importing expect from expect-webdriverio in every test file, add the global entry point to your tsconfig.json:
{
"compilerOptions": {
"types": ["expect-webdriverio/expect-global"]
}
}
Augmenting @jest/globals JestMatchers
Unlike @types/jest, @jest/globals does not export a global namespace that can be easily extended. While module augmentation is possible, it does not support inheriting matchers via the extends keyword. Supporting it would require manually duplicating all expect-webdriverio matcher interfaces inside the module declaration.
This limitation is a known upstream issue tracked in Jest.
Matching a list of elements
On $$(), the expected value decides how the texts (or other values) of the elements are compared. These are the matchers of the expect package, so they are the same with Mocha. Results with toHaveText on $$('li'):
| Expected value | Passes when | Order | Other elements | String options (trim, ignoreCase…) |
|---|---|---|---|---|
['Coffee', 'Tea'] | each element has the text at its index | fixed | no: same count | applied |
'Tea' | every element has the text | - | no | applied |
expect.oneOf('Tea', 'Coffee') | each element has one of the texts; a text can be missing | any | no | applied |
expect.arrayContaining(['Tea', 'Coffee']) | each text is on at least one element | any | allowed | not applied |
expect.not.arrayContaining(['Milk']) | at least one of the texts is on no element | any | allowed | not applied |
expect.arrayOf(expect.stringMatching(/^(Tea|Coffee)$/)) | every element matches the matcher; also passes with no element | any | no | not applied |
some($$('li')) with 'Tea' | at least one element has the text | any | allowed | applied |
A list matcher (arrayContaining, arrayOf) compares the raw values of all the elements at once. The string options do not change these values: use a nested matcher, e.g. expect.stringMatching(/tea/i). A list matcher throws an error with some(), which checks each element alone.
// <li>Coffee</li> <li>Tea</li>
await expect($$('li')).toHaveText(['Coffee', 'Tea']) // passes
await expect($$('li')).toHaveText(['Tea', 'Coffee']) // fails: the order is different
await expect($$('li')).toHaveText(expect.oneOf('Tea', 'Coffee')) // passes
await expect($$('li')).toHaveText(expect.arrayContaining(['Tea'])) // passes: Coffee is allowed
await expect($$('li')).toHaveText(expect.not.arrayContaining(['Milk'])) // passes
await expect($$('li')).toHaveText(expect.arrayOf(expect.stringMatching(/^(Tea|Coffee)$/))) // passes
await expect(some($$('li'))).toHaveText('Tea') // passes
// <li>Tea</li> <li>Tea</li>
await expect($$('li')).toHaveText(expect.arrayContaining(['Tea', 'Coffee'])) // fails: Coffee is missing
// <li> Coffee </li> <li> Tea </li>
await expect($$('li')).toHaveText(['Coffee', 'Tea']) // passes: the default trim applies
await expect($$('li')).toHaveText(expect.arrayContaining(['Coffee'])) // fails: the value is " Coffee "
Jasmine
When paired with Jasmine, @wdio/jasmine-framework is required to ensure proper runtime configuration. The adapter registers the WDIO matchers with addAsyncMatchers and sets a hybrid global expect:
- Jasmine synchronous matchers (
toBe,toEqual, ...) stay synchronous and returnvoid. - WDIO matchers and Jasmine async matchers (
toBeResolved, ...) go toexpectAsyncand return a promise: alwaysawaitthem.
Jasmine collects the failures until the spec ends (built-in soft assertions), so the expect-webdriverio SoftAssertion service is neither required nor supported.
Global expect with @wdio/jasmine-framework
@wdio/jasmine-framework v10 has the types of its hybrid expect.
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await expect(browser).toHaveUrl('https://example.com')
// Jasmine synchronous matcher
expect(true).toBe(true)
})
})
Expected in tsconfig.json:
{
"compilerOptions": {
"types": [
"@types/jasmine",
"@wdio/globals/types",
"@wdio/jasmine-framework"
]
}
}
expectAsync also has the WDIO matchers, e.g. await expectAsync(browser).toHaveUrl('https://example.com'), typed since @wdio/jasmine-framework 10.0.2.
Since @wdio/jasmine-framework 10.0.2, expect.extend() adds custom matchers to the global expect. Call it before the tests run: at the top level of a spec file, or in the before hook of the configuration, not in a beforeAll(), because the framework reads the matchers before the first beforeAll() of the specs. The matchers become Jasmine async matchers, so await them. The types of the global expect have extend() since @wdio/jasmine-framework 10.1.0 (webdriverio#15982). With 10.0.2, call it on import { expect } from '@wdio/globals', which has the type and adds the matchers to the same global expect. See Custom Matchers and the playground spec.
Note: When using Jasmine, Jest's expect matchers are not leveraged, meaning standard Jest-specific assertion matchers are unavailable.
Jasmine without @wdio/jasmine-framework
With Jasmine 6 alone, register the WDIO matchers on expectAsync yourself, and add expect-webdriverio/jasmine to types. Do not use expect-webdriverio/jasmine with @wdio/jasmine-framework, which has its own types.
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await expectAsync(browser).toHaveUrl('https://example.com')
// Standard Jasmine async matchers work as expected
await expectAsync(Promise.resolve(true)).toBeResolvedTo(true)
})
})
Expected in tsconfig.json:
{
"compilerOptions": {
"types": [
"@types/jasmine",
"expect-webdriverio/jasmine"
]
}
}
Use expect from expect-webdriverio
The expect export from expect-webdriverio remains available under Jasmine if you prefer an explicit import strategy. See the playground example.
import { expect as wdioExpect } from 'expect-webdriverio'
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
await wdioExpect(browser).toHaveUrl('https://example.com')
// Does not require await
wdioExpect(true).toBe(true)
})
})
Since
@wdio/jasmine-framework10.0.2, the types of thisexpectkeep the Jest matchers, as at runtime.
Asymmetric matchers
jasmine.any(Type),jasmine.anything(),jasmine.is(),jasmine.stringContaining(),jasmine.stringMatching(),jasmine.truthy(),jasmine.falsy(),jasmine.empty()andjasmine.notEmpty()work in the element matchers, also in an array of expected values on$$().- On
$$(),jasmine.arrayContaining()andjasmine.arrayWithExactContents()compare the values of all the elements at once, see Matching a subset of element values. - Network matchers support
jasmine.objectContaining(). - WDIO asymmetric matchers also work properly.
import { expect as wdioExpect } from 'expect-webdriverio'
describe('My tests', async () => {
it('should verify my browser to have the expected url', async () => {
// Working Jasmine asymmetric matchers
await expectAsync(browser).toHaveUrl(jasmine.stringContaining('WebdriverIO'))
await expectAsync(browser).toHaveUrl(jasmine.stringMatching('/WebdriverIO/'))
await expectAsync(browser).toHaveUrl(jasmine.any(String))
await expectAsync(browser).toHaveUrl(jasmine.anything())
// Working WDIO asymmetric matcher
await expectAsync(browser).toHaveUrl(wdioExpect.stringContaining('WebdriverIO'))
})
})
Matching a list of elements
On $$(), Jasmine's list matchers compare the values of all the elements at once. Results with toHaveText on $$('li'):
| Expected value | Passes when | Order | Other elements | String options (trim, ignoreCase…) |
|---|---|---|---|---|
['Coffee', 'Tea'] | each element has the text at its index | fixed | no: same count | applied |
jasmine.arrayContaining(['Tea', 'Coffee']) | each text is on at least one element | any | allowed | not applied |
jasmine.arrayWithExactContents(['Tea', 'Coffee']) | same count, and each text is on at least one element | any | no | not applied |
jasmine.arrayWithExactContents() does not count a text that it has 2 times: ['Tea', 'Tea'] passes on Coffee, Tea, because the count is the same and Tea is in the list. This is how Jasmine compares. For an exact list with repeated texts, use an array in the order of the elements.
The other list matchers of Jest, e.g. expect.arrayOf(), are on the expect of expect-webdriverio.
// <li>Coffee</li> <li>Tea</li>
await expect($$('li')).toHaveText(['Coffee', 'Tea']) // passes
await expect($$('li')).toHaveText(['Tea', 'Coffee']) // fails: the order is different
await expect($$('li')).toHaveText(jasmine.arrayWithExactContents(['Tea', 'Coffee'])) // passes: any order
await expect($$('li')).toHaveText(jasmine.arrayContaining(['Tea'])) // passes: Coffee is allowed
await expect($$('li')).toHaveText(jasmine.arrayWithExactContents(['Tea'])) // fails: Coffee is not expected
await expect($$('li')).toHaveText(jasmine.arrayWithExactContents(['Tea', 'Tea'])) // passes: see above
// <li> Coffee </li> <li> Tea </li>
await expect($$('li')).toHaveText(jasmine.arrayWithExactContents(['Tea', 'Coffee'])) // fails: the values are " Coffee " and " Tea "
Cucumber
More details to come. In short, when paired with @wdio/cucumber-framework, you can use WebDriverIO's expect library seamlessly within your Cucumber step definitions and Gherkin-based tests.
Browser Runner
Browser Runner (@wdio/browser-runner) allows you to leverage component frameworks like React, Preact, Vue.js, Svelte, and SolidJS. It relies on the Jest's standard expect library and only registers core expect-webdriverio matchers. Because it runs within a browser environment rather than fully embedding expect-webdriverio, global features like DefaultOption and SoftAssertion are currently unsupported.
OneOf & Some
The asymmetric matcher expect.oneOf and the modifier some have been adapted to work in Browser Runner:
expect.oneOf: Works standard out of the box.expect.some: Reimplemented to integrate with Jest's native expect runner and exposed directly viaexpect.some. Note: Importingsomefromexpect-webdriverio/apiwill not work in Browser Runner mode. Always access it throughexpect.some
it('some elements are displayed', async () => {
// `expect.some` uses global export. Ignore TypeScript errors on `expect.some` until proper typings are provided.
await expect(expect.some($$('p=Times clicked: 1'))).toBeDisplayed()
})
Multiple Elements & Multi-remote
$$()is supported, awaited or not, and filtered (Element[]), including by the snapshot matchers.- Multi-remote is not supported: the Browser Runner runs tests in a single browser session, and stops with
No "browserName" defined in capability objectwhen given multi-remote capabilities. Use a Node.js runner framework instead, such as Mocha, Jasmine or Cucumber.
Matcher names for adapters and tools
A framework adapter, a lint rule or a runner that must know the matchers before expect exists can import their names from expect-webdriverio/api, instead of a copy of the list:
import { wdioCustomMatcherNames, asymmetricMatcherNames, inverseAsymmetricMatcherNames } from 'expect-webdriverio/api'
wdioCustomMatcherNames // ['toBeChecked', ..., 'toMatchSnapshot']: the names of `wdioCustomMatchers`, not the matchers of the `expect` package
asymmetricMatcherNames // ['any', ..., 'oneOf', 'multiRemote']: the asymmetric matchers of the `expect` package and of expect-webdriverio
inverseAsymmetricMatcherNames // ['arrayContaining', ..., 'stringMatching']: the same, on `expect.not`
Each matcher returns a promise, except toMatchSnapshot and toMatchInlineSnapshot when the received value is not an element, a list of elements or a promise. The lists do not have the matchers that you add with expect.extend(), or the some modifier.
expect.extend() also adds each matcher to expect and expect.not as an asymmetric matcher, and the asymmetric lists do not have these. The asymmetric form of the matchers of wdioCustomMatcherNames does not work, so do not add them to the asymmetric lists: the matchers are async (expect.toHaveTitle('x').asymmetricMatch(browser) gives undefined), except toMatchSnapshot and toMatchInlineSnapshot, which compare or write a snapshot of the current test, not an equality. A sync matcher that you add with expect.extend() works as an asymmetric matcher.
Custom Matchers
expect-webdriverio registers WebdriverIO custom matchers out of the box for a seamless experience.
To use WebdriverIO custom matchers (except asymmetric matchers) directly in:
- Jest: Register matchers manually with
expect.extend. - Jasmine: Using
@wdio/jasmine-frameworkprovides an out-of-the-box experience.- Else, register matchers manually with
jasmine.addAsyncMatchers, then they will be available onexpectAsync.
- Else, register matchers manually with
- Types: Type augmentation for custom matchers is provided. See Types.md for details.
Adding your own matchers
Similar to how expect-webdriverio provide custom matchers it's possible to add your own custom matchers.
- Jasmine see custom matchers doc
- Everyone else see Jest's expect.extend
Custom matchers should be added in wdio before hook
// wdio.conf.js
{
async before () {
const { addCustomMatchers } = await import('./myMatchers')
addCustomMatchers()
}
}
// myMatchers.js - Jest example
export function addCustomMatchers () {
expect.extend({
myMatcher (actual, expected) {
return { pass: actual === expected, message: () => 'some message' }
}
})
}
TypeScript
Add the types of your matchers to the ExpectWebdriverIO namespace. It works with every framework: the types of expect-webdriverio/jest and of expect-webdriverio/jasmine (and @wdio/jasmine-framework) extend it, also for expect.soft().
// my-matchers.d.ts, or any .ts file of your project
declare global {
namespace ExpectWebdriverIO {
interface Matchers<R, T> {
// A matcher for any value
toBeWithinRange(floor: number, ceiling: number): R
// An async matcher for elements only: `never` blocks it on other values
toHaveDataState: T extends ChainablePromiseElement | WebdriverIO.Element
? (state: string | ExpectWebdriverIO.PartialMatcher<string>, options?: ExpectWebdriverIO.CommandOptions) => Promise<R>
: never
}
interface AsymmetricMatchers {
// The asymmetric form: `expect.toBeWithinRange(1, 10)`
toBeWithinRange(floor: number, ceiling: number): ExpectWebdriverIO.PartialMatcher<number>
}
}
}
export {}
Matcherstakes both type parameters,<R, T>, as inexpect-webdriverio:Ris the return type of the assertion, andTis the type of the value given toexpect(). WithMatchers<R>only, TypeScript fails:All declarations of 'Matchers' must have identical type parameters.- An async matcher, which waits for the element, returns
Promise<R>. A matcher that compares a value at once returnsR. ExpectWebdriverIO.PartialMatchertakes the type of the value, e.g.PartialMatcher<string>.- In a file with an
importor anexport(a module), put the namespace indeclare global { … }, and end withexport {}when the file has no other export. In a.d.tsfile with noimportand noexport, writedeclare namespace ExpectWebdriverIO { … }directly. - Use the public types:
ExpectWebdriverIO.*(also as named exports ofexpect-webdriverio),WebdriverIO.Element, and the 2 global namesChainablePromiseElementandChainablePromiseArray(the types of a not-awaited$()and$$(), fromwebdriverio). The other helper types ofexpect-webdriverioare not global. - With Jest, you can also add the types to the
expectmodule (declare module 'expect' { interface Matchers<R, T> { … } }), or to thejestnamespace. Thejestnamespace does not typeexpect.soft().
Examples
describe('suite', () => {
before(async () => {
await browser.url('https://github.com/webdriverio/expect-webdriverio')
await expect(browser).toHaveUrl('https://github.com/webdriverio/expect-webdriverio')
await expect(browser).toHaveTitle('expect-webdriverio', { containing: true })
})
it('find elements', async () => {
const formInputs = await $$('form input')
// make sure every form element is enabled
// waits automatically for formInputs to have at least one element
await expect(formInputs).toBeEnabled({ wait: 5000 })
const selectOptions = await $$('form select>option')
// make sure there is at least one option in select
await expect(selectOptions).toBeElementsArrayOfSize({ gte: 1 })
// or pass in an element directly
await expect($$('button')).toBeElementsArrayOfSize(3)
})
it('checks text values', async () => {
// assert certain text accurate
const repoTitle = await $('expect-webdriverio')
await expect(repoTitle).toHaveText('expect-webdriverio')
})
it('advanced', async () => {
const myInput = await $('#username')
await expect(myInput).toHaveElementClass('form-control', { message: 'Not a form control!', })
await expect(myInput).toHaveAttribute('class', 'form-control')
await expect(myInput).toHaveValue('user', { containing: true, ignoreCase: true })
// Simply invert assertions
await expect(myInput).not.toHaveElementProperty('height', 0)
})
})
Boilerplate Projects
WebdriverIO test runner
- Mocha https://github.com/mgrybyk/webdriverio-devtools
- Cucumber https://gitlab.com/bar_foo/wdio-cucumber-typescript
- Jasmine https://github.com/webdriverio/jasmine-boilerplate
Standalone
more boilerplate projects coming soon, feel free to propose yours!
Playgrounds
Additional examples are available as internal playgrounds to the project. See playgrounds folder, including multi-remote examples.