🏡 index : quine-global/webdriverio.git

import { transformCommandLogResult } from '@wdio/utils'

import { REG_EXPS } from './constants.js'

abstract class WebDriverError extends Error {
    abstract url: URL
    abstract opts: RequestInit

    /**
     * return timeout error with information about the executing command on which the test hangs
     */
    computeErrorMessage() {
        const cmdName = this.#getExecCmdName()
        const cmdArgs = this.#getExecCmdArgs(this.opts)

        const cmdInfoMsg = `when running "${cmdName}" with method "${this.opts.method}"`
        const cmdArgsMsg = cmdArgs ? ` and args ${cmdArgs}` : ''

        return `WebDriverError: ${this.message} ${cmdInfoMsg}${cmdArgsMsg}`
    }

    #getExecCmdName(): string {
        const { href } = this.url
        const res = href.match(REG_EXPS.commandName) || []

        return res[1] || href
    }

    #getExecCmdArgs(requestOptions: RequestInit): string {
        const { body: cmdJson } = requestOptions as unknown as { body: Record<string, unknown> }

        if (typeof cmdJson !== 'object') {
            return ''
        }

        const transformedRes = transformCommandLogResult(cmdJson)

        if (typeof transformedRes === 'string') {
            return transformedRes
        }

        if (typeof cmdJson.script === 'string') {
            const scriptRes = cmdJson.script.match(REG_EXPS.execFn) || []

            return `"${scriptRes[1] || cmdJson.script}"`
        }

        return Object.keys(cmdJson).length ? `"${JSON.stringify(cmdJson)}"` : ''
    }
}

export class WebDriverRequestError extends WebDriverError {
    url: URL
    opts: RequestInit

    statusCode?: number
    body?: unknown
    code?: string

    constructor (err: Error, url: URL, opts: RequestInit) {
        let message = err.message
        if (err.message === 'fetch failed') {
            message = `Failed to fetch [${opts.method}] ${url.href}: please make sure you have a WebDriver compatible server running on ${url.origin}`
        }

        super(message)
        this.url = url
        this.opts = opts

        const errorCode = typeof err.cause === 'object' && err.cause && 'code' in err.cause && typeof err.cause.code === 'string'
            ? err.cause.code
            : 'code' in err && typeof err.code === 'string'
                ? err.code
                : undefined
        if (errorCode) {
            this.code = errorCode
            this.message = errorCode === 'UND_ERR_CONNECT_TIMEOUT'
                ? 'Request timed out! Consider increasing the "connectionRetryTimeout" option.'
                : 'Request failed with error code ' + errorCode
        }

        this.message = this.computeErrorMessage()
    }
}

export class WebDriverResponseError extends WebDriverError {
    url: URL
    opts: RequestInit
    constructor (response: unknown, url: URL, opts: RequestInit) {
        const errorObj: { message?: string, error?: string, class?: string, name?: string } = !response || typeof response !== 'object' || !('body' in response) || !response.body
            ? new Error('Response has empty body')
            : typeof response.body === 'string' && response.body.length
                ? new Error(response.body)
                : typeof response.body !== 'object'
                    ? new Error('Unknown error')
                    : 'value' in response.body && response.body.value
                        ? response.body.value
                        : response.body

        /**
         * e.g. in Firefox or Safari, error are following the following structure:
         * ```
         * {
         *   value: {
         *     error: '...',
         *     message: '...',
         *     stacktrace: '...'
         *   }
         * }
         * ```
         */
        let errorMessage = errorObj.message || errorObj.error || errorObj.class || 'unknown error'

        /**
         * Improve Chromedriver's error message for an invalid selector
         *
         * Chrome:
         *  error: 'invalid argument'
         * message: 'invalid argument: invalid locator\n  (Session info: chrome=122.0.6261.94)'
         * Firefox:
         *  error: 'invalid selector'
         *  message: 'Given xpath expression "//button" is invalid: NotSupportedError: Operation is not supported'
         * Safari:
         *  error: 'timeout'
         *  message: ''
         */
        if (typeof errorMessage === 'string' && errorMessage.includes('invalid locator')) {
            const requestOptions = opts.body as unknown as { using: string; value: string }
            errorMessage = (
                `The selector "${requestOptions.value}" used with strategy "${requestOptions.using}" is invalid!`
            )
        }

        super(errorMessage)
        if (errorObj.error) {
            this.name = errorObj.error
        } else if (errorMessage && errorMessage.includes('stale element reference')) {
            this.name = 'stale element reference'
        } else {
            this.name = errorObj.name || 'WebDriver Error'
        }

        Error.captureStackTrace(this, WebDriverResponseError)
        this.url = url
        this.opts = opts
        this.message = this.computeErrorMessage()
    }
}