diff --git a/__mocks__/webdriverio.ts b/__mocks__/webdriverio.ts index d09f068d9..3ba311ba9 100644 --- a/__mocks__/webdriverio.ts +++ b/__mocks__/webdriverio.ts @@ -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(), diff --git a/packages/wdio-runner/src/index.ts b/packages/wdio-runner/src/index.ts index 38c7c18f1..ab5c8a2ae 100644 --- a/packages/wdio-runner/src/index.ts +++ b/packages/wdio-runner/src/index.ts @@ -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 diff --git a/packages/wdio-runner/src/utils.ts b/packages/wdio-runner/src/utils.ts index 43caa1960..ac5973883 100644 --- a/packages/wdio-runner/src/utils.ts +++ b/packages/wdio-runner/src/utils.ts @@ -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] } diff --git a/packages/wdio-runner/tests/index.test.ts b/packages/wdio-runner/tests/index.test.ts index e513ba7d6..2b41d5609 100644 --- a/packages/wdio-runner/tests/index.test.ts +++ b/packages/wdio-runner/tests/index.test.ts @@ -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() diff --git a/packages/webdriverio/src/protocol-stub.ts b/packages/webdriverio/src/protocol-stub.ts index 0ce5a53b8..6ddb30fc9 100644 --- a/packages/webdriverio/src/protocol-stub.ts +++ b/packages/webdriverio/src/protocol-stub.ts @@ -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) { - 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.`) } -} diff --git a/packages/webdriverio/tests/module.test.ts b/packages/webdriverio/tests/module.test.ts index a2c119611..ca90be556 100644 --- a/packages/webdriverio/tests/module.test.ts +++ b/packages/webdriverio/tests/module.test.ts @@ -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]) => { diff --git a/packages/webdriverio/tests/multiremote.test.ts b/packages/webdriverio/tests/multiremote.test.ts index 0bffcafdc..16db37ffa 100644 --- a/packages/webdriverio/tests/multiremote.test.ts +++ b/packages/webdriverio/tests/multiremote.test.ts @@ -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 () => { diff --git a/packages/webdriverio/tests/protocol-stub.test.ts b/packages/webdriverio/tests/protocol-stub.test.ts index 5c7c454ba..0024705ba 100644 --- a/packages/webdriverio/tests/protocol-stub.test.ts +++ b/packages/webdriverio/tests/protocol-stub.test.ts @@ -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 () => { diff --git a/website/docs/CustomCommands.md b/website/docs/CustomCommands.md index d91d5f46d..5d5df1fe6 100644 --- a/website/docs/CustomCommands.md +++ b/website/docs/CustomCommands.md @@ -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`
+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 { -## 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`: diff --git a/website/docs/api/Browser.md b/website/docs/api/Browser.md index cffa90e6d..633de0f42 100644 --- a/website/docs/api/Browser.md +++ b/website/docs/api/Browser.md @@ -34,8 +34,8 @@ In addition to that the following commands are available: | Name | Parameters | Details | | ---- | ---------- | ------- | -| `addCommand` | - `commandName` (Type: `String`)
- `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`)
- `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`)
- `fn` (Type: `Function`)
- `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`)
- `fn` (Type: `Function`)
- `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