"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Metrics = void 0;
const path_1 = require("path");
class Metrics {
    browser;
    page;
    trace;
    webVitals = {};
    constructor({ page }) {
        this.page = page;
        this.browser = page.context().browser();
        this.trace = { traceEvents: [] };
    }
    /**
     * Returns durations from the Server-Timing header.
     *
     * @param fields Optional fields to filter.
     */
    async getServerTiming(fields = []) {
        return this.page.evaluate((f) => performance.getEntriesByType('navigation')[0].serverTiming.reduce((acc, entry) => {
            if (f.length === 0 || f.includes(entry.name)) {
                acc[entry.name] = entry.duration;
            }
            return acc;
        }, {}), fields);
    }
    /**
     * Returns time to first byte (TTFB) using the Navigation Timing API.
     *
     * @see https://web.dev/ttfb/#measure-ttfb-in-javascript
     *
     * @return TTFB value.
     */
    async getTimeToFirstByte() {
        return await this.page.evaluate(() => {
            const { responseStart, startTime } = performance.getEntriesByType('navigation')[0];
            return responseStart - startTime;
        });
    }
    /**
     * Returns the Largest Contentful Paint (LCP) value using the dedicated API.
     *
     * @see https://w3c.github.io/largest-contentful-paint/
     * @see https://web.dev/lcp/#measure-lcp-in-javascript
     *
     * @return LCP value.
     */
    async getLargestContentfulPaint() {
        return await this.page.evaluate(() => new Promise((resolve) => {
            new PerformanceObserver((entryList) => {
                const entries = entryList.getEntries();
                // The last entry is the largest contentful paint.
                const largestPaintEntry = entries.at(-1);
                resolve(largestPaintEntry?.startTime || 0);
            }).observe({
                type: 'largest-contentful-paint',
                buffered: true,
            });
        }));
    }
    /**
     * Returns the Cumulative Layout Shift (CLS) value using the dedicated API.
     *
     * @see https://github.com/WICG/layout-instability
     * @see https://web.dev/cls/#measure-layout-shifts-in-javascript
     *
     * @return CLS value.
     */
    async getCumulativeLayoutShift() {
        return await this.page.evaluate(() => new Promise((resolve) => {
            let CLS = 0;
            new PerformanceObserver((l) => {
                const entries = l.getEntries();
                entries.forEach((entry) => {
                    if (!entry.hadRecentInput) {
                        CLS += entry.value;
                    }
                });
                resolve(CLS);
            }).observe({
                type: 'layout-shift',
                buffered: true,
            });
        }));
    }
    /**
     * Returns the loading durations using the Navigation Timing API. All the
     * durations exclude the server response time.
     *
     * @return Object with loading metrics durations.
     */
    async getLoadingDurations() {
        return await this.page.evaluate(() => {
            const [{ requestStart, responseStart, responseEnd, domContentLoadedEventEnd, loadEventEnd, },] = performance.getEntriesByType('navigation');
            const paintTimings = performance.getEntriesByType('paint');
            const firstPaintStartTime = paintTimings.find(({ name }) => name === 'first-paint').startTime;
            const firstContentfulPaintStartTime = paintTimings.find(({ name }) => name === 'first-contentful-paint').startTime;
            return {
                // Server side metric.
                serverResponse: responseStart - requestStart,
                // For client side metrics, consider the end of the response (the
                // browser receives the HTML) as the start time (0).
                firstPaint: firstPaintStartTime - responseEnd,
                domContentLoaded: domContentLoadedEventEnd - responseEnd,
                loaded: loadEventEnd - responseEnd,
                firstContentfulPaint: firstContentfulPaintStartTime - responseEnd,
                timeSinceResponseEnd: performance.now() - responseEnd,
            };
        });
    }
    /**
     * Starts Chromium tracing with predefined options for performance testing.
     *
     * @param options Options to pass to `browser.startTracing()`.
     */
    async startTracing(options = {}) {
        return await this.browser.startTracing(this.page, {
            screenshots: false,
            categories: ['devtools.timeline'],
            ...options,
        });
    }
    /**
     * Stops Chromium tracing and saves the trace.
     */
    async stopTracing() {
        const traceBuffer = await this.browser.stopTracing();
        const traceJSON = JSON.parse(traceBuffer.toString());
        this.trace = traceJSON;
    }
    /**
     * @return Durations of all traced `keydown`, `keypress`, and `keyup`
     * events.
     */
    getTypingEventDurations() {
        return [
            this.getEventDurations('keydown'),
            this.getEventDurations('keypress'),
            this.getEventDurations('keyup'),
        ];
    }
    /**
     * @return Durations of all traced `focus` and `focusin` events.
     */
    getSelectionEventDurations() {
        return [
            this.getEventDurations('focus'),
            this.getEventDurations('focusin'),
        ];
    }
    /**
     * @return Durations of all traced `click` events.
     */
    getClickEventDurations() {
        return [this.getEventDurations('click')];
    }
    /**
     * @return Durations of all traced `mouseover` and `mouseout` events.
     */
    getHoverEventDurations() {
        return [
            this.getEventDurations('mouseover'),
            this.getEventDurations('mouseout'),
        ];
    }
    /**
     * @param eventType Type of event to filter.
     * @return Durations of all events of a given type.
     */
    getEventDurations(eventType) {
        if (this.trace.traceEvents.length === 0) {
            throw new Error('No trace events found. Did you forget to call stopTracing()?');
        }
        return this.trace.traceEvents
            .filter((item) => item.cat === 'devtools.timeline' &&
            item.name === 'EventDispatch' &&
            item?.args?.data?.type === eventType &&
            !!item.dur)
            .map((item) => (item.dur ? item.dur / 1000 : 0));
    }
    /**
     * Initializes the web-vitals library upon next page navigation.
     *
     * Defaults to automatically triggering the navigation,
     * but it can also be done manually.
     *
     * @example
     * ```js
     * await metrics.initWebVitals();
     * console.log( await metrics.getWebVitals() );
     * ```
     *
     * @example
     * ```js
     * await metrics.initWebVitals( false );
     * await page.goto( '/some-other-page' );
     * console.log( await metrics.getWebVitals() );
     * ```
     *
     * @param reload Whether to force navigation by reloading the current page.
     */
    async initWebVitals(reload = true) {
        await this.page.addInitScript({
            path: (0, path_1.join)(__dirname, '../../../../node_modules/web-vitals/dist/web-vitals.umd.cjs'),
        });
        await this.page.exposeFunction('__reportVitals__', (data) => {
            const measurement = JSON.parse(data);
            this.webVitals[measurement.name] = measurement.value;
        });
        await this.page.addInitScript(() => {
            const reportVitals = (measurement) => window.__reportVitals__(JSON.stringify(measurement));
            window.addEventListener('DOMContentLoaded', () => {
                // @ts-ignore
                window.webVitals.onCLS(reportVitals);
                // @ts-ignore
                window.webVitals.onFCP(reportVitals);
                // @ts-ignore
                window.webVitals.onFID(reportVitals);
                // @ts-ignore
                window.webVitals.onINP(reportVitals);
                // @ts-ignore
                window.webVitals.onLCP(reportVitals);
                // @ts-ignore
                window.webVitals.onTTFB(reportVitals);
            });
        });
        if (reload) {
            // By reloading the page the script will be applied.
            await this.page.reload();
        }
    }
    /**
     * Returns web vitals as collected by the web-vitals library.
     *
     * If the web-vitals library hasn't been loaded on the current page yet,
     * it will be initialized with a page reload.
     *
     * Reloads the page to force web-vitals to report all collected metrics.
     *
     * @return {WebVitalsMeasurements} Web vitals measurements.
     */
    async getWebVitals() {
        // Reset values.
        this.webVitals = {};
        const hasScript = await this.page.evaluate(() => typeof window.webVitals !== 'undefined');
        if (!hasScript) {
            await this.initWebVitals();
        }
        // Trigger navigation so the web-vitals library reports values on unload.
        await this.page.reload();
        return this.webVitals;
    }
}
exports.Metrics = Metrics;
//# sourceMappingURL=index.js.map