🏡 index : quine-global/webdriverio.git

import type { Capabilities, Services, FunctionProperties, ThenArg } from '@wdio/types'
import type { Browser as PuppeteerBrowser } from 'puppeteer-core/lib/esm/puppeteer/api/Browser.js'

import CommandHandler from './commands.js'
import type Auditor from './auditor.js'
import { setUnsupportedCommand, getLighthouseDriver } from './utils.js'
import { DEFAULT_THROTTLE_STATE, NETWORK_STATES } from './constants.js'
import type { DevtoolsConfig, EnablePerformanceAuditsOptions, PWAAudits } from './types.js'

export default class DevToolsService implements Services.ServiceInstance {
    private _command: CommandHandler[] = []
    private _browser?: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser

    constructor (private _options: DevtoolsConfig) {}

    async before (
        caps: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities,
        specs: string[],
        browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser
    ) {
        this._browser = browser
        return await this._setupHandler()
    }

    async onReload () {
        if (!this._browser) {
            return
        }

        return this._setupHandler()
    }

    async beforeCommand (commandName: string, params: unknown[]) {
        return Promise.all(this._command.map(async c => await c._beforeCmd(commandName, params)))
    }

    async afterCommand (commandName: string) {
        if (commandName === 'switchToWindow') {
            await this._setupHandler()
        }

        return Promise.all(this._command.map(async c => await c._afterCmd(commandName)))
    }

    /**
     * set flag to run performance audits for page transitions
     */
    _enablePerformanceAudits ({ networkThrottling, cpuThrottling, cacheEnabled, formFactor }: EnablePerformanceAuditsOptions = DEFAULT_THROTTLE_STATE) {
        if (!NETWORK_STATES[networkThrottling]) {
            throw new Error(`Network throttling profile "${networkThrottling}" is unknown, choose between ${Object.keys(NETWORK_STATES).join(', ')}`)
        }

        if (typeof cpuThrottling !== 'number') {
            throw new Error(`CPU throttling rate needs to be typeof number but was "${typeof cpuThrottling}"`)
        }

        if (this._command.length === 1) {
            this._command[0].enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor })
        } else {
            for (const c of this._command) {
                c.enablePerformanceAudits({ networkThrottling, cpuThrottling, cacheEnabled, formFactor })
            }
        }
    }

    /**
     * custom command to disable performance audits
     */
    _disablePerformanceAudits () {
        if (this._command.length === 1) {
            this._command[0].disablePerformanceAudits()
        } else {
            for (const c of this._command) {
                c.disablePerformanceAudits()
            }
        }
    }

    async _setThrottlingProfile(
        networkThrottling = DEFAULT_THROTTLE_STATE.networkThrottling,
        cpuThrottling: number = DEFAULT_THROTTLE_STATE.cpuThrottling,
        cacheEnabled: boolean = DEFAULT_THROTTLE_STATE.cacheEnabled
    ) {
        if (this._command.length === 1) {
            this._command[0].setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled)
        } else {
            for (const c of this._command) {
                c.setThrottlingProfile(networkThrottling, cpuThrottling, cacheEnabled)
            }
        }
    }

    async _checkPWA (auditsToBeRun?: PWAAudits[]) {
        if (this._command.length === 1) {
            return await this._command[0].checkPWA(auditsToBeRun)
        }
        return Promise.all(this._command.map(async c => await c.checkPWA(auditsToBeRun)))
    }

    async _setupHandler () {
        if (!this._browser) {
            return
        }

        /**
         * In case of switchToWindow, needs to not add more commands to the array
         */
        this._command.length = 0

        /**
         * To avoid if-else, gather all browser instances into an array
         */
        const browsers = Object.keys(this._browser).includes('sessionId') ?
            [this._browser] :
            (this._browser as WebdriverIO.MultiRemoteBrowser).instances.map(i => (this._browser as WebdriverIO.MultiRemoteBrowser).getInstance(i))

        for (const browser of browsers) {
            const puppeteer = await (browser as WebdriverIO.Browser).getPuppeteer().catch(() => undefined) as unknown as PuppeteerBrowser
            if (!puppeteer) {
                return setUnsupportedCommand(browser as WebdriverIO.Browser)
            }

            const url = await (browser as WebdriverIO.Browser).getUrl()
            const target = url !== 'data:,'
                ? await puppeteer.waitForTarget(
                    async (t) => (
                        t.url().includes(url) &&
                        !t.url().includes('BiDi-CDP Mapper') &&
                        Boolean(await t.page())
                    )
                )
                : await puppeteer.waitForTarget(
                    async (t) => (
                        t.type() === 'page' ||
                        // @ts-expect-error
                        Boolean(t._getTargetInfo().browserContextId) &&
                        !!(await t.page())
                    )
                )

            if (!target) {
                throw new Error('No page target found')
            }

            const page = await target.page()
            if (!page) {
                throw new Error('No page found')
            }

            const session = await target.createCDPSession()
            const driver = await getLighthouseDriver(session, target)

            const cmd = new CommandHandler(session, page, driver, this._options, browser)
            await cmd._initCommand()
            this._command.push(cmd)
        }

        this._browser.addCommand('enablePerformanceAudits', this._enablePerformanceAudits.bind(this))
        this._browser.addCommand('disablePerformanceAudits', this._disablePerformanceAudits.bind(this))
        this._browser.addCommand('checkPWA', this._checkPWA.bind(this))
    }
}

export * from './types.js'

type CommandHandlerCommands = FunctionProperties<CommandHandler>
type AuditorCommands = Omit<FunctionProperties<Auditor>, '_audit' | '_auditPWA' | 'updateCommands'>

/**
 * ToDo(Christian): use key remapping with TS 4.1
 * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types
 */
interface BrowserExtension extends CommandHandlerCommands, AuditorCommands {}

export type BrowserExtensionSync = {
    [K in keyof BrowserExtension]: (...args: Parameters<BrowserExtension[K]>) => ThenArg<ReturnType<BrowserExtension[K]>>
}

declare global {
    namespace WebdriverIO {
        interface ServiceOption extends DevtoolsConfig {}
    }

    namespace WebdriverIO {
        interface Browser extends BrowserExtension { }
        interface MultiRemoteBrowser extends BrowserExtension { }
    }
}