import zip from 'lodash.zip' import clone from 'lodash.clonedeep' import { webdriverMonad, wrapCommand } from '@wdio/utils' import type { Options } from '@wdio/types' import type { ProtocolCommands } from '@wdio/protocols' import { multiremoteHandler } from './middlewares.js' import { getPrototype } from './utils/index.js' import type { BrowserCommandsType, WebdriverIOEventMap } from './types.js' type EventEmitter = (args: unknown) => void /** * Multiremote class */ export default class MultiRemote { instances: Record = {} baseInstance?: MultiRemoteDriver sessionId?: string /** * add instance to multibrowser instance */ async addInstance (browserName: string, client: WebdriverIO.Browser) { this.instances[browserName] = client return this.instances[browserName] } /** * modifier for multibrowser instance */ modifier (wrapperClient: { options: Options.WebdriverIO, commandList: (keyof (ProtocolCommands & BrowserCommandsType) & 'getInstance')[] }) { const propertiesObject: Record = {} propertiesObject.commandList = { value: wrapperClient.commandList } propertiesObject.options = { value: wrapperClient.options } propertiesObject.getInstance = { value: (browserName: string) => this.instances[browserName] } for (const commandName of wrapperClient.commandList) { propertiesObject[commandName] = { value: this.commandWrapper(commandName), configurable: true } } propertiesObject.__propertiesObject__ = { value: propertiesObject } this.baseInstance = new MultiRemoteDriver(this.instances, propertiesObject) const client = Object.create(this.baseInstance, propertiesObject) /** * attach instances to wrapper client * ToDo(Christian): deprecate and remove */ for (const [identifier, instance] of Object.entries(this.instances)) { client[identifier] = instance } return client } /** * helper method to generate element objects from results, so that we can call, e.g. * * ``` * const elem = $('#elem') * elem.getHTML() * ``` * * or in case multiremote is used * * ``` * const elems = $$('div') * elems[0].getHTML() * ``` */ static elementWrapper ( instances: Record, result: unknown, propertiesObject: Record, scope: MultiRemote ): WebdriverIO.MultiRemoteElement { const prototype = { ...propertiesObject, ...clone(getPrototype('element')), scope: { value: 'element' } } const element = webdriverMonad({}, (client: WebdriverIO.MultiRemoteElement) => { /** * attach instances to wrapper client */ for (const [i, identifier] of Object.entries(Object.keys(instances))) { // @ts-expect-error ToDo(Christian): deprecate client[identifier] = result[i] } client.instances = Object.keys(instances) client.isMultiremote = true client.selector = Array.isArray(result) && result[0] ? result[0].selector : null // @ts-expect-error ToDo(Christian): remove eventually delete client.sessionId return client }, prototype) // @ts-ignore return element(this.sessionId, multiremoteHandler(scope.commandWrapper.bind(scope))) } /** * handle commands for multiremote instances */ commandWrapper (commandName: keyof (ProtocolCommands & BrowserCommandsType) & 'getInstance') { const instances = this.instances const self = this if (commandName === 'getInstance') { return function (this: Record, browserName: string) { if (!this[browserName]) { throw new Error(`Multiremote object has no instance named "${browserName}"`) } return this[browserName] } } return wrapCommand(commandName, async function (this: WebdriverIO.MultiRemoteBrowser | WebdriverIO.MultiRemoteElement, ...args: unknown[]) { const mElem = this as WebdriverIO.MultiRemoteElement const scope = (this as WebdriverIO.MultiRemoteElement).selector ? Object.entries(mElem.instances.reduce((ins, instanceName) => ( // @ts-expect-error ToDo(Christian): deprecate { ...ins, [instanceName]: mElem[instanceName] } ), {} as Record)) : Object.entries(instances) const result = await Promise.all( scope.map( ([, instance]) => instance[commandName](...args) ) ) /** * return element object to call commands directly */ if (commandName === '$') { const elem = MultiRemote.elementWrapper(instances, result, this.__propertiesObject__, self) return elem } else if (commandName === '$$') { const zippedResult = zip(...result) return zippedResult.map((singleResult) => MultiRemote.elementWrapper(instances, singleResult, this.__propertiesObject__, self)) } return result }) } } /** * event listener class that propagates events to sub drivers */ /* istanbul ignore next */ export class MultiRemoteDriver { instances: string[] isMultiremote = true as const __propertiesObject__: Record constructor ( instances: Record, propertiesObject: Record ) { this.instances = Object.keys(instances) this.__propertiesObject__ = propertiesObject } on (this: WebdriverIO.MultiRemoteBrowser, eventName: keyof WebdriverIOEventMap, emitter: EventEmitter) { this.instances.forEach((instanceName) => this.getInstance(instanceName).on(eventName, emitter)) return undefined } once (this: WebdriverIO.MultiRemoteBrowser, eventName: keyof WebdriverIOEventMap, emitter: EventEmitter) { this.instances.forEach((instanceName) => this.getInstance(instanceName).once(eventName, emitter)) return undefined } emit (this: WebdriverIO.MultiRemoteBrowser, eventName: keyof WebdriverIOEventMap, emitter: EventEmitter) { return this.instances.map( (instanceName) => this.getInstance(instanceName).emit(eventName, emitter) ).some(Boolean) } eventNames (this: WebdriverIO.MultiRemoteBrowser) { return this.instances.map( (instanceName) => this.getInstance(instanceName).eventNames() ) } getMaxListeners (this: WebdriverIO.MultiRemoteBrowser) { return this.instances.map( (instanceName) => this.getInstance(instanceName).getMaxListeners() ) } listenerCount (this: WebdriverIO.MultiRemoteBrowser, eventName: string) { return this.instances.map( (instanceName) => this.getInstance(instanceName).listenerCount(eventName) ) } listeners (this: WebdriverIO.MultiRemoteBrowser, eventName: string) { return this.instances.map( (instanceName) => this.getInstance(instanceName).listeners(eventName) ).reduce((prev, cur) => { prev.concat(cur) return prev }, []) } removeListener (this: WebdriverIO.MultiRemoteBrowser, eventName: string, emitter: EventEmitter) { this.instances.forEach((instanceName) => this.getInstance(instanceName).removeListener(eventName, emitter)) return undefined } removeAllListeners (this: WebdriverIO.MultiRemoteBrowser, eventName: string) { this.instances.forEach((instanceName) => this.getInstance(instanceName).removeAllListeners(eventName)) return undefined } }