🏡 index : quine-global/webdriverio.git

import logger from '@wdio/logger'
import type { FakeTimerInstallOpts, InstalledClock, install } from '@sinonjs/fake-timers'

const log = logger('webdriverio:ClockManager')

declare global {
    interface Window {
        __clock: InstalledClock
        __wdio_sinon: {
            install: typeof install
        }
    }
}

function installFakeTimers (options: FakeTimerInstallOpts) {
    window.__clock = window.__wdio_sinon.install(options)
}

function uninstallFakeTimers () {
    window.__clock.uninstall()
}

declare const WDIO_FAKER_SCRIPT: string
const fakerScript = WDIO_FAKER_SCRIPT

export class ClockManager {
    #browser: WebdriverIO.Browser
    #resetFn: (() => Promise<unknown>) = () => Promise.resolve()
    #isInstalled = false

    constructor(browser: WebdriverIO.Browser) {
        this.#browser = browser
    }

    /**
     * Install fake timers on the browser. If you call the `emulate` command, WebdriverIO will automatically install
     * the fake timers for you. You can use this method to re-install the fake timers if you have called `restore`.
     *
     * @param options {FakeTimerInstallOpts} Options to pass to the fake clock
     * @returns {Promise<void>}
     */
    async install(options?: FakeTimerInstallOpts) {
        if (this.#isInstalled) {
            return log.warn('Fake timers are already installed')
        }

        /**
         * skip if we are not running in a Node.js environment
         */
        if (globalThis.window) {
            return
        }

        /**
         * load Node.js specific modules dynamically to avoid loading them in the browser
         */
        const emulateOptions = options || {} as FakeTimerInstallOpts
        const functionDeclaration = fakerScript
        const installOptions: FakeTimerInstallOpts = {
            ...emulateOptions,
            now: emulateOptions.now && (emulateOptions.now instanceof Date) ? emulateOptions.now.getTime() : emulateOptions.now
        }

        const [, libScript, restoreInstallScript] = await Promise.all([
            /**
             * install fake timers for current ex
             */
            this.#browser.executeScript(`return (${functionDeclaration}).apply(null, arguments)`, []).then(() => (
                this.#browser.execute(installFakeTimers, installOptions)
            )),
            /**
             * add preload script to to emulate clock for upcoming page loads
             */
            this.#browser.scriptAddPreloadScript({ functionDeclaration }),
            this.#browser.addInitScript(installFakeTimers, installOptions)
        ])
        this.#resetFn = async () => Promise.all([
            this.#browser.scriptRemovePreloadScript({ script: libScript.script }),
            this.#browser.execute(uninstallFakeTimers),
            restoreInstallScript
        ])
        this.#isInstalled = true
    }

    /**
     * Restore all overridden native functions. This is automatically called between tests, so should not
     * generally be needed.
     *
     * ```ts
     * it('should restore the clock', async () => {
     *   console.log(new Date()) // returns e.g. 1722560447102
     *
     *   const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
     *
     *   await clock.restore()
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1722560447102
     * })
     * ```
     *
     * @returns {Promise<void>}
     */
    async restore() {
        await this.#resetFn()
        this.#isInstalled = false
    }

    /**
     * Move the clock the specified number of `milliseconds`. Any timers within the affected range of time will be called.
     * @param ms {number} The number of milliseconds to move the clock.
     *
     * ```ts
     * it('should move the clock', async () => {
     *   console.log(new Date()) // returns e.g. 1722560447102
     *
     *   const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
     *
     *   await clock.tick(1000)
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1618383601000
     * })
     * ```
     *
     * @param    {number}  ms  The number of milliseconds to move the clock.
     * @returns  {Promise<void>}
     */
    async tick(ms: number) {
        await this.#browser.execute((ms) => window.__clock.tick(ms), ms)
    }

    /**
     * Change the system time to the new now. Now can be a timestamp, date object, or not passed in which defaults
     * to 0. No timers will be called, nor will the time left before they trigger change.
     *
     * ```ts
     * it('should set the system time', async () => {
     *   const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
     *
     *   await clock.setSystemTime(new Date(2011, 3, 15))
     *   console.log(await browser.execute(() => new Date().getTime())) // returns 1302850800000
     * })
     * ```
     *
     * @param date {Date|number} The new date to set the system time to.
     * @returns    {Promise<void>}
     */
    async setSystemTime(date: number | Date) {
        const serializableSystemTime = date instanceof Date ? date.getTime() : date
        await this.#browser.execute((date) => window.__clock.setSystemTime(date), serializableSystemTime)
    }
}