UNPKG

@wordpress/e2e-test-utils-playwright

Version:
258 lines 9.78 kB
"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-expect-error This is valid but web-vitals does not register the global types. window.webVitals.onCLS(reportVitals); // @ts-expect-error This is valid but web-vitals does not register the global types. window.webVitals.onFCP(reportVitals); // @ts-expect-error This is valid but web-vitals does not register the global types. window.webVitals.onFID(reportVitals); // @ts-expect-error This is valid but web-vitals does not register the global types. window.webVitals.onINP(reportVitals); // @ts-expect-error This is valid but web-vitals does not register the global types. window.webVitals.onLCP(reportVitals); // @ts-expect-error This is valid but web-vitals does not register the global types. 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( // @ts-expect-error This is valid but web-vitals does not register the global types. () => 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