From 252d58abab34262e01f0e94826eda4286d2cb307 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Nov 2023 15:07:17 +0100 Subject: [PATCH] E2E Utils: add support for web-vitals.js (#55660) --- package-lock.json | 18 ++- .../e2e-test-utils-playwright/package.json | 3 +- .../src/metrics/index.ts | 107 ++++++++++++++++++ .../e2e-test-utils-playwright/src/types.ts | 27 +---- 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index c655a48714ac5..9439981c29d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52358,6 +52358,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", + "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w==", + "dev": true + }, "node_modules/webdriver": { "version": "8.16.20", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.16.20.tgz", @@ -55210,7 +55216,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" }, "engines": { "node": ">=12" @@ -70317,7 +70324,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" } }, "@wordpress/e2e-tests": { @@ -97170,6 +97178,12 @@ "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", "dev": true }, + "web-vitals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", + "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w==", + "dev": true + }, "webdriver": { "version": "8.16.20", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.16.20.tgz", diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index 775e1da771331..f8d9b7cd2f017 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -37,7 +37,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" }, "peerDependencies": { "@playwright/test": ">=1" diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index 68343f6d7c482..fac05d9004bd8 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -2,6 +2,11 @@ * External dependencies */ import type { Page, Browser } from '@playwright/test'; +import { join } from 'path'; +// resolution-mode support in TypeScript 5.3 will resolve this. +// See https://devblogs.microsoft.com/typescript/announcing-typescript-5-3-beta/ +// @ts-expect-error +import type { Metric } from 'web-vitals'; type EventType = | 'click' @@ -32,11 +37,22 @@ type MetricsConstructorProps = { page: Page; }; +interface WebVitalsMeasurements { + CLS?: number; + FCP?: number; + FID?: number; + INP?: number; + LCP?: number; + TTFB?: number; +} + export class Metrics { browser: Browser; page: Page; trace: Trace; + webVitals: WebVitalsMeasurements = {}; + constructor( { page }: MetricsConstructorProps ) { this.page = page; this.browser = page.context().browser()!; @@ -273,4 +289,95 @@ export class Metrics { ) .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: join( + __dirname, + '../../../../node_modules/web-vitals/dist/web-vitals.umd.cjs' + ), + } ); + + await this.page.exposeFunction( + '__reportVitals__', + ( data: string ) => { + const measurement: Metric = JSON.parse( data ); + this.webVitals[ measurement.name ] = measurement.value; + } + ); + + await this.page.addInitScript( () => { + const reportVitals = ( measurement: unknown ) => + 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; + } } diff --git a/packages/e2e-test-utils-playwright/src/types.ts b/packages/e2e-test-utils-playwright/src/types.ts index 62e38033b73e8..3dd6b0683aa33 100644 --- a/packages/e2e-test-utils-playwright/src/types.ts +++ b/packages/e2e-test-utils-playwright/src/types.ts @@ -1,29 +1,12 @@ +/** + * External dependencies + */ declare global { interface Window { // Silence the warning for `window.wp` in Playwright's evaluate functions. wp: any; - } - - // Experimental API that is subject to change. - // See https://developer.mozilla.org/en-US/docs/Web/API/LayoutShiftAttribution - interface LayoutShiftAttribution { - readonly node: Node; - readonly previousRect: DOMRectReadOnly; - readonly currentRect: DOMRectReadOnly; - readonly toJSON: () => string; - } - - // Experimental API that is subject to change. - // See https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift - interface LayoutShift extends PerformanceEntry { - readonly duration: number; - readonly entryType: 'layout-shift'; - readonly name: 'layout-shift'; - readonly startTime: DOMHighResTimeStamp; - readonly value: number; - readonly hadRecentInput: boolean; - readonly lastInputTime: DOMHighResTimeStamp; - readonly sources: LayoutShiftAttribution[]; + // Helper function added by Metrics fixture for web-vitals.js. + __reportVitals__: ( data: string ) => void; } }