Skip to content

Commit

Permalink
E2E Test Utils: Add new fixtures for performance metrics (#52993)
Browse files Browse the repository at this point in the history
  • Loading branch information
swissspidy authored Sep 19, 2023
1 parent d3aa780 commit 8076b0f
Show file tree
Hide file tree
Showing 11 changed files with 2,212 additions and 74 deletions.
1,959 changes: 1,950 additions & 9 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/e2e-test-utils-playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@wordpress/url": "file:../url",
"change-case": "^4.1.2",
"form-data": "^4.0.0",
"get-port": "^5.1.1",
"lighthouse": "^10.4.0",
"mime": "^3.0.0"
},
"peerDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e-test-utils-playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export { Admin } from './admin';
export { Editor } from './editor';
export { PageUtils } from './page-utils';
export { RequestUtils } from './request-utils';
export { Metrics } from './metrics';
export { Lighthouse } from './lighthouse';
export { test, expect } from './test';
70 changes: 70 additions & 0 deletions packages/e2e-test-utils-playwright/src/lighthouse/index.ts
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;
}
}
111 changes: 111 additions & 0 deletions packages/e2e-test-utils-playwright/src/metrics/index.ts
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 {
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,
} );
} )
);
}
}
39 changes: 37 additions & 2 deletions packages/e2e-test-utils-playwright/src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
* External dependencies
*/
import * as path from 'path';
import { test as base, expect } from '@playwright/test';
import { test as base, expect, chromium } from '@playwright/test';
import type { ConsoleMessage } from '@playwright/test';
import * as getPort from 'get-port';

/**
* Internal dependencies
*/
import { Admin, Editor, PageUtils, RequestUtils } from './index';
import {
Admin,
Editor,
PageUtils,
RequestUtils,
Metrics,
Lighthouse,
} from './index';

const STORAGE_STATE_PATH =
process.env.STORAGE_STATE_PATH ||
Expand Down Expand Up @@ -103,9 +111,12 @@ const test = base.extend<
editor: Editor;
pageUtils: PageUtils;
snapshotConfig: void;
metrics: Metrics;
lighthouse: Lighthouse;
},
{
requestUtils: RequestUtils;
lighthousePort: number;
}
>( {
admin: async ( { page, pageUtils }, use ) => {
Expand Down Expand Up @@ -146,6 +157,30 @@ const test = base.extend<
},
{ scope: 'worker', auto: true },
],
// Spins up a new browser for use by the Lighthouse fixture
// so that Lighthouse can connect to the debugging port.
// As a worker-scoped fixture, this will only launch 1
// instance for the whole test worker, so multiple tests
// will share the same instance with the same port.
lighthousePort: [
async ( {}, use ) => {
const port = await getPort();
const browser = await chromium.launch( {
args: [ `--remote-debugging-port=${ port }` ],
} );

await use( port );

await browser.close();
},
{ scope: 'worker' },
],
lighthouse: async ( { page, lighthousePort }, use ) => {
await use( new Lighthouse( page, lighthousePort ) );
},
metrics: async ( { page }, use ) => {
await use( new Metrics( page ) );
},
} );

export { test, expect };
22 changes: 22 additions & 0 deletions packages/e2e-test-utils-playwright/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ declare global {
// 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[];
}
}

export {};
1 change: 1 addition & 0 deletions packages/e2e-test-utils-playwright/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"incremental": false,
"composite": false,
"module": "CommonJS",
"moduleResolution": "node16",
"types": [ "node" ],
"rootDir": "src",
"noEmit": false,
Expand Down
11 changes: 11 additions & 0 deletions patches/lighthouse+10.4.0.patch
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
declare const lighthouse: typeof import("./index.js")['default'];
//# sourceMappingURL=index.d.cts.map
35 changes: 3 additions & 32 deletions test/performance/specs/front-end-block-theme.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,43 +30,14 @@ test.describe( 'Front End Performance', () => {
for ( let i = 0; i < rounds; i++ ) {
test( `Measure TTFB, LCP, and LCP-TTFB (${
i + 1
} of ${ rounds })`, async ( { page } ) => {
} of ${ rounds })`, async ( { page, metrics } ) => {
// Go to the base URL.
// eslint-disable-next-line playwright/no-networkidle
await page.goto( '/', { waitUntil: 'networkidle' } );

// Take the measurements.
const [ lcp, ttfb ] = await page.evaluate( () => {
return Promise.all( [
// Measure the Largest Contentful Paint time.
// Based on https://www.checklyhq.com/learn/headless/basics-performance#largest-contentful-paint-api-largest-contentful-paint
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 );
} ).observe( {
type: 'largest-contentful-paint',
buffered: true,
} );
} ),
// Measure the Time To First Byte.
// Based on https://web.dev/ttfb/#measure-ttfb-in-javascript
new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const [ pageNav ] =
entryList.getEntriesByType( 'navigation' );

resolve( pageNav.responseStart );
} ).observe( {
type: 'navigation',
buffered: true,
} );
} ),
] );
} );
const lcp = await metrics.getLargestContentfulPaint();
const ttfb = await metrics.getTimeToFirstByte();

// Ensure the numbers are valid.
expect( lcp ).toBeGreaterThan( 0 );
Expand Down
Loading

0 comments on commit 8076b0f

Please sign in to comment.