๐Ÿก index : quine-global/webdriverio.git

/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from 'vitest'

/**
 * This flag helps to indicate that WebdriverIO is running in a unit test environment.
 * Setting this environment changes the behavior of some functions to e.g. not exit
 * the process or enter code sections that are hard to mock out.
 */
process.env.WDIO_UNIT_TESTS = '1'
globalThis.WDIO_RESQ_SCRIPT = ''
globalThis.WDIO_FAKER_SCRIPT = ''

const ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf'
const SHADOW_ELEMENT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf'

let manualMockResponse: any

const path = '/session'

const customResponses = new Set<{ pattern, response }>()
const defaultSessionId = 'foobar-123'
let sessionId = defaultSessionId
const genericElementId = 'some-elem-123'
const genericSubElementId = 'some-sub-elem-321'
const genericSubSubElementId = 'some-sub-sub-elem-231'
const genericShadowElementId = 'some-shadow-elem-123'
const genericSubShadowElementId = 'some-shadow-sub-elem-321'

/**
 * Transform the specified property of each object in the collection by replacing 'mockFunction' with a predefined function (vi.fn()).
 * This is intended to ensure that, when converting the request body to a string, functions are retained and not omitted.
 * @param collection - An array of objects to process.
 * @returns A new array with updated objects.
 */
const transformPropertyWithMockFunction = (collection: any[]) => {
    return collection.map(item => {
        for (const prop in item) {
            if (item[prop] && item[prop] === 'mockFunction') {
                item[prop] = vi.fn()
            }
        }
        return item
    })
}

const requestMock: any = vi.fn().mockImplementation((uri, params) => {
    let value: any = {}
    let jsonwpMode = false
    let sessionResponse: any = {
        sessionId,
        capabilities: {
            browserName: 'mockBrowser',
            platformName: 'node',
            browserVersion: '1234',
            setWindowRect: true
        }
    }

    if (typeof uri === 'string') {
        uri = new URL(uri)
    }

    for (const { pattern, response } of customResponses) {
        if (!(uri as URL).pathname.match(pattern)) {
            continue
        }
        return Response.json(response)
    }

    let body: any = params?.body

    try {
        body = body && JSON.parse(body.toString())
    } catch {
        return Response.json({}, {
            status: 422,
            statusText: 'Unprocessable Entity'
        })
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.jsonwpMode
    ) {
        jsonwpMode = true
        sessionResponse = {
            sessionId,
            browserName: 'mockBrowser'
        }
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.mobileMode
    ) {
        sessionResponse.capabilities.deviceName = 'iNode'
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.mobileMode &&
        body.capabilities.alwaysMatch.nativeAppMode
    ) {
        sessionResponse.capabilities.app = 'mockApp'
        delete sessionResponse.capabilities.browserName
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.mobileMode &&
        body.capabilities.alwaysMatch.windowsAppMode
    ) {
        sessionResponse.capabilities['appium:automationName'] = 'windows'
        delete sessionResponse.capabilities.browserName
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.mobileMode &&
        body.capabilities.alwaysMatch.macAppMode
    ) {
        sessionResponse.capabilities['appium:automationName'] = 'mac2'
        delete sessionResponse.capabilities.browserName
    }

    if (
        body &&
        body.capabilities &&
        body.capabilities.alwaysMatch.keepBrowserName
    ) {
        sessionResponse.capabilities.browserName = body.capabilities.alwaysMatch.browserName
    }

    if (body?.capabilities?.alwaysMatch?.browserName === 'bidi') {
        sessionResponse.capabilities.webSocketUrl = 'ws://webdriver.io'
    }

    switch (uri.pathname) {
    case path:
        value = sessionResponse

        if (body.capabilities.alwaysMatch.browserName && body.capabilities.alwaysMatch.browserName.includes('devtools')) {
            value.capabilities['goog:chromeOptions'] = {
                debuggerAddress: 'localhost:1234'
            }
        }

        if (body.capabilities.alwaysMatch.platformName && body.capabilities.alwaysMatch.platformName.includes('iOS')) {
            value.capabilities.platformName = 'iOS'
        }

        break
    case `/session/${sessionId}/element`:
        if (body && body.value === '#nonexisting') {
            value = { elementId: null }
            break
        }

        if (body && body.value === 'html') {
            value = { [ELEMENT_KEY]: 'html-element' }
            break
        }

        if (body && body.value === '#slowRerender') {
            ++requestMock.retryCnt
            if (requestMock.retryCnt === 2) {
                ++requestMock.retryCnt
                value = { elementId: null }
                break
            }
        }
        value = {
            [ELEMENT_KEY]: genericElementId
        }

        break
    case `${path}/${sessionId}/element/${genericElementId}/element`:
        value = {
            [ELEMENT_KEY]: genericSubElementId
        }
        break
    case `${path}/${sessionId}/shadow/${genericShadowElementId}/element`:
        value = {
            [ELEMENT_KEY]: genericSubShadowElementId
        }
        break
    case `${path}/${sessionId}/element/${genericElementId}/shadow`:
        value = {
            [SHADOW_ELEMENT_KEY]: genericShadowElementId
        }
        break
    case `${path}/${sessionId}/element/${genericSubElementId}/element`:
        value = {
            [ELEMENT_KEY]: genericSubSubElementId
        }
        break
    case `${path}/${sessionId}/element/html-element/rect`:
        value = {
            x: 0,
            y: 0,
            height: 1000,
            width: 1000
        }
        break
    case `${path}/${sessionId}/element/${genericElementId}/rect`:
        value = {
            x: 15,
            y: 20,
            height: 30,
            width: 50
        }
        break
    case `${path}/${sessionId}/element/${genericSubElementId}/rect`:
        value = {
            x: 100,
            y: 200,
            height: 120,
            width: 150
        }
        break
    case `${path}/${sessionId}/element/${genericElementId}/size`:
        value = {
            height: 30,
            width: 50
        }
        break
    case `${path}/${sessionId}/element/${genericElementId}/location`:
        value = {
            x: 15,
            y: 20
        }
        break
    case `${path}/${sessionId}/element/${genericElementId}/displayed`:
        value = true
        break
    case `${path}/${sessionId}/elements`:
        value = [
            { [ELEMENT_KEY]: genericElementId },
            { [ELEMENT_KEY]: 'some-elem-456' },
            { [ELEMENT_KEY]: 'some-elem-789' },
        ]
        break
    case `${path}/${sessionId}/element/${genericElementId}/css/width`:
        value = '1250px'
        break
    case `${path}/${sessionId}/element/${genericElementId}/css/margin-top`:
    case `${path}/${sessionId}/element/${genericElementId}/css/margin-right`:
    case `${path}/${sessionId}/element/${genericElementId}/css/margin-bottom`:
    case `${path}/${sessionId}/element/${genericElementId}/css/margin-left`:
        value = '42px'
        break
    case `${path}/${sessionId}/element/${genericElementId}/css/padding-top`:
    case `${path}/${sessionId}/element/${genericElementId}/css/padding-bottom`:
        value = '4px'
        break
    case `${path}/${sessionId}/element/${genericElementId}/css/padding-right`:
    case `${path}/${sessionId}/element/${genericElementId}/css/padding-left`:
        value = '2px'
        break
    case `${path}/${sessionId}/element/${genericElementId}/property/tagName`:
        value = 'BODY'
        break
    case `/session/${sessionId}/element/${genericElementId}/css/display`:
        value = 'contents'
        break
    case `/session/${sessionId}/execute`:
    case `/session/${sessionId}/execute/sync`: {
        const script = Function(body.script)
        const args = transformPropertyWithMockFunction(body.args.map((arg: any) => (arg && (arg.ELEMENT || arg[ELEMENT_KEY])) || arg))
        let result: any = null
        if (body.script.includes('resq')) {
            if (body.script.includes('react$$')) {
                result = [
                    { [ELEMENT_KEY]: genericElementId },
                    { [ELEMENT_KEY]: 'some-elem-456' },
                    { [ELEMENT_KEY]: 'some-elem-789' },
                ]
            } else if (body.script.includes('react$')) {
                result = args[0] === 'myNonExistingComp'
                    ? new Error('foobar')
                    : { [ELEMENT_KEY]: genericElementId }
            } else {
                result = null
            }
        } else if (body.script.includes('testLocatorStrategy')) {
            result = { [ELEMENT_KEY]: genericElementId }
        } else if (body.script.includes('testLocatorStrategiesMultiple')) {
            result = [
                { [ELEMENT_KEY]: genericElementId },
                { [ELEMENT_KEY]: 'some-elem-456' },
                { [ELEMENT_KEY]: 'some-elem-789' },
            ]
        } else if (body.script.includes('previousElementSibling')) {
            result = body.args[0][ELEMENT_KEY] === genericSubElementId
                ? { [ELEMENT_KEY]: 'some-previous-elem' }
                : {}
        } else if (body.script.includes('parentElement')) {
            result = body.args[0][ELEMENT_KEY] === genericSubElementId
                ? { [ELEMENT_KEY]: 'some-parent-elem' }
                : {}
        } else if (body.script.includes('nextElementSibling')) {
            result = body.args[0][ELEMENT_KEY] === genericElementId
                ? { [ELEMENT_KEY]: 'some-next-elem' }
                : {}
        } else if (body.script.includes('scrollX')) {
            result = [0, 0]
        } else if (body.script.includes('function isFocused')) {
            result = true
        } else if (body.script.includes('mobile:')) {
            result = true
        } else if (body.script.includes('document.URL')) {
            result = 'https://webdriver.io/?foo=bar'
        } else if (body.script.includes('function checkVisibility')) {
            result = true
        } else {
            result = script.apply(this, args)
        }

        //false and 0 are valid results
        value = Boolean(result) || result === false || result === 0 || result === null ? result : {}
        break
    } case `/session/${sessionId}/execute/async`: {
        const script = Function(body.script)
        let result
        script.call(this, ...body.args, (_result: any) => result = _result)
        value = result ?? {}
        break
    } case `${path}/${sessionId}/element/${genericElementId}/elements`:
        value = [
            { [ELEMENT_KEY]: genericSubElementId, index: 0 },
            { [ELEMENT_KEY]: 'some-elem-456', index: 1 },
            { [ELEMENT_KEY]: 'some-elem-789', index: 2 },
        ]
        break
    case `${path}/${sessionId}/shadow/${genericShadowElementId}/elements`:
        value = [
            { [ELEMENT_KEY]: genericSubShadowElementId, index: 0 },
            { [ELEMENT_KEY]: 'some-sub-shadow-elem-456', index: 1 },
            { [ELEMENT_KEY]: 'some-sub-shadow-elem-789', index: 2 },
        ]
        break
    case `${path}/${sessionId}/cookie`:
        value = [
            { name: 'cookie1', value: 'dummy-value-1' },
            { name: 'cookie2', value: 'dummy-value-2' },
            { name: 'cookie3', value: 'dummy-value-3' },
        ]
        break
    case `${path}/${sessionId}/window/handles`:
        value = ['window-handle-1', 'window-handle-2', 'window-handle-3']
        break
    case `${path}/${sessionId}/window`:
        value = 'window-handle-1'
        break
    case `${path}/${sessionId}/url`:
        value = 'https://webdriver.io/?foo=bar'
        break
    case `${path}/${sessionId}/title`:
        value = 'WebdriverIO ยท Next-gen browser and mobile automation test framework for Node.js | WebdriverIO'
        break
    case `${path}/${sessionId}/screenshot`:
    case `${path}/${sessionId}/appium/stop_recording_screen`:
        value = Buffer.from('some screenshot').toString('base64')
        break
    case `${path}/${sessionId}/print`:
        value = Buffer.from('some pdf print').toString('base64')
        break
    case `${path}/${sessionId}/element/${genericElementId}/screenshot`:
        value = Buffer.from('some element screenshot').toString('base64')
        break
    case '/grid/api/hub':
        value = { some: 'config' }
        break
    case '/grid/api/testsession':
        value = '<!DOCTYPE html><html lang="en"></html>'
        break
    case '/connectionRefused':
        if (requestMock.retryCnt < 5) {
            ++requestMock.retryCnt
            value = {
                stacktrace: 'java.lang.RuntimeException: java.net.ConnectException: Connection refused',
                stackTrace: [],
                message: 'java.net.ConnectException: Connection refused: connect',
                error: 'unknown error'
            }
        } else {
            value = { foo: 'bar' }
        }
    }

    if (uri.pathname.endsWith('timeout') && requestMock.retryCnt < 5) {
        const timeoutError: any = new Error('Timeout')
        timeoutError.name = 'TimeoutError'
        timeoutError.code = 'ETIMEDOUT'
        timeoutError.event = 'request'
        ++requestMock.retryCnt

        return Promise.reject(timeoutError)
    }

    if (uri.pathname.startsWith(`/session/${sessionId}/element/`) && uri.pathname.includes('/attribute/')) {
        value = `${uri.pathname.substring(uri.pathname.lastIndexOf('/') + 1)}-value`
    }

    if (uri.pathname.endsWith('sumoerror')) {
        return Promise.reject(new Error('ups'))
    }

    /**
     * Simulate a stale element
     */
    if (uri.pathname === `/session/${sessionId}/element/${genericSubSubElementId}/click`) {
        ++requestMock.retryCnt

        if (requestMock.retryCnt > 1) {
            const response = { value: null }
            return Response.json(response, {
                status: 200,
                headers: { foo: 'bar' }
            })
        }

        // https://www.w3.org/TR/webdriver1/#handling-errors
        const error = {
            value: {
                'error': 'stale element reference',
                'message': 'element is not attached to the page document'
            }
        }

        return Response.json(error, {
            status: 404,
            headers: { foo: 'bar' }
        })
    }

    /**
     * empty response
     */
    if (uri.pathname === '/empty') {
        return Response.json('', {
            status: 500,
            headers: { foo: 'bar' }
        })
    }

    /**
     * session error due to wrong path
     */
    if (uri.pathname === '/wrong/path') {
        return Response.json({}, {
            status: 404,
            headers: { foo: 'bar' }
        })
    }

    /**
     * simulate failing response
     */
    if (uri.pathname === '/failing') {
        ++requestMock.retryCnt

        /**
         * success this request if you retry 3 times
         */
        if (requestMock.retryCnt > 3) {
            const response = { value: 'caught' }

            return Response.json(response, {
                status: 200,
                headers: { foo: 'bar' }
            })
        }

        return Response.json({}, {
            status: 400,
            headers: { foo: 'bar' }
        })
    }

    /**
     * overwrite if manual response is set
     */
    let statusCode = 200
    if (Array.isArray(manualMockResponse)) {
        value = manualMockResponse.shift() || value

        if (typeof value.statusCode === 'number') {
            statusCode = value.statusCode
        }

        if (manualMockResponse.length === 0) {
            manualMockResponse = null
        }
    } else if (manualMockResponse) {
        value = manualMockResponse
        manualMockResponse = null
    }

    let response: any = { value }
    if (jsonwpMode) {
        response = { value, sessionId, status: 0 }
    }

    if (uri.pathname.startsWith('/grid')) {
        response = response.value
    }

    return Response.json(response, {
        status: statusCode,
        headers: { foo: 'bar' }
    })
})

requestMock.retryCnt = 0
requestMock.setMockResponse = (value: any) => {
    manualMockResponse = value
}
requestMock.customResponseFor = (pattern: RegExp, response: any) => {
    const existingEntry = Array.from(customResponses.values())
        .find((p) => p.pattern.toString() === pattern.toString())
    if (existingEntry) {
        customResponses.delete(existingEntry)
    }
    customResponses.add({ pattern, response })
}

requestMock.getSessionId = () => sessionId
requestMock.setSessionId = (newSessionId: any) => {
    sessionId = newSessionId
}
requestMock.resetSessionId = () => {
    sessionId = defaultSessionId
}

vi.stubGlobal('fetch', requestMock)