import nock from 'nock' import type { CommandEndpoint, Protocol } from '@wdio/protocols' import { WebDriverProtocol, MJsonWProtocol, AppiumProtocol, ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol } from '@wdio/protocols' const REGEXP_SESSION_ID = /\/[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}/ const SESSION_ID = 'XXX' const protocols: Protocol[] = [ WebDriverProtocol, MJsonWProtocol, AppiumProtocol, ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol ] type RequestMethods = 'get' | 'post' type protocolFlattenedType = { method: string, endpoint: string, commandData: CommandEndpoint } const protocolFlattened: Map = new Map() export interface CommandMock { [commandName: string]: (...args: unknown[]) => nock.Interceptor } for (const protocol of protocols) { for (const [endpoint, methods] of Object.entries(protocol)) { for (const [method, commandData] of Object.entries(methods)) { protocolFlattened.set(commandData.command, { method, endpoint, commandData }) } } } export default class WebDriverMock { command: CommandMock scope: nock.Scope constructor(host: string = 'localhost', port: number = 4444, public path: string = '/') { this.scope = nock(`http://${host}:${port}`, { encodedQueryParams: true }) this.command = new Proxy({}, { get: this.get.bind(this) }) as unknown as CommandMock } /** * To allow random session IDs in url paths we have to set up a custom * matcher that strips out the sessionID part from the expected url * and actual url and replaces it with a constant session id * @param {String} expectedPath path to match against * @returns {Function} to be called by Nock to match actual path */ static pathMatcher(expectedPath: string): (path:string) =>boolean { return (path: string) => { const sessionId = path.match(REGEXP_SESSION_ID) /** * no session ID found so we can check against expected path directly */ if (!sessionId) { return path === expectedPath } /** * remove the session ID from expected and actual path * to only compare non arbitrary parts */ expectedPath = expectedPath.replace(':sessionId', SESSION_ID) path = path.replace(`${sessionId[0].slice(1)}`, SESSION_ID) return path === expectedPath } } get(_obj: unknown, commandName: string) { const { method, endpoint, commandData } = protocolFlattened.get(commandName) as protocolFlattenedType return (...args: unknown[]) => { let urlPath = endpoint for (const [i, param] of Object.entries(commandData.variables || [])) { urlPath = urlPath.replace(`:${param.name}`, args[parseInt(i)] as string) } if (method === 'POST') { const reqMethod = method.toLowerCase() as RequestMethods return this.scope[reqMethod](WebDriverMock.pathMatcher(urlPath), (body: Record) => { for (const param of commandData.parameters) { /** * check if parameter was set */ if (!body[param.name]) { return false } /** * check if parameter has correct type */ if (param.required && typeof body[param.name] === 'undefined') { return false } } /** * all parameters are valid */ return true }) } const reqMethod = method.toLowerCase() as RequestMethods return this.scope[reqMethod](WebDriverMock.pathMatcher(urlPath)) } } }