-
Notifications
You must be signed in to change notification settings - Fork 4.6k
E2E Test Utils: Add new fixtures for performance metrics #52993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a48f6dc
aaaf0e4
dd8a12c
81843ce
fb27c7a
5594c52
26cc7d4
f7c6a3a
a61dcb1
24c9caa
b0a6ac3
1364564
068e63a
3c2ba72
fae4e49
360e9ae
c5243ef
f0d53e1
7b64157
581ef5d
c4951e6
5a16e43
fc5b5fa
bed3ea6
d607c81
2eb1b9a
db123bf
30b6d47
f77e26f
918f630
a3b4df8
7c7cdce
18fbdc5
85efcc8
e36f502
7b1c83c
ded25ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import type { Page } from '@playwright/test'; | ||
| import * as lighthouse from 'lighthouse/core/index.cjs'; | ||
|
|
||
| export class Lighthouse { | ||
| constructor( | ||
| public readonly page: Page, | ||
| public readonly port: number | ||
| ) { | ||
| this.page = page; | ||
| this.port = port; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the Lighthouse report for the current URL. | ||
| * | ||
| * Runs several Lighthouse audits in a separate browser window and returns | ||
| * the summary. | ||
| */ | ||
| async getReport() { | ||
| // From https://github.com/GoogleChrome/lighthouse/blob/d149e9c1b628d5881ca9ca451278d99ff1b31d9a/core/config/default-config.js#L433-L503 | ||
| const audits = { | ||
| 'largest-contentful-paint': 'LCP', | ||
| 'total-blocking-time': 'TBT', | ||
| interactive: 'TTI', | ||
| 'cumulative-layout-shift': 'CLS', | ||
| 'experimental-interaction-to-next-paint': 'INP', | ||
| }; | ||
|
|
||
| const report = await lighthouse( | ||
| this.page.url(), | ||
| { port: this.port }, | ||
| { | ||
| extends: 'lighthouse:default', | ||
| settings: { | ||
| // "provided" means no throttling. | ||
| // TODO: Make configurable. | ||
| throttlingMethod: 'provided', | ||
| // Default is "mobile". | ||
| // See https://github.com/GoogleChrome/lighthouse/blob/main/docs/emulation.md | ||
| // TODO: Make configurable. | ||
| formFactor: 'desktop', | ||
| screenEmulation: { | ||
| disabled: true, | ||
| }, | ||
| // Speeds up the report. | ||
| disableFullPageScreenshot: true, | ||
| // Only run certain audits to speed things up. | ||
| onlyAudits: Object.keys( audits ), | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| const result: Record< string, number > = {}; | ||
|
|
||
| if ( ! report ) { | ||
| return result; | ||
| } | ||
|
|
||
| const { lhr } = report; | ||
|
|
||
| for ( const [ audit, acronym ] of Object.entries( audits ) ) { | ||
| result[ acronym ] = lhr.audits[ audit ]?.numericValue || 0; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import type { Page } from '@playwright/test'; | ||
|
|
||
| export class Metrics { | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
felixarntz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| constructor( public readonly page: Page ) { | ||
| this.page = page; | ||
| } | ||
|
|
||
| /** | ||
| * Returns durations from the Server-Timing header. | ||
| * | ||
| * @param fields Optional fields to filter. | ||
| */ | ||
| async getServerTiming( fields: string[] = [] ) { | ||
| return this.page.evaluate< Record< string, number >, string[] >( | ||
| ( f: string[] ) => | ||
| ( | ||
| performance.getEntriesByType( | ||
| 'navigation' | ||
| ) as PerformanceNavigationTiming[] | ||
| )[ 0 ].serverTiming.reduce( | ||
| ( acc, entry ) => { | ||
| if ( f.length === 0 || f.includes( entry.name ) ) { | ||
| acc[ entry.name ] = entry.duration; | ||
| } | ||
| return acc; | ||
| }, | ||
| {} as Record< string, number > | ||
| ), | ||
| fields | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Returns time to first byte (TTFB) using the Navigation Timing API. | ||
| * | ||
| * @see https://web.dev/ttfb/#measure-ttfb-in-javascript | ||
| * | ||
| * @return {Promise<number>} TTFB value. | ||
| */ | ||
| async getTimeToFirstByte() { | ||
| return this.page.evaluate< number >( () => { | ||
| const { responseStart, startTime } = ( | ||
| performance.getEntriesByType( | ||
| 'navigation' | ||
| ) as PerformanceNavigationTiming[] | ||
| )[ 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 {Promise<number>} LCP value. | ||
| */ | ||
| async getLargestContentfulPaint() { | ||
| return this.page.evaluate< number >( | ||
| () => | ||
| 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 {Promise<number>} CLS value. | ||
| */ | ||
| async getCumulativeLayoutShift() { | ||
| return this.page.evaluate< number >( | ||
| () => | ||
| new Promise( ( resolve ) => { | ||
| let CLS = 0; | ||
|
|
||
| new PerformanceObserver( ( l ) => { | ||
| const entries = l.getEntries() as LayoutShift[]; | ||
|
|
||
| entries.forEach( ( entry ) => { | ||
| if ( ! entry.hadRecentInput ) { | ||
| CLS += entry.value; | ||
| } | ||
| } ); | ||
|
|
||
| resolve( CLS ); | ||
| } ).observe( { | ||
| type: 'layout-shift', | ||
| buffered: true, | ||
| } ); | ||
| } ) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| "incremental": false, | ||
| "composite": false, | ||
| "module": "CommonJS", | ||
| "moduleResolution": "node16", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensures dynamic imports are left untouched by TypeScript, otherwise it would transform them into Dynamic import can be used to import ESM files in CJS environments.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this should be the default now in |
||
| "types": [ "node" ], | ||
| "rootDir": "src", | ||
| "noEmit": false, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| diff --git a/node_modules/lighthouse/core/index.d.cts b/node_modules/lighthouse/core/index.d.cts | ||
| index 1c399e1..23c3d1f 100644 | ||
| --- a/node_modules/lighthouse/core/index.d.cts | ||
| +++ b/node_modules/lighthouse/core/index.d.cts | ||
| @@ -1,4 +1,6 @@ | ||
| export = lighthouse; | ||
| /** @type {import('./index.js')['default']} */ | ||
| +// Otherwise TS is confused when using ES types in CJS. | ||
| +// @ts-ignore | ||
swissspidy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| declare const lighthouse: typeof import("./index.js")['default']; | ||
| //# sourceMappingURL=index.d.cts.map | ||
Uh oh!
There was an error while loading. Please reload this page.