🏡 index : quine-global/webdriverio.git

import { getBrowserObject } from '@wdio/utils'
import type { WebDriverResultEvent } from 'webdriver'

import Timer from '../../utils/Timer.js'
import type { WaitUntilOptions } from '../../types.js'

/**
 * This wait command is your universal weapon if you want to wait on something. It expects a condition
 * and waits until that condition is fulfilled with a truthy value to be returned.
 *
 * A common example is to wait until a certain element contains a certain text (see example).
 *
 * @alias browser.waitUntil
 * @param {Function}          condition  condition to wait on until returning a truthy value
 * @param {WaitUntilOptions=} options    command options
 * @param {Number=}           options.timeout     time in ms (default set based on [`waitforTimeout`](/docs/configuration#waitfortimeout) config value)
 * @param {String=}           options.timeoutMsg  error message to throw when waitUntil times out
 * @param {Number=}           options.interval    interval between condition checks (default set based on [`waitforInterval`](/docs/configuration#waitforinterval) config value)
 * @return {Boolean} true if condition is fulfilled
 * @uses utility/pause
 * @example https://github.com/webdriverio/example-recipes/blob/0bfb2b8d212b627a2659b10f4449184b657e1d59/waitUntil/index.html#L3-L8
 * @example https://github.com/webdriverio/example-recipes/blob/0c9252b0a4f7e18a34cece74e5798c1fe464c120/waitUntil/waitUntilExample.js#L16-L24
 * @type utility
 *
 */
export function waitUntil<ReturnValue>(
    this: WebdriverIO.Browser | WebdriverIO.Element,
    condition: () => ReturnValue | Promise<ReturnValue>,
    {
        timeout = this.options.waitforTimeout,
        interval = this.options.waitforInterval,
        timeoutMsg
    }: Partial<WaitUntilOptions> = {}
): Promise<Exclude<ReturnValue, false | 0 | '' | null | undefined>> {
    if (typeof condition !== 'function') {
        throw new Error('Condition is not a function')
    }

    /**
     * ensure that timeout and interval are set properly
     */
    if (typeof timeout !== 'number') {
        timeout = this.options.waitforTimeout as number
    }

    if (typeof interval !== 'number') {
        interval = this.options.waitforInterval as number
    }

    /**
     * abort the Timer if the session is deleted during the wait
     */
    const browser = getBrowserObject(this)
    const abort = new AbortController()
    const abortOnSessionEnd = (result: WebDriverResultEvent) => {
        if (result.command === 'deleteSession') {
            abort.abort()
        }
    }
    browser.on('result', abortOnSessionEnd)

    const fn = condition.bind(this)
    const timer = new Timer(interval as number, timeout as number, fn, true, abort.signal)
    return timer.catch<Exclude<ReturnValue, false | 0 | '' | null | undefined>>((e: Error) => {
        if (e.message === 'timeout') {
            if (typeof timeoutMsg === 'string') {
                throw new Error(timeoutMsg)
            }
            throw new Error(`waitUntil condition timed out after ${timeout}ms`)
        }

        const err = new Error(`waitUntil condition failed with the following reason: ${e && e.message || e}`)
        const origStack = e.stack
        if (!origStack || !err.stack) {
            throw err
        }

        /**
         * sanitize error message and clean up stack trace
         */
        const [errMsg, ...waitUntilErrorStackLines] = err.stack.split('\n')
        err.stack = [
            errMsg,
            ...(origStack.split('\n').slice(1)),
            '    ---',
            ...waitUntilErrorStackLines
        ].filter((errorLine) => (
            !errorLine.includes('/node_modules/webdriverio/') &&
            !errorLine.includes('/node_modules/@wdio/')
        )).join('\n')

        throw err
    }).finally(() => {
        browser.off('result', abortOnSessionEnd)
    })
}