🏡 index : quine-global/webdriverio.git

import type { CDPSession } from 'puppeteer-core/lib/esm/puppeteer/api/CDPSession.js'
import type { Protocol } from 'devtools-protocol'

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

interface RequestLog {
    id?: string
    url?: string
    requests: Request[]
}

interface Request {
    id: string,
    url: string,
    method: string
    loaderId?: string
    statusCode?: number
    requestHeaders?: Protocol.Network.Headers
    responseHeaders?: Protocol.Network.Headers
    timing?: Protocol.Network.ResourceTiming
    type?: Protocol.Network.ResourceType
    redirect?: {
        url: string
        statusCode: number
        requestHeaders?: Protocol.Network.Headers
        responseHeaders?: Protocol.Network.Headers
        timing?: Protocol.Network.ResourceTiming
    }
}

export interface RequestPayload {
    size: number
    encoded: number
    count: number
}

export default class NetworkHandler {
    requestLog: RequestLog = { requests: [] }
    requestTypes: {
        [key in Protocol.Network.ResourceType]?: RequestPayload
    } = {}
    cachedFirstRequest?: Request

    constructor (session: CDPSession) {
        session.on('Network.dataReceived', this.onDataReceived.bind(this))
        session.on('Network.responseReceived', this.onNetworkResponseReceived.bind(this))
        session.on('Network.requestWillBeSent', this.onNetworkRequestWillBeSent.bind(this))
        session.on('Page.frameNavigated', this.onPageFrameNavigated.bind(this))
    }

    findRequest (params: Protocol.Network.DataReceivedEvent | Protocol.Network.ResponseReceivedEvent) {
        let request = this.requestLog.requests.find((req) => req.id === params.requestId)

        /**
         * If no match is found, check if the corresponding request is the cached first request
         */
        if (!request && this.cachedFirstRequest && this.cachedFirstRequest.id === params.requestId) {
            request = this.cachedFirstRequest
        }
        return request
    }

    onDataReceived (params: Protocol.Network.DataReceivedEvent) {
        const request = this.findRequest(params)

        /**
         * ensure that
         *  - a requestWillBeSent event was triggered before
         *  - the request type is accurate and known (sometimes this is not the case when `Network.requestWillBeSent` is triggered)
         */
        if (!request || !request.type || !this.requestTypes[request.type]) {
            return
        }

        const type = request.type
        const requestType = this.requestTypes[type] || ({} as RequestPayload)
        requestType.size += params.dataLength
        requestType.encoded += params.encodedDataLength
    }

    onNetworkResponseReceived (params: Protocol.Network.ResponseReceivedEvent) {
        const request = this.findRequest(params)
        /**
         * ensure that a requestWillBeSent event was triggered before
         */
        if (!request) {
            return
        }

        request.statusCode = params.response.status
        request.requestHeaders = params.response.requestHeaders
        request.responseHeaders = params.response.headers
        request.timing = params.response.timing
        request.type = params.type
    }

    onNetworkRequestWillBeSent (params: Protocol.Network.RequestWillBeSentEvent) {
        let isFirstRequestOfFrame = false

        if (
            /**
             * A new page was opened when request type is a document.
             * The first request is sent before the Page.frameNavigated event is triggered,
             * so this request must be cached to be able to add it to the requestLog later.
             */
            params.type === 'Document' &&
            /**
             * ensure that only page loads triggered by non scripts (devtools only) are considered
             * new page loads
             */
            params.initiator.type === 'other' &&
            /**
             * ignore pages not initated by the user
             */
            IGNORED_URLS.filter((url) => params.request.url.startsWith(url)).length === 0
        ) {
            isFirstRequestOfFrame = true

            /**
             * reset the request type sizes
             */
            this.requestTypes = {}
        }

        const log: Request = {
            id: params.requestId,
            url: params.request.url,
            method: params.request.method
        }

        if (params.redirectResponse) {
            log.redirect = {
                url: params.redirectResponse.url,
                statusCode: params.redirectResponse.status,
                requestHeaders: params.redirectResponse.requestHeaders,
                responseHeaders: params.redirectResponse.headers,
                timing: params.redirectResponse.timing
            }
        }

        if (params.type) {
            const requestType = this.requestTypes[params.type]
            if (!requestType) {
                this.requestTypes[params.type] = {
                    size: 0,
                    encoded: 0,
                    count: 1
                }
            } else if (requestType) {
                requestType.count++
            }
        }

        if (isFirstRequestOfFrame) {
            log.loaderId = params.loaderId
            this.cachedFirstRequest = log
            return
        }
        return this.requestLog.requests.push(log)
    }

    onPageFrameNavigated (params: Protocol.Page.FrameNavigatedEvent) {
        /**
         * Only create a requestLog for pages that don't have a parent frame.
         * I.e. iframes are ignored
         */
        if (!params.frame.parentId && IGNORED_URLS.filter((url) => params.frame.url.startsWith(url)).length === 0) {
            this.requestLog = {
                id: params.frame.loaderId,
                url: params.frame.url,
                requests: []
            }
            /**
             * Add the first request that was cached before the actual requestLog could be created
             */
            if (this.cachedFirstRequest && this.cachedFirstRequest.loaderId === params.frame.loaderId) {
                /**
                 * Delete the loaderId of the first request so that all request data has the same structure
                 */
                delete this.cachedFirstRequest.loaderId
                this.requestLog.requests.push(this.cachedFirstRequest)
                this.cachedFirstRequest = undefined
            }
        }
    }
}