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<string, WebdriverIO.Browser> = {}
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<string, PropertyDescriptor> = {}
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<string, WebdriverIO.Browser>,
result: unknown,
propertiesObject: Record<string, PropertyDescriptor>,
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))) {
client[identifier] = result[i]
}
client.instances = Object.keys(instances)
client.isMultiremote = true
client.selector = Array.isArray(result) && result[0]
? result[0].selector
: null
delete client.sessionId
return client
}, prototype)
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<string, WebdriverIO.Browser | WebdriverIO.Element>, 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) => (
{ ...ins, [instanceName]: mElem[instanceName] }
), {} as Record<string, Element[]>))
: 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
*/
export class MultiRemoteDriver {
instances: string[]
isMultiremote = true as const
__propertiesObject__: Record<string, PropertyDescriptor>
constructor (
instances: Record<string, WebdriverIO.Browser>,
propertiesObject: Record<string, PropertyDescriptor>
) {
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
}
}