Skip to content
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

E2E Test Utils: Add new fixtures for performance metrics #52993

Merged
merged 37 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a48f6dc
Add Playwright metrics fixtures
swissspidy Jul 26, 2023
aaaf0e4
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 9, 2023
dd8a12c
Fix version discrepancy
swissspidy Aug 9, 2023
81843ce
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 9, 2023
fb27c7a
Fix doc block
swissspidy Aug 9, 2023
5594c52
Fix ESM issues
swissspidy Aug 9, 2023
26cc7d4
Remove comments
swissspidy Aug 9, 2023
f7c6a3a
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 9, 2023
a61dcb1
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 11, 2023
24c9caa
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 11, 2023
b0a6ac3
Downgrade again to 1.32.0
swissspidy Aug 13, 2023
1364564
Update docblocks
swissspidy Aug 13, 2023
068e63a
Remove unneeded entry
swissspidy Aug 13, 2023
3c2ba72
Revert "Remove unneeded entry"
swissspidy Aug 13, 2023
fae4e49
Remove unnecessary peer dependency
swissspidy Aug 14, 2023
360e9ae
Add comments
swissspidy Aug 14, 2023
c5243ef
Rename variables
swissspidy Aug 14, 2023
f0d53e1
TTFB measures the elapsed time between startTime and responseStart
swissspidy Aug 14, 2023
7b64157
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 14, 2023
581ef5d
Simplify `getTimeToFirstByte` util
swissspidy Aug 14, 2023
c4951e6
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 16, 2023
5a16e43
Combine `lighthouseBrowser` & `lighthousePort` fixture
swissspidy Aug 16, 2023
fc5b5fa
Add comment
swissspidy Aug 16, 2023
bed3ea6
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 16, 2023
d607c81
Disable throttling in Lighthouse
swissspidy Aug 17, 2023
2eb1b9a
Disable mobile emulation
swissspidy Aug 17, 2023
db123bf
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 17, 2023
30b6d47
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 18, 2023
f77e26f
disableFullPageScreenshot
swissspidy Aug 18, 2023
918f630
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 21, 2023
a3b4df8
Split into two classes
swissspidy Aug 21, 2023
7c7cdce
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Aug 31, 2023
18fbdc5
Use patch-package instead
swissspidy Aug 31, 2023
85efcc8
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Sep 1, 2023
e36f502
Merge branch 'trunk' into add/playwright-utils-metrics
swissspidy Sep 19, 2023
7b1c83c
Add explanatory comment
swissspidy Sep 19, 2023
ded25ba
Lint fixes
swissspidy Sep 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,026 changes: 1,996 additions & 30 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@
"node-watch": "0.7.0",
"npm-run-all": "4.1.5",
"patch-package": "6.2.2",
"playwright": "1.32.0",
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
"postcss": "8.4.16",
"postcss-loader": "6.2.1",
"prettier": "npm:wp-prettier@2.8.5",
Expand Down
5 changes: 4 additions & 1 deletion packages/e2e-test-utils-playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
"@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": {
"@playwright/test": ">=1"
"@playwright/test": ">=1",
"playwright": ">=1"
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
},
"publishConfig": {
"access": "public"
Expand Down
1 change: 1 addition & 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,5 @@ export { Admin } from './admin';
export { Editor } from './editor';
export { PageUtils } from './page-utils';
export { RequestUtils } from './request-utils';
export { Metrics } from './metrics';
export { test, expect } from './test';
160 changes: 160 additions & 0 deletions packages/e2e-test-utils-playwright/src/metrics/index.ts
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* External dependencies
*/
import type { Page } from '@playwright/test';
import * as lighthouse from 'lighthouse/core/index.cjs';

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, public readonly port: number ) {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
this.page = page;
this.port = port;
}

/**
* 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 >(
() =>
new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
const [ pageNav ] = entryList.getEntriesByType(
'navigation'
) as PerformanceNavigationTiming[];

resolve( pageNav.responseStart );
} ).observe( {
type: 'navigation',
buffered: true,
} );
} )
);
}

/**
* 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,
} );
} )
);
}

/**
* Returns the Lighthouse report for the current URL.
*
* Runs several Lighthouse audits in a separate browser window and returns
* the summary.
*/
async getLighthouseReport() {
// 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: {
// 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;
}
}
28 changes: 27 additions & 1 deletion packages/e2e-test-utils-playwright/src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import * as path from 'path';
import { test as base, expect } from '@playwright/test';
import type { ConsoleMessage } from '@playwright/test';
import { type Browser, chromium } from 'playwright';
import * as getPort from 'get-port';

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

const STORAGE_STATE_PATH =
process.env.STORAGE_STATE_PATH ||
Expand Down Expand Up @@ -103,9 +105,12 @@ const test = base.extend<
editor: Editor;
pageUtils: PageUtils;
snapshotConfig: void;
metrics: Metrics;
},
{
requestUtils: RequestUtils;
port: number;
browser: Browser;
}
>( {
admin: async ( { page, pageUtils }, use ) => {
Expand Down Expand Up @@ -140,6 +145,27 @@ const test = base.extend<
},
{ scope: 'worker', auto: true },
],
port: [
async ( {}, use ) => {
const port = await getPort();
await use( port );
},
{ scope: 'worker' },
],
browser: [
async ( { port }, use ) => {
const browser = await chromium.launch( {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
args: [ `--remote-debugging-port=${ port }` ],
} );
await use( browser );

await browser.close();
},
{ scope: 'worker' },
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
],
metrics: async ( { page, port }, use ) => {
await use( new Metrics( page, port ) );
},
} );

export { test, expect };
17 changes: 17 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,23 @@ declare global {
// Silence the warning for `window.wp` in Playwright's evaluate functions.
wp: any;
}

interface LayoutShiftAttribution {
readonly node: Node;
readonly previousRect: DOMRectReadOnly;
readonly currentRect: DOMRectReadOnly;
readonly toJSON: () => string;
}
interface LayoutShift extends PerformanceEntry {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
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 {};
2 changes: 2 additions & 0 deletions packages/e2e-test-utils-playwright/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"incremental": false,
"composite": false,
"module": "CommonJS",
"moduleResolution": "node16",
Copy link
Member Author

Choose a reason for hiding this comment

The 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 require() calls.

Dynamic import can be used to import ESM files in CJS environments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be the default now in tsconfig.base.json?

"skipLibCheck": true,
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
"types": [ "node" ],
"rootDir": "src",
"noEmit": false,
Expand Down
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 @@ -24,44 +24,15 @@ test.describe( 'Front End Performance', () => {
await requestUtils.activateTheme( 'twentytwentyone' );
} );

test( 'Report TTFB, LCP, and LCP-TTFB', async ( { page } ) => {
test( 'Report TTFB, LCP, and LCP-TTFB', async ( { page, metrics } ) => {
let i = 16;
while ( i-- ) {
// Go to the base URL.
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
35 changes: 3 additions & 32 deletions test/performance/specs/front-end-classic-theme.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,15 @@ test.describe( 'Front End Performance', () => {
saveResultsFile( __filename, results );
} );

test( 'Measure TTFB, LCP, and LCP-TTFB', async ( { page } ) => {
test( 'Measure TTFB, LCP, and LCP-TTFB', async ( { page, metrics } ) => {
let i = 16;
while ( i-- ) {
// Go to the base URL.
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