From 18caa796e85254902e159358b6fad24d3ef0f8b0 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Thu, 21 Sep 2023 11:23:18 +0000 Subject: [PATCH] Performance Tests: Separate page setup from test (#53808) --- .../src/lighthouse/index.ts | 13 +- .../src/metrics/index.ts | 181 ++++- .../e2e-test-utils-playwright/src/test.ts | 4 +- test/performance/fixtures/index.js | 1 + test/performance/fixtures/perf-utils.ts | 171 +++++ .../specs/front-end-block-theme.spec.js | 32 +- .../specs/front-end-classic-theme.spec.js | 27 +- test/performance/specs/post-editor.spec.js | 680 ++++++++++-------- test/performance/specs/site-editor.spec.js | 276 ++++--- test/performance/utils.js | 138 ---- 10 files changed, 902 insertions(+), 621 deletions(-) create mode 100644 test/performance/fixtures/index.js create mode 100644 test/performance/fixtures/perf-utils.ts diff --git a/packages/e2e-test-utils-playwright/src/lighthouse/index.ts b/packages/e2e-test-utils-playwright/src/lighthouse/index.ts index 274799db6c9c2..0ba2c8e67ba61 100644 --- a/packages/e2e-test-utils-playwright/src/lighthouse/index.ts +++ b/packages/e2e-test-utils-playwright/src/lighthouse/index.ts @@ -4,11 +4,16 @@ import type { Page } from '@playwright/test'; import * as lighthouse from 'lighthouse/core/index.cjs'; +type LighthouseConstructorProps = { + page: Page; + port: number; +}; + export class Lighthouse { - constructor( - public readonly page: Page, - public readonly port: number - ) { + page: Page; + port: number; + + constructor( { page, port }: LighthouseConstructorProps ) { this.page = page; this.port = port; } diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index d90a2f34fc510..68343f6d7c482 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -1,11 +1,46 @@ /** * External dependencies */ -import type { Page } from '@playwright/test'; +import type { Page, Browser } from '@playwright/test'; + +type EventType = + | 'click' + | 'focus' + | 'focusin' + | 'keydown' + | 'keypress' + | 'keyup' + | 'mouseout' + | 'mouseover'; + +interface TraceEvent { + cat: string; + name: string; + dur?: number; + args: { + data?: { + type: EventType; + }; + }; +} + +interface Trace { + traceEvents: TraceEvent[]; +} + +type MetricsConstructorProps = { + page: Page; +}; export class Metrics { - constructor( public readonly page: Page ) { + browser: Browser; + page: Page; + trace: Trace; + + constructor( { page }: MetricsConstructorProps ) { this.page = page; + this.browser = page.context().browser()!; + this.trace = { traceEvents: [] }; } /** @@ -38,10 +73,10 @@ export class Metrics { * * @see https://web.dev/ttfb/#measure-ttfb-in-javascript * - * @return {Promise} TTFB value. + * @return TTFB value. */ async getTimeToFirstByte() { - return this.page.evaluate< number >( () => { + return await this.page.evaluate< number >( () => { const { responseStart, startTime } = ( performance.getEntriesByType( 'navigation' @@ -57,10 +92,10 @@ export class Metrics { * @see https://w3c.github.io/largest-contentful-paint/ * @see https://web.dev/lcp/#measure-lcp-in-javascript * - * @return {Promise} LCP value. + * @return LCP value. */ async getLargestContentfulPaint() { - return this.page.evaluate< number >( + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { new PerformanceObserver( ( entryList ) => { @@ -83,10 +118,10 @@ export class Metrics { * @see https://github.com/WICG/layout-instability * @see https://web.dev/cls/#measure-layout-shifts-in-javascript * - * @return {Promise} CLS value. + * @return CLS value. */ async getCumulativeLayoutShift() { - return this.page.evaluate< number >( + return await this.page.evaluate< number >( () => new Promise( ( resolve ) => { let CLS = 0; @@ -108,4 +143,134 @@ export class Metrics { } ) ); } + + /** + * 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' + ) as PerformanceNavigationTiming[]; + const paintTimings = performance.getEntriesByType( + 'paint' + ) as PerformancePaintTiming[]; + + 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: 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: TraceEvent ): boolean => + item.cat === 'devtools.timeline' && + item.name === 'EventDispatch' && + item?.args?.data?.type === eventType && + !! item.dur + ) + .map( ( item ) => ( item.dur ? item.dur / 1000 : 0 ) ); + } } diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index c428df46c56f1..778f71b6d770e 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -176,10 +176,10 @@ const test = base.extend< { scope: 'worker' }, ], lighthouse: async ( { page, lighthousePort }, use ) => { - await use( new Lighthouse( page, lighthousePort ) ); + await use( new Lighthouse( { page, port: lighthousePort } ) ); }, metrics: async ( { page }, use ) => { - await use( new Metrics( page ) ); + await use( new Metrics( { page } ) ); }, } ); diff --git a/test/performance/fixtures/index.js b/test/performance/fixtures/index.js new file mode 100644 index 0000000000000..0f68fc5637f5a --- /dev/null +++ b/test/performance/fixtures/index.js @@ -0,0 +1 @@ +export { PerfUtils } from './perf-utils'; diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts new file mode 100644 index 0000000000000..e66f394234acd --- /dev/null +++ b/test/performance/fixtures/perf-utils.ts @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import { expect } from '@wordpress/e2e-test-utils-playwright'; + +/** + * External dependencies + */ +import fs from 'fs'; +import path from 'path'; +import type { Page } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { readFile } from '../utils.js'; + +type PerfUtilsConstructorProps = { + page: Page; +}; + +export class PerfUtils { + page: Page; + + constructor( { page }: PerfUtilsConstructorProps ) { + this.page = page; + } + + /** + * Returns the locator for the editor canvas element. This supports both the + * legacy and the iframed canvas. + * + * @return Locator for the editor canvas element. + */ + async getCanvas() { + return await Promise.any( [ + ( async () => { + const legacyCanvasLocator = this.page.locator( + '.wp-block-post-content' + ); + await legacyCanvasLocator.waitFor( { + timeout: 120_000, + } ); + return legacyCanvasLocator; + } )(), + ( async () => { + const iframedCanvasLocator = this.page.frameLocator( + '[name=editor-canvas]' + ); + await iframedCanvasLocator + .locator( 'body' ) + .waitFor( { timeout: 120_000 } ); + return iframedCanvasLocator; + } )(), + ] ); + } + + /** + * Saves the post as a draft and returns its URL. + * + * @return URL of the saved draft. + */ + async saveDraft() { + await this.page + .getByRole( 'button', { name: 'Save draft' } ) + .click( { timeout: 60_000 } ); + await expect( + this.page.getByRole( 'button', { name: 'Saved' } ) + ).toBeDisabled(); + + return this.page.url(); + } + + /** + * Disables the editor autosave function. + */ + async disableAutosave() { + await this.page.evaluate( () => { + return window.wp.data + .dispatch( 'core/editor' ) + .updateEditorSettings( { + autosaveInterval: 100000000000, + localAutosaveInterval: 100000000000, + } ); + } ); + + const { autosaveInterval } = await this.page.evaluate( () => { + return window.wp.data.select( 'core/editor' ).getEditorSettings(); + } ); + + expect( autosaveInterval ).toBe( 100000000000 ); + } + + /** + * Enters the Site Editor's edit mode. + * + * @return Locator for the editor canvas element. + */ + async enterSiteEditorEditMode() { + const canvas = await this.getCanvas(); + + await canvas.locator( 'body' ).click(); + await canvas + .getByRole( 'document', { name: /Block:( Post)? Content/ } ) + .click(); + + return canvas; + } + + /** + * Loads blocks from the small post with containers fixture into the editor + * canvas. + */ + async loadBlocksForSmallPostWithContainers() { + return await this.loadBlocksFromHtml( + path.join( + process.env.ASSETS_PATH!, + 'small-post-with-containers.html' + ) + ); + } + + /** + * Loads blocks from the large post fixture into the editor canvas. + */ + async loadBlocksForLargePost() { + return await this.loadBlocksFromHtml( + path.join( process.env.ASSETS_PATH!, 'large-post.html' ) + ); + } + + /** + * Loads blocks from an HTML fixture with given path into the editor canvas. + * + * @param filepath Path to the HTML fixture. + */ + async loadBlocksFromHtml( filepath: string ) { + if ( ! fs.existsSync( filepath ) ) { + throw new Error( `File not found: ${ filepath }` ); + } + + return await this.page.evaluate( ( html: string ) => { + const { parse } = window.wp.blocks; + const { dispatch } = window.wp.data; + const blocks = parse( html ); + + blocks.forEach( ( block: any ) => { + if ( block.name === 'core/image' ) { + delete block.attributes.id; + delete block.attributes.url; + } + } ); + + dispatch( 'core/block-editor' ).resetBlocks( blocks ); + }, readFile( filepath ) ); + } + + /** + * Generates and loads a 1000 empty paragraphs into the editor canvas. + */ + async load1000Paragraphs() { + await this.page.evaluate( () => { + const { createBlock } = window.wp.blocks; + const { dispatch } = window.wp.data; + const blocks = Array.from( { length: 1000 } ).map( () => + createBlock( 'core/paragraph' ) + ); + dispatch( 'core/block-editor' ).resetBlocks( blocks ); + } ); + } +} diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index 5cd05b79495ac..ca48535a21a46 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -1,7 +1,9 @@ +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ + /** * WordPress dependencies */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; const results = { timeToFirstByte: [], @@ -10,7 +12,12 @@ const results = { }; test.describe( 'Front End Performance', () => { - test.use( { storageState: {} } ); // User will be logged out. + test.use( { + storageState: {}, // User will be logged out. + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'twentytwentythree' ); @@ -26,25 +33,22 @@ test.describe( 'Front End Performance', () => { const samples = 16; const throwaway = 0; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - test( `Measure TTFB, LCP, and LCP-TTFB (${ - i + 1 - } of ${ rounds })`, async ( { page, metrics } ) => { + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, 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 = await metrics.getLargestContentfulPaint(); const ttfb = await metrics.getTimeToFirstByte(); - - // Ensure the numbers are valid. - expect( lcp ).toBeGreaterThan( 0 ); - expect( ttfb ).toBeGreaterThan( 0 ); + const lcp = await metrics.getLargestContentfulPaint(); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); @@ -52,3 +56,5 @@ test.describe( 'Front End Performance', () => { } ); } } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 68e833fe3f999..0b6c3ec22c046 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -1,7 +1,9 @@ +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ + /** * WordPress dependencies */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; const results = { timeToFirstByte: [], @@ -10,7 +12,12 @@ const results = { }; test.describe( 'Front End Performance', () => { - test.use( { storageState: {} } ); // User will be logged out. + test.use( { + storageState: {}, // User will be logged out. + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'twentytwentyone' ); @@ -25,9 +32,9 @@ test.describe( 'Front End Performance', () => { const samples = 16; const throwaway = 0; - const rounds = samples + throwaway; - for ( let i = 1; i <= rounds; i++ ) { - test( `Report TTFB, LCP, and LCP-TTFB (${ i } of ${ rounds })`, async ( { + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + test( `Measure TTFB, LCP, and LCP-TTFB (${ i } of ${ iterations })`, async ( { page, metrics, } ) => { @@ -36,15 +43,11 @@ test.describe( 'Front End Performance', () => { await page.goto( '/', { waitUntil: 'networkidle' } ); // Take the measurements. - const lcp = await metrics.getLargestContentfulPaint(); const ttfb = await metrics.getTimeToFirstByte(); - - // Ensure the numbers are valid. - expect( lcp ).toBeGreaterThan( 0 ); - expect( ttfb ).toBeGreaterThan( 0 ); + const lcp = await metrics.getLargestContentfulPaint(); // Save the results. - if ( i >= throwaway ) { + if ( i > throwaway ) { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); @@ -52,3 +55,5 @@ test.describe( 'Front End Performance', () => { } ); } } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 7b02ddd7f846b..ecacd33b9aa5b 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -1,26 +1,15 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ /** - * External dependencies + * WordPress dependencies */ -const path = require( 'path' ); +import { test, expect, Metrics } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -const { - getTypingEventDurations, - getClickEventDurations, - getHoverEventDurations, - getSelectionEventDurations, - getLoadingDurations, - loadBlocksFromHtml, - load1000Paragraphs, - sum, -} = require( '../utils' ); +import { PerfUtils } from '../fixtures'; +import { sum } from '../utils.js'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 const BROWSER_IDLE_WAIT = 1000; @@ -42,6 +31,15 @@ const results = { }; test.describe( 'Post Editor Performance', () => { + test.use( { + perfUtils: async ( { page }, use ) => { + await use( new PerfUtils( { page } ) ); + }, + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); + test.afterAll( async ( {}, testInfo ) => { await testInfo.attach( 'results', { body: JSON.stringify( results, null, 2 ), @@ -49,356 +47,444 @@ test.describe( 'Post Editor Performance', () => { } ); } ); - test.beforeEach( async ( { admin, page } ) => { - await admin.createNewPost(); - // Disable auto-save to avoid impacting the metrics. - await page.evaluate( () => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - autosaveInterval: 100000000000, - localAutosaveInterval: 100000000000, - } ); + test.describe( 'Loading', () => { + let draftURL = null; + + test( 'Setup the test post', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForLargePost(); + draftURL = await perfUtils.saveDraft(); } ); - } ); - test( 'Loading', async ( { browser, page } ) => { - // Turn the large post HTML into blocks and insert. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); - - // Get the URL that we will be testing against. - const draftURL = page.url(); - - // Start the measurements. const samples = 10; const throwaway = 1; - const rounds = throwaway + samples; - for ( let i = 0; i < rounds; i++ ) { - // Open a fresh page in a new context to prevent caching. - const testPage = await browser.newPage(); - - // Go to the test page URL. - await testPage.goto( draftURL ); - - // Get canvas (handles both legacy and iframed canvas). - const canvas = await Promise.any( [ - ( async () => { - const legacyCanvasLocator = testPage.locator( - '.wp-block-post-content' - ); - await legacyCanvasLocator.waitFor( { timeout: 120_000 } ); - return legacyCanvasLocator; - } )(), - ( async () => { - const iframedCanvasLocator = testPage.frameLocator( - '[name=editor-canvas]' + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { + page, + perfUtils, + metrics, + } ) => { + // Open the test draft. + await page.goto( draftURL ); + const canvas = await perfUtils.getCanvas(); + + // Wait for the first block. + await canvas.locator( '.wp-block' ).first().waitFor( { + timeout: 120_000, + } ); + + // Get the durations. + const loadingDurations = await metrics.getLoadingDurations(); + + // Save the results. + if ( i > throwaway ) { + Object.entries( loadingDurations ).forEach( + ( [ metric, duration ] ) => { + if ( metric === 'timeSinceResponseEnd' ) { + results.firstBlock.push( duration ); + } else { + results[ metric ].push( duration ); + } + } ); - await iframedCanvasLocator - .locator( 'body' ) - .waitFor( { timeout: 120_000 } ); - return iframedCanvasLocator; - } )(), - ] ); - - await canvas.locator( '.wp-block' ).first().waitFor( { - timeout: 120_000, + } } ); + } + } ); + + test.describe( 'Typing', () => { + let draftURL = null; + + test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + draftURL = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); + + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); + + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await metrics.startTracing(); + + // Type the testing sequence into the empty paragraph. + await paragraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); + + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations(); // Save the results. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( testPage ); - Object.entries( loadingDurations ).forEach( - ( [ metric, duration ] ) => { - results[ metric ].push( duration ); - } + for ( let i = throwaway; i < iterations; i++ ) { + results.type.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] ); } - - await testPage.close(); - } + } ); } ); - test( 'Typing', async ( { browser, page, editor } ) => { - // Load the large post fixture. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Append an empty paragraph. - await editor.insertBlock( { name: 'core/paragraph' } ); + test.describe( 'Typing within containers', () => { + let draftURL = null; - // Start tracing. - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Set up the test post', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForSmallPostWithContainers(); + draftURL = await perfUtils.saveDraft(); } ); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); + + // Select the block where we type in. + const firstParagraph = canvas + .getByRole( 'document', { name: 'Paragraph block' } ) + .first(); + await firstParagraph.click(); + + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await metrics.startTracing(); + + // Start typing in the middle of the text. + await firstParagraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); - // Type the testing sequence into the empty paragraph. - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, - } ); + // Stop tracing. + await metrics.stopTracing(); - // Stop tracing and save results. - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations(); - for ( let i = throwaway; i < rounds; i++ ) { - results.type.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results.typeContainer.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } ); } ); - test( 'Typing within containers', async ( { browser, page, editor } ) => { - await loadBlocksFromHtml( - page, - path.join( - process.env.ASSETS_PATH, - 'small-post-with-containers.html' - ) - ); - - // Select the block where we type in - await editor.canvas - .getByRole( 'document', { name: 'Paragraph block' } ) - .first() - .click(); - - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], - } ); + test.describe( 'Selecting blocks', () => { + let draftURL = null; - const samples = 10; - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const throwaway = 1; - const rounds = samples + throwaway; - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, + test( 'Set up the test post', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const canvas = await perfUtils.getCanvas(); - for ( let i = throwaway; i < rounds; i++ ) { - results.typeContainer.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } - } ); + const paragraphs = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); - test( 'Selecting blocks', async ( { browser, page, editor } ) => { - await load1000Paragraphs( page ); - const paragraphs = editor.canvas.locator( '.wp-block' ); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], - } ); + // Start tracing. + await metrics.startTracing(); - await paragraphs.nth( i ).click(); + // Click the next paragraph. + await paragraphs.nth( i ).click(); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + await metrics.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); - const allDurations = getSelectionEventDurations( traceResults ); - results.focus.push( - allDurations.reduce( ( acc, eventDurations ) => { - return acc + sum( eventDurations ); - }, 0 ) - ); + // Get the durations. + const allDurations = metrics.getSelectionEventDurations(); + + // Save the results. + if ( i > throwaway ) { + results.focus.push( + allDurations.reduce( ( acc, eventDurations ) => { + return acc + sum( eventDurations ); + }, 0 ) + ); + } } - } + } ); } ); - test( 'Opening persistent list view', async ( { browser, page } ) => { - await load1000Paragraphs( page ); - const listViewToggle = page.getByRole( 'button', { - name: 'Document Overview', + test.describe( 'Opening persistent List View', () => { + let draftURL = null; + + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + + const listViewToggle = page.getByRole( 'button', { + name: 'Document Overview', } ); - // Open List View - await listViewToggle.click(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); + + // Start tracing. + await metrics.startTracing(); + + // Open List View. + await listViewToggle.click(); + await expect( listViewToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + await metrics.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ mouseClickEvents ] = - getClickEventDurations( traceResults ); - results.listViewOpen.push( mouseClickEvents[ 0 ] ); - } + // Get the durations. + const [ mouseClickEvents ] = metrics.getClickEventDurations(); - // Close List View - await listViewToggle.click(); - } + // Save the results. + if ( i > throwaway ) { + results.listViewOpen.push( mouseClickEvents[ 0 ] ); + } + + // Close List View + await listViewToggle.click(); + await expect( listViewToggle ).toHaveAttribute( + 'aria-expanded', + 'false' + ); + } + } ); } ); - test( 'Opening the inserter', async ( { browser, page } ) => { - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + test.describe( 'Opening Inserter', () => { + let draftURL = null; + + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); - // Open Inserter. - await globalInserterToggle.click(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); + + // Start tracing. + await metrics.startTracing(); + + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); - const traceBuffer = await browser.stopTracing(); + // Stop tracing. + await metrics.stopTracing(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ mouseClickEvents ] = - getClickEventDurations( traceResults ); - results.inserterOpen.push( mouseClickEvents[ 0 ] ); - } + // Get the durations. + const [ mouseClickEvents ] = metrics.getClickEventDurations(); - // Close Inserter. - await globalInserterToggle.click(); - } - } ); + // Save the results. + if ( i > throwaway ) { + results.inserterOpen.push( mouseClickEvents[ 0 ] ); + } - test( 'Searching the inserter', async ( { browser, page } ) => { - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + // Close Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'false' + ); + } } ); + } ); - // Open Inserter. - await globalInserterToggle.click(); + test.describe( 'Searching Inserter', () => { + let draftURL = null; - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); - await page.keyboard.type( 'p' ); + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); - const traceBuffer = await browser.stopTracing(); + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); - results.inserterSearch.push( - keyDownEvents[ 0 ] + keyPressEvents[ 0 ] + keyUpEvents[ 0 ] - ); - } + // Start tracing. + await metrics.startTracing(); - await page.keyboard.press( 'Backspace' ); - } + // Type to trigger search. + await page.keyboard.type( 'p' ); + + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations(); + + // Save the results. + if ( i > throwaway ) { + results.inserterSearch.push( + keyDownEvents[ 0 ] + + keyPressEvents[ 0 ] + + keyUpEvents[ 0 ] + ); + } - // Close Inserter. - await globalInserterToggle.click(); + await page.keyboard.press( 'Backspace' ); + } + } ); } ); - test( 'Hovering Inserter Items', async ( { browser, page } ) => { - await load1000Paragraphs( page ); - const globalInserterToggle = page.getByRole( 'button', { - name: 'Toggle block inserter', + test.describe( 'Hovering Inserter items', () => { + let draftURL = null; + + test( 'Set up the test page', async ( { admin, perfUtils } ) => { + await admin.createNewPost(); + await perfUtils.load1000Paragraphs(); + draftURL = await perfUtils.saveDraft(); } ); - const paragraphBlockItem = page.locator( - '.block-editor-inserter__menu .editor-block-list-item-paragraph' - ); - const headingBlockItem = page.locator( - '.block-editor-inserter__menu .editor-block-list-item-heading' - ); - // Open Inserter. - await globalInserterToggle.click(); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + // Go to the test page. + await page.goto( draftURL ); + await perfUtils.disableAutosave(); - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( BROWSER_IDLE_WAIT ); - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + const globalInserterToggle = page.getByRole( 'button', { + name: 'Toggle block inserter', } ); + const paragraphBlockItem = page.locator( + '.block-editor-inserter__menu .editor-block-list-item-paragraph' + ); + const headingBlockItem = page.locator( + '.block-editor-inserter__menu .editor-block-list-item-heading' + ); + + // Open Inserter. + await globalInserterToggle.click(); + await expect( globalInserterToggle ).toHaveAttribute( + 'aria-expanded', + 'true' + ); + + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( BROWSER_IDLE_WAIT ); - // Hover Items. - await paragraphBlockItem.hover(); - await headingBlockItem.hover(); + // Start tracing. + await metrics.startTracing(); - const traceBuffer = await browser.stopTracing(); + // Hover Inserter items. + await paragraphBlockItem.hover(); + await headingBlockItem.hover(); - if ( i >= throwaway ) { - const traceResults = JSON.parse( traceBuffer.toString() ); + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. const [ mouseOverEvents, mouseOutEvents ] = - getHoverEventDurations( traceResults ); - for ( let k = 0; k < mouseOverEvents.length; k++ ) { - results.inserterHover.push( - mouseOverEvents[ k ] + mouseOutEvents[ k ] - ); + metrics.getHoverEventDurations(); + + // Save the results. + if ( i > throwaway ) { + for ( let k = 0; k < mouseOverEvents.length; k++ ) { + results.inserterHover.push( + mouseOverEvents[ k ] + mouseOutEvents[ k ] + ); + } } } - } - - // Close Inserter. - await globalInserterToggle.click(); + } ); } ); } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 1a5c0a96b6846..21a739dfd5c12 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -1,21 +1,14 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/* eslint-disable playwright/no-conditional-in-test, playwright/expect-expect */ /** - * External dependencies + * WordPress dependencies */ -const path = require( 'path' ); +import { test, Metrics } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies */ -const { - getTypingEventDurations, - getLoadingDurations, - loadBlocksFromHtml, -} = require( '../utils' ); +import { PerfUtils } from '../fixtures'; // See https://github.com/WordPress/gutenberg/issues/51383#issuecomment-1613460429 const BROWSER_IDLE_WAIT = 1000; @@ -36,9 +29,16 @@ const results = { listViewOpen: [], }; -let testPageId; - test.describe( 'Site Editor Performance', () => { + test.use( { + perfUtils: async ( { page }, use ) => { + await use( new PerfUtils( { page } ) ); + }, + metrics: async ( { page }, use ) => { + await use( new Metrics( { page } ) ); + }, + } ); + test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'emptytheme' ); await requestUtils.deleteAllTemplates( 'wp_template' ); @@ -56,156 +56,136 @@ test.describe( 'Site Editor Performance', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, page } ) => { - // Start a new page. - await admin.createNewPost( { postType: 'page' } ); + test.describe( 'Loading', () => { + let draftURL = null; - // Disable auto-save to avoid impacting the metrics. - await page.evaluate( () => { - window.wp.data.dispatch( 'core/editor' ).updateEditorSettings( { - autosaveInterval: 100000000000, - localAutosaveInterval: 100000000000, + test( 'Setup the test page', async ( { page, admin, perfUtils } ) => { + await admin.createNewPost( { postType: 'page' } ); + await perfUtils.loadBlocksForLargePost(); + await perfUtils.saveDraft(); + + await admin.visitSiteEditor( { + postId: new URL( page.url() ).searchParams.get( 'post' ), + postType: 'page', } ); - } ); - } ); - test( 'Loading', async ( { browser, page, admin } ) => { - // Load the large post fixture. - await loadBlocksFromHtml( - page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); - - // Get the ID of the saved page. - testPageId = new URL( page.url() ).searchParams.get( 'post' ); - - // Open the test page in Site Editor. - await admin.visitSiteEditor( { - postId: testPageId, - postType: 'page', + draftURL = page.url(); } ); - // Get the URL that we will be testing against. - const draftURL = page.url(); - - // Start the measurements. const samples = 10; const throwaway = 1; - const rounds = samples + throwaway; - for ( let i = 0; i < rounds; i++ ) { - // Open a fresh page in a new context to prevent caching. - const testPage = await browser.newPage(); - - // Go to the test page URL. - await testPage.goto( draftURL ); - - // Wait for the first block. - await testPage - .frameLocator( 'iframe[name="editor-canvas"]' ) - .locator( '.wp-block' ) - .first() - .waitFor( { timeout: 120_000 } ); - - // Save the results. - if ( i >= throwaway ) { - const loadingDurations = await getLoadingDurations( testPage ); - Object.entries( loadingDurations ).forEach( - ( [ metric, duration ] ) => { - results[ metric ].push( duration ); - } - ); - } - - await testPage.close(); + const iterations = samples + throwaway; + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { + page, + perfUtils, + metrics, + } ) => { + // Go to the test draft. + await page.goto( draftURL ); + const canvas = await perfUtils.getCanvas(); + + // Wait for the first block. + await canvas.locator( '.wp-block' ).first().waitFor( { + timeout: 120_000, + } ); + + // Get the durations. + const loadingDurations = await metrics.getLoadingDurations(); + + // Save the results. + if ( i > throwaway ) { + Object.entries( loadingDurations ).forEach( + ( [ metric, duration ] ) => { + if ( metric === 'timeSinceResponseEnd' ) { + results.firstBlock.push( duration ); + } else { + results[ metric ].push( duration ); + } + } + ); + } + } ); } } ); + test.describe( 'Typing', () => { + let draftURL = null; - test( 'Typing', async ( { browser, page, admin, editor } ) => { - // Load the large post fixture. - await loadBlocksFromHtml( + test( 'Setup the test post', async ( { page, - path.join( process.env.ASSETS_PATH, 'large-post.html' ) - ); - - // Save the draft. - await page - .getByRole( 'button', { name: 'Save draft' } ) - // Loading the large post HTML can take some time so we need a higher - // timeout value here. - .click( { timeout: 60_000 } ); - await expect( - page.getByRole( 'button', { name: 'Saved' } ) - ).toBeDisabled(); - - // Get the ID of the saved page. - testPageId = new URL( page.url() ).searchParams.get( 'post' ); - - // Open the test page in Site Editor. - await admin.visitSiteEditor( { - postId: testPageId, - postType: 'page', - } ); + admin, + editor, + perfUtils, + } ) => { + await admin.createNewPost( { postType: 'page' } ); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + await perfUtils.saveDraft(); + + await admin.visitSiteEditor( { + postId: new URL( page.url() ).searchParams.get( 'post' ), + postType: 'page', + } ); - // Wait for the first paragraph to be ready. - const firstParagraph = editor.canvas - .getByText( 'Lorem ipsum dolor sit amet' ) - .first(); - await firstParagraph.waitFor( { timeout: 60_000 } ); - - // Enter edit mode. - await editor.canvas.locator( 'body' ).click(); - // Second click is needed for the legacy edit mode. - await editor.canvas - .getByRole( 'document', { name: /Block:( Post)? Content/ } ) - .click(); - - // Append an empty paragraph. - // Since `editor.insertBlock( { name: 'core/paragraph' } )` is not - // working in page edit mode, we need to _manually_ insert a new - // paragraph. - await editor.canvas - .getByText( 'Quamquam tu hanc copiosiorem etiam soles dicere.' ) - .last() - .click(); // Enters edit mode for the last post's element, which is a list item. - - await page.keyboard.press( 'Enter' ); // Creates a new list item. - await page.keyboard.press( 'Enter' ); // Exits the list and creates a new paragraph. - - // Start tracing. - await browser.startTracing( page, { - screenshots: false, - categories: [ 'devtools.timeline' ], + draftURL = page.url(); } ); + test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + await page.goto( draftURL ); + await perfUtils.disableAutosave(); + + // Wait for the loader overlay to disappear. This is necessary + // because the overlay is still visible for a while after the editor + // canvas is ready, and we don't want it to affect the typing + // timings. + await page + .locator( '.edit-site-canvas-loader' ) + .waitFor( { state: 'hidden', timeout: 120_000 } ); + + const canvas = await perfUtils.getCanvas(); + + // Enter edit mode (second click is needed for the legacy edit mode). + await canvas.locator( 'body' ).click(); + await canvas + .getByRole( 'document', { name: /Block:( Post)? Content/ } ) + .click(); + + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const rounds = samples + throwaway; + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await metrics.startTracing(); + + // Type the testing sequence into the empty paragraph. + await paragraph.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); - // Type the testing sequence into the empty paragraph. - await page.keyboard.type( 'x'.repeat( rounds ), { - delay: BROWSER_IDLE_WAIT, - } ); + // Stop tracing. + await metrics.stopTracing(); - // Stop tracing and save results. - const traceBuffer = await browser.stopTracing(); - const traceResults = JSON.parse( traceBuffer.toString() ); - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - getTypingEventDurations( traceResults ); - for ( let i = throwaway; i < rounds; i++ ) { - results.type.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations(); + + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results.type.push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } ); } ); } ); + +/* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */ diff --git a/test/performance/utils.js b/test/performance/utils.js index b86d09a10b301..e14ff71436d73 100644 --- a/test/performance/utils.js +++ b/test/performance/utils.js @@ -58,141 +58,3 @@ export function deleteFile( filePath ) { unlinkSync( filePath ); } } - -function isEvent( item ) { - return ( - item.cat === 'devtools.timeline' && - item.name === 'EventDispatch' && - item.dur && - item.args && - item.args.data - ); -} - -function isKeyDownEvent( item ) { - return isEvent( item ) && item.args.data.type === 'keydown'; -} - -function isKeyPressEvent( item ) { - return isEvent( item ) && item.args.data.type === 'keypress'; -} - -function isKeyUpEvent( item ) { - return isEvent( item ) && item.args.data.type === 'keyup'; -} - -function isFocusEvent( item ) { - return isEvent( item ) && item.args.data.type === 'focus'; -} - -function isFocusInEvent( item ) { - return isEvent( item ) && item.args.data.type === 'focusin'; -} - -function isClickEvent( item ) { - return isEvent( item ) && item.args.data.type === 'click'; -} - -function isMouseOverEvent( item ) { - return isEvent( item ) && item.args.data.type === 'mouseover'; -} - -function isMouseOutEvent( item ) { - return isEvent( item ) && item.args.data.type === 'mouseout'; -} - -function getEventDurationsForType( trace, filterFunction ) { - return trace.traceEvents - .filter( filterFunction ) - .map( ( item ) => item.dur / 1000 ); -} - -export function getTypingEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isKeyDownEvent ), - getEventDurationsForType( trace, isKeyPressEvent ), - getEventDurationsForType( trace, isKeyUpEvent ), - ]; -} - -export function getSelectionEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isFocusEvent ), - getEventDurationsForType( trace, isFocusInEvent ), - ]; -} - -export function getClickEventDurations( trace ) { - return [ getEventDurationsForType( trace, isClickEvent ) ]; -} - -export function getHoverEventDurations( trace ) { - return [ - getEventDurationsForType( trace, isMouseOverEvent ), - getEventDurationsForType( trace, isMouseOutEvent ), - ]; -} - -export async function getLoadingDurations( page ) { - return await page.evaluate( () => { - const [ - { - requestStart, - responseStart, - responseEnd, - domContentLoadedEventEnd, - loadEventEnd, - }, - ] = performance.getEntriesByType( 'navigation' ); - const paintTimings = performance.getEntriesByType( 'paint' ); - 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: - paintTimings.find( ( { name } ) => name === 'first-paint' ) - .startTime - responseEnd, - domContentLoaded: domContentLoadedEventEnd - responseEnd, - loaded: loadEventEnd - responseEnd, - firstContentfulPaint: - paintTimings.find( - ( { name } ) => name === 'first-contentful-paint' - ).startTime - responseEnd, - // This is evaluated right after Playwright found the block selector. - firstBlock: performance.now() - responseEnd, - }; - } ); -} - -export async function loadBlocksFromHtml( page, filepath ) { - if ( ! existsSync( filepath ) ) { - throw new Error( `File not found (${ filepath })` ); - } - - return await page.evaluate( ( html ) => { - const { parse } = window.wp.blocks; - const { dispatch } = window.wp.data; - const blocks = parse( html ); - - blocks.forEach( ( block ) => { - if ( block.name === 'core/image' ) { - delete block.attributes.id; - delete block.attributes.url; - } - } ); - - dispatch( 'core/block-editor' ).resetBlocks( blocks ); - }, readFile( filepath ) ); -} - -export async function load1000Paragraphs( page ) { - await page.evaluate( () => { - const { createBlock } = window.wp.blocks; - const { dispatch } = window.wp.data; - const blocks = Array.from( { length: 1000 } ).map( () => - createBlock( 'core/paragraph' ) - ); - dispatch( 'core/block-editor' ).resetBlocks( blocks ); - } ); -}