import type { Services, Capabilities } from '@wdio/types'
import logger from '@wdio/logger'
import initializePlugin from './initializePlugin.js'
const log = logger('@wdio/utils:initializeServices')
type IntialisedService = (
[Services.ServiceClass | { default: Function }, WebdriverIO.ServiceOption, string] |
[Services.HookFunctions, Record<string, unknown>] |
[Services.ServiceClass, WebdriverIO.ServiceOption]
)
type Service = Services.ServiceEntry | Services.ServiceClass
type ServiceWithOptions = [Service, WebdriverIO.ServiceOption]
* Maps list of services of a config file into a list of actionable objects
* @param {Object} config config of running session
* @param {Object} caps capabilities of running session
* @return {[(Object|Class), Object][]} list of services with their config objects
*/
async function initializeServices (services: ServiceWithOptions[]): Promise<IntialisedService[]> {
const initializedServices: IntialisedService[] = []
for (const [serviceName, serviceConfig = {}] of services) {
* allow custom services that are already initialized, e.g.
*
* ```
* services: [
* [{ beforeTest: () => { ... } }]
* ]
* ```
*/
if (typeof serviceName === 'object') {
log.debug('initialize custom initiated service')
initializedServices.push([serviceName as Services.HookFunctions, {}])
continue
}
* allow custom service classes, e.g.
*
* ```
* class MyService { ... }
* ```
*
* in wdio.conf.js:
*
* ```
* services: [MyService]
* ```
*/
if (typeof serviceName === 'function') {
log.debug(`initialize custom service "${serviceName.name}"`)
initializedServices.push([serviceName as Services.ServiceClass, serviceConfig])
continue
}
* services as NPM packages
*
* ```
* services: ['@wdio/lighthouse-service']
* ```
*/
log.debug(`initialize service "${serviceName}" as NPM package`)
const service = await initializePlugin(serviceName, 'service')
initializedServices.push([service as Services.ServiceClass, serviceConfig, serviceName])
}
return initializedServices
}
* formats service array into proper structure which is an array with
* the service object as first parameter and the service option as
* second parameter
* @param {[Any]} service list of services from config file
* @return {[service, serviceConfig][]} formatted list of services
*/
function sanitizeServiceArray (service: Services.ServiceEntry): ServiceWithOptions {
return Array.isArray(service) ? service : [service, {}]
}
* initialize service for launcher process
* @param {Object} config wdio config
* @param {Object[]} caps list of capabilities
* @return {Object} containing a list of launcher services as well
* as a list of services that don't need to be
* required in the worker
*/
export async function initializeLauncherService (
config: Omit<WebdriverIO.Config, 'capabilities' | keyof Services.HookFunctions>,
caps: Capabilities.TestrunnerCapabilities
): Promise<{
ignoredWorkerServices: string[];
launcherServices: Services.ServiceInstance[];
}> {
const ignoredWorkerServices = []
const launcherServices: Services.ServiceInstance[] = []
let serviceLabelToBeInitialised = 'unknown'
try {
const services = await initializeServices(config.services!.map(sanitizeServiceArray))
for (const [service, serviceConfig, serviceName] of services) {
* add custom services as object or function
*/
if (typeof service === 'object' && !serviceName) {
serviceLabelToBeInitialised = 'object'
launcherServices.push(service as object)
continue
}
* add class service from imported package
*/
const Launcher = (service as Services.ServicePlugin).launcher
if (typeof Launcher === 'function' && serviceName) {
serviceLabelToBeInitialised = `"${serviceName}"`
launcherServices.push(new Launcher(serviceConfig, caps as Capabilities.ResolvedTestrunnerCapabilities, config))
}
* add class service from passed in class
*/
if (typeof service === 'function' && !serviceName) {
serviceLabelToBeInitialised = `"${service.constructor?.name || service.toString()}"`
launcherServices.push(new service(serviceConfig, caps as Capabilities.ResolvedTestrunnerCapabilities, config))
}
* check if service has a default export, if not we can later filter it out so the
* service module is not even loaded in the worker process
*/
if (
serviceName &&
typeof (service as { default: Function }).default !== 'function' &&
typeof service !== 'function'
) {
ignoredWorkerServices.push(serviceName)
}
}
} catch (err) {
throw new Error(`Failed to initialise launcher service ${serviceLabelToBeInitialised}: ${(err as Error).stack}`)
}
return { ignoredWorkerServices, launcherServices }
}
* initialize services for worker instance
* @param {Object} config wdio config
* @param {Object} caps worker capabilities
* @param {object} ignoredWorkerServices list of services that don't need to be required in a worker
* as they don't export a service for it
* @return {Object[]} list if worker initiated worker services
*/
export async function initializeWorkerService (
config: WebdriverIO.Config,
caps: WebdriverIO.Capabilities,
ignoredWorkerServices: string[] = []
): Promise<Services.ServiceInstance[]> {
let serviceLabelToBeInitialised = 'unknown'
const initializedServices: Services.ServiceInstance[] = []
const workerServices = config.services!
.map(sanitizeServiceArray)
.filter(([serviceName]) => !ignoredWorkerServices.includes(serviceName as string))
try {
const services = await initializeServices(workerServices)
for (const [service, serviceConfig, serviceName] of services) {
* add object service
*/
if (typeof service === 'object' && !serviceName) {
serviceLabelToBeInitialised = 'object'
initializedServices.push(service as Services.ServiceInstance)
continue
}
const Service = (service as Services.ServicePlugin).default || service as Services.ServiceClass
if (typeof Service === 'function') {
serviceLabelToBeInitialised = serviceName || Service.constructor?.name || Service.toString()
initializedServices.push(new Service(serviceConfig, caps, config))
continue
}
}
return initializedServices
} catch (err) {
throw new Error(`Failed to initialise service ${serviceLabelToBeInitialised}: ${(err as Error).stack}`)
}
}