allow to use custom commands and overwrite command before session start

This commit is contained in:
Christian Bromann 2022-11-29 16:43:53 -08:00
parent adb037129c
commit 785d0cf78c
No known key found for this signature in database
GPG key ID: 355B5D0EA83B4D19
10 changed files with 97 additions and 68 deletions

View file

@ -8,6 +8,8 @@ const getWdioMock = () => {
debug: vi.fn(),
on: vi.fn(),
sessionId: 'fakeid',
addCommand: vi.fn(),
overwriteCommand: vi.fn(),
react$: vi.fn(),
react$$: vi.fn(),
custom$: vi.fn(),

View file

@ -273,11 +273,28 @@ export default class Runner extends EventEmitter {
caps: Capabilities.RemoteCapability
) {
try {
/**
* get all custom or overwritten commands users tried to register before the
* test started, e.g. after all imports
*/
const customStubCommands: [string, (...args: any[]) => any, boolean][] = (this._browser as any | undefined)?.customCommands || []
const overwrittenCommands: [any, (...args: any[]) => any, boolean][] = (this._browser as any | undefined)?.overwrittenCommands || []
this._browser = await initialiseInstance(config, caps, this._isMultiremote)
_setGlobal('browser', this._browser, config.injectGlobals)
_setGlobal('driver', this._browser, config.injectGlobals)
_setGlobal('expect', expect, config.injectGlobals)
/**
* re-assign previously registered custom commands to the actual instance
*/
for (const params of customStubCommands) {
this._browser.addCommand(...params)
}
for (const params of overwrittenCommands) {
this._browser.overwriteCommand(...params)
}
/**
* import and set options for `expect-webdriverio` assertion lib once
* the browser was initiated

View file

@ -91,7 +91,12 @@ export async function initialiseInstance (
}
const browser = await multiremote(options, config)
for (let browserName of Object.keys(capabilities)) {
/**
* only attach to global environment if `injectGlobals` is set to true
*/
const browserNames = config.injectGlobals ? Object.keys(capabilities) : []
for (let browserName of browserNames) {
// @ts-ignore allow random global browser names
global[browserName] = browser[browserName]
}

View file

@ -502,6 +502,27 @@ describe('wdio-runner', () => {
})
})
describe('_startSession', () => {
it('can start a session', async () => {
const runner = new WDIORunner()
const browser = await runner['_startSession']({} as any, {} as any)
expect(typeof browser?.deleteSession).toBe('function')
expect(_setGlobal).toBeCalledTimes(3)
expect(setOptions).toBeCalledTimes(1)
})
it('transfers custom commands from old instance to new one', async () => {
const runner = new WDIORunner()
runner['_browser'] = {
customCommands: [[1, 2, 3]],
overwrittenCommands: [[3, 2, 1]]
} as any
const browser = await runner['_startSession']({} as any, {} as any)
expect(browser?.addCommand).toBeCalledWith(1, 2, 3)
expect(browser?.overwriteCommand).toBeCalledWith(3, 2, 1)
})
})
describe('_shutdown', () => {
it('should emit exit', async () => {
const runner = new WDIORunner()

View file

@ -1,11 +1,5 @@
import { capabilitiesEnvironmentDetector } from '@wdio/utils'
import type { Capabilities, Options } from '@wdio/types'
import type { AttachOptions } from 'webdriver'
/**
* these commands can be used outside test scope and may be used accidentally by user before browser session is started
*/
const WARN_ON_COMMANDS = ['addCommand', 'overwriteCommand']
/**
* create `browser` object with capabilities and environment flags before session is started
@ -17,12 +11,18 @@ export default class ProtocolStub {
(options.capabilities || {}) as unknown as Capabilities.DesiredCapabilities
)
const browser = addCommands({
const browser: any = {
options,
capabilities,
requestedCapabilities: capabilities,
customCommands: [], // internally used to transfer custom commands to the actual protocol instance
overwrittenCommands: [], // internally used to transfer overwritten commands to the actual protocol instance
commandList: [],
...capabilitiesEnvironmentDetector(capabilities, (options as any)._automationProtocol || 'webdriver')
})
}
browser.options = options
browser.addCommand = (...args: any) => browser.customCommands.push(args)
browser.overwriteCommand = (...args: any) => browser.overwrittenCommands.push(args)
return browser
}
@ -34,35 +34,20 @@ export default class ProtocolStub {
throw new Error('Protocol Stub: Make sure to start webdriver or devtools session before reloading it.')
}
static attachToSession (
options: AttachOptions,
modifier?: (...args: any[]) => any
) {
static attachToSession (options: never, modifier?: Function) {
if (options || !modifier) {
return ProtocolStub.newSession(options as any)
throw new Error('You are trying to attach to a protocol stub, this should never occur, please file an issue.')
}
/**
* MultiRemote
* MultiRemote is needed
*/
return addCommands(modifier({
return modifier({
commandList: []
}))
})
}
}
/**
* provide better visibility to users that want to add / overwrite commands
* before session is started
* @param {object} browser
*/
function addCommands (browser: Record<string, any>) {
WARN_ON_COMMANDS.forEach((commandName) => {
browser[commandName] = commandNotAvailable(commandName)
})
return browser
}
/**
* transform capabilities provided by user to look like
* capabilities returned by WebDriver
@ -85,11 +70,3 @@ function emulateSessionCapabilities (caps: Capabilities.DesiredCapabilities) {
return capabilities
}
/**
* warn user to avoid usage of command before browser session is started.
* @param {string} commandName
*/
function commandNotAvailable (commandName: string) {
return () => { throw new Error(`Unable to use '${commandName}' before browser session is started.`) }
}

View file

@ -187,10 +187,6 @@ describe('WebdriverIO module interface', () => {
expect(browser.sessionId).toBeUndefined()
expect(browser.capabilities).toEqual({ browserName: 'chrome', chrome: true })
// @ts-ignore test types
expect(() => browser.addCommand()).toThrow()
// @ts-ignore test types
expect(() => browser.overwriteCommand()).toThrow()
const flags: any = {}
Object.entries(browser).forEach(([key, value]) => {

View file

@ -59,13 +59,6 @@ test('should properly create stub instance', async () => {
expect(browser.browserB).toBeDefined()
expect(browser.browserA.$).toBeUndefined()
expect(browser.browserA.$).toBeUndefined()
// @ts-expect-error invalid params
expect(() => browser.addCommand()).toThrow()
// @ts-expect-error invalid params
expect(() => browser.browserA.addCommand()).toThrow()
// @ts-expect-error invalid params
expect(() => browser.browserB.overwriteCommand()).toThrow()
})
test('should allow to call on elements', async () => {

View file

@ -19,7 +19,7 @@ describe('newSession', () => {
foo: 'bar'
}
})
expect(Object.keys(session)).toHaveLength(10)
expect(Object.keys(session)).toHaveLength(14)
expect(session.isAndroid).toBe(false)
expect(session.isChrome).toBe(false)
expect(session.isFirefox).toBe(false)
@ -31,25 +31,22 @@ describe('newSession', () => {
platformName: 'iOS',
foo: 'bar'
})
expect(() => session.addCommand()).toThrow()
expect(() => session.overwriteCommand()).toThrow()
})
})
describe('attachToSession', () => {
it('should return newSession if not multiremote', async () => {
it('should throw if not multiremote', async () => {
const modifier = vi.fn()
const session = await ProtocolStub.attachToSession({
expect(() => ProtocolStub.attachToSession({
sessionId: '1234',
capabilities: { browserName: 'chrome' }
}, modifier)
} as never, modifier)).toThrow()
expect(modifier).not.toBeCalled()
expect(session.isChrome).toBe(true)
})
it('should return newSession if modifier was not passed', async () => {
const session = await ProtocolStub.attachToSession({ sessionId: '1234' })
expect(session.capabilities).toEqual({})
expect(() => ProtocolStub.attachToSession({ sessionId: '1234' } as never))
.toThrow()
})
it('should call modifier if multiremote', async () => {

View file

@ -3,11 +3,32 @@ id: customcommands
title: Custom Commands
---
## Adding 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.
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](/docs/api/browser) object.
## 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`](/docs/api/browser) or [`WebdriverIO.Element`](/docs/api/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`<br />
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`](/docs/api/browser) object.
```js
browser.addCommand('getUrlAndTitle', async function (customVar) {
@ -20,7 +41,7 @@ browser.addCommand('getUrlAndTitle', async function (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](/docs/api/element) object.
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`](/docs/api/element) object.
```js
browser.addCommand("waitAndClick", async function () {
@ -167,7 +188,7 @@ declare namespace WebdriverIO {
</TabItem>
</Tabs>
## Integrate 3rd party libraries
## 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.
@ -193,7 +214,7 @@ it('execute external library in a sync way', async () => {
**Note:** The result of your custom command is the result of the promise you return.
## Overwriting native commands
## Overwriting Commands
You can also overwrite native commands with `overwriteCommand`.
@ -201,7 +222,7 @@ It is not recommended to do this, because it may lead to unpredictable behavior
The overall approach is similar to `addCommand`, the only difference is that the first argument in the command function is the original function that you are about to overwrite. Please see some examples below.
### Overwriting browser commands
### Overwriting Browser Commands
```js
/**
@ -219,7 +240,7 @@ browser.overwriteCommand('pause', async (origPauseFunction, ms) => {
console.log(`was sleeping for ${await browser.pause(1000)}`)
```
#### Overwriting element commands
### Overwriting Element Commands
Overwriting commands on element level is almost the same. Simply pass `true` as the third argument to `overwriteCommand`:

View file

@ -34,8 +34,8 @@ In addition to that the following commands are available:
| Name | Parameters | Details |
| ---- | ---------- | ------- |
| `addCommand` | - `commandName` (Type: `String`)<br />- `fn` (Type: `Function`) | Allows to define custom commands that can be called from the browser object for compisition purposes. Read more in the [Custom Command](/docs/customcommands) guide. |
| `overwriteCommand` | - `commandName` (Type: `String`)<br />- `fn` (Type: `Function`) | Allows to overwite any browser command with custom functionality. Use carefully as it can confuse framework users. Read more in the [Custom Command](/docs/customcommands#overwriting-native-commands) guide. |
| `addCommand` | - `commandName` (Type: `String`)<br />- `fn` (Type: `Function`)<br />- `attachToElement` (Type: `boolean`) | Allows to define custom commands that can be called from the browser object for compisition purposes. Read more in the [Custom Command](/docs/customcommands) guide. |
| `overwriteCommand` | - `commandName` (Type: `String`)<br />- `fn` (Type: `Function`)<br />- `attachToElement` (Type: `boolean`) | Allows to overwite any browser command with custom functionality. Use carefully as it can confuse framework users. Read more in the [Custom Command](/docs/customcommands#overwriting-native-commands) guide. |
## Remarks