Custom Commands
If you want to extend the browser
instance with your own set of commands, the browser method addCommand
is here for you. You can write your command in a asynchronous way, just as in your specs.
Parameters
Command Name
A name that defines the command and will be attached to the browser or element scope.
Type: String
Custom Function
A function that is being executed when the command is called. The this
scope is either WebdriverIO.Browser
or WebdriverIO.Element
depending whether the command gets attached to the browser or element scope.
Type: Function
Target Scope
Flag to decide whether to attach the command to the browser or element scope. If set to true
the command will be an element command.
Type: Boolean
Default: false
Examples
This example shows how to add a new command that returns the current URL and title as one result. The scope (this
) is a WebdriverIO.Browser
object.
browser.addCommand('getUrlAndTitle', async function (customVar) {
// `this` refers to the `browser` scope
return {
url: await this.getUrl(),
title: await this.getTitle(),
customVar: customVar
}
})
Additionally, you can extend the element instance with your own set of commands, by passing true
as the final argument. The scope (this
) in this case is a WebdriverIO.Element
object.
browser.addCommand("waitAndClick", async function () {
// `this` is return value of $(selector)
await this.waitForDisplayed()
await this.click()
}, true)
Custom commands give you the opportunity to bundle a specific sequence of commands you use frequently as a single call. You can define custom commands at any point in your test suite; just make sure that the command is defined before its first use. (The before
hook in your wdio.conf.js
is one good place to create them.)
Once defined, you can use them as follows:
it('should use my custom command', async () => {
await browser.url('http://www.github.com')
const result = await browser.getUrlAndTitle('foobar')
assert.strictEqual(result.url, 'https://github.com/')
assert.strictEqual(result.title, 'GitHub · Where software is built')
assert.strictEqual(result.customVar, 'foobar')
})
Note: If you register a custom command to the browser
scope, the command won't be accessible for elements. Likewise, if you register a command to the element scope, it won't be accessible in the browser
scope:
browser.addCommand("myCustomBrowserCommand", () => { return 1 })
const elem = await $('body')
console.log(typeof browser.myCustomBrowserCommand) // outputs "function"
console.log(typeof elem.myCustomBrowserCommand()) // outputs "undefined"
browser.addCommand("myCustomElementCommand", () => { return 1 }, true)
const elem2 = await $('body')
console.log(typeof browser.myCustomElementCommand) // outputs "undefined"
console.log(await elem2.myCustomElementCommand('foobar')) // outputs "1"
const elem3 = await $('body')
elem3.addCommand("myCustomElementCommand2", () => { return 2 })
console.log(typeof browser.myCustomElementCommand2) // outputs "undefined"
console.log(await elem3.myCustomElementCommand2('foobar')) // outputs "2"
Note: If you need to chain a custom command, the command should end with $
,
browser.addCommand("user$", (locator) => { return ele })
browser.addCommand("user$", (locator) => { return ele }, true)
await browser.user$('foo').user$('bar').click()
Be careful to not overload the browser
scope with too many custom commands.
We recommend defining custom logic in page objects, so they are bound to a specific page.
Extend Type Definitions
With TypeScript, it's easy to extend WebdriverIO interfaces. Add types to your custom commands like this:
-
Create a type definition file (e.g.,
./src/types/wdio.d.ts
) -
a. If using a module-style type definition file (using import/export and
declare global WebdriverIO
in the type definition file), make sure to include the file path in thetsconfig.json
include
property.b. If using ambient-style type definition files (no import/export in type definition files and
declare namespace WebdriverIO
for custom commands), make sure thetsconfig.json
does not contain anyinclude
section, since this will cause all type definition files not listed in theinclude
section to not be recognized by typescript.
- Modules (using import/export)
- Ambient Type Definitions (no tsconfig include)
{
"compilerOptions": { ... },
"include": [
"./test/**/*.ts",
"./src/types/**/*.ts"
]
}
{
"compilerOptions": { ... }
}
- Add definitions for your commands according to your execution mode.
- Modules (using import/export)
- Ambient Type Definitions
declare global {
namespace WebdriverIO {
interface Browser {
browserCustomCommand: (arg: any) => Promise<void>
}
interface MultiRemoteBrowser {
browserCustomCommand: (arg: any) => Promise<void>
}
interface Element {
elementCustomCommand: (arg: any) => Promise<number>
}
}
}
declare namespace WebdriverIO {
interface Browser {
browserCustomCommand: (arg: any) => Promise<void>
}
interface MultiRemoteBrowser {
browserCustomCommand: (arg: any) => Promise<void>
}
interface Element {
elementCustomCommand: (arg: any) => Promise<number>
}
}
Integrate 3rd Party Libraries
If you use external libraries (e.g., to do database calls) that support promises, a nice approach to integrate them is to wrap certain API methods with a custom command.
When returning the promise, WebdriverIO ensures that it doesn't continue with the next command until the promise is resolved. If the promise gets rejected, the command will throw an error.
browser.addCommand('makeRequest', async (url) => {
const response = await fetch(url)
return await response.json()
})
Then, just use it in your WDIO test specs:
it('execute external library in a sync way', async () => {
await browser.url('...')
const body = await browser.makeRequest('http://...')
console.log(body) // returns response body
})
Note: The result of your custom command is the result of the promise you return.