* Add performance script to measure load times of cart and checkout blocks

* Temp commit

* Temp commit

* Temp commit

* Remove specific test from performance e2e command

* Add performance reporter

* Add step to clean performance file before running tests

* Add test report constant & average and logPerformanceResult test utils

* Update uses of product name constant

* Add cart coupon performance test

* Check if report file is empty before parsing

* Limit performance tests to only ones in performance directory

* Round the averages and add a linebreak after each entry in the log

* Fix formatting of report and only output after all tests

* Log each loading metric as an individual data point

* Improve formatting

* Get load times in ms from profiler

* Revert changes to fixtures

* Remove trace.json from git

* Remove checkout test file (tests not implemented)

* Check performance log file exists before truncating it

* Fix checkout coupon test

* Remove console logs

* Revert to use virtual products after rename of constant

* Ignore performance tests when running regular e2e tests

* Feedback changes

* Tidy up

* Fix packag-log.json:

Co-authored-by: Alex Florisca <alex.florisca@automattic.com>
This commit is contained in:
Thomas Roberts 2022-04-04 13:30:07 +01:00 committed by GitHub
parent d005dbd2cf
commit 10793e8e18
14 changed files with 279 additions and 24 deletions

View File

@ -67,9 +67,10 @@
"storybook:deploy": "rimraf ./storybook/dist/* && npm run storybook:build && gh-pages -d ./storybook/dist",
"test": "wp-scripts test-unit-js --config tests/js/jest.config.json",
"test:debug": "ndb .",
"test:e2e": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js",
"test:e2e-dev": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --puppeteer-interactive",
"test:e2e-dev-watch": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --watch --puppeteer-interactive",
"test:e2e": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --testPathIgnorePatterns performance",
"test:performance": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js -- performance",
"test:e2e-dev": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --puppeteer-interactive --testPathIgnorePatterns performance",
"test:e2e-dev-watch": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --watch --puppeteer-interactive --testPathIgnorePatterns performance",
"test:e2e:update": "npm run wp-env:config && cross-env NODE_CONFIG_DIR=tests/e2e/config wp-scripts test-e2e --config tests/e2e/config/jest.config.js --updateSnapshot",
"test:help": "wp-scripts test-unit-js --help",
"pretest:php": "npm run wp-env run composer 'install --no-interaction'",
@ -132,6 +133,7 @@
"@wordpress/dependency-extraction-webpack-plugin": "3.2.1",
"@wordpress/dom": "3.2.7",
"@wordpress/e2e-test-utils": "6.0.2",
"@wordpress/e2e-tests": "^3.1.1",
"@wordpress/element": "4.0.4",
"@wordpress/env": "4.1.3",
"@wordpress/html-entities": "3.2.3",

View File

@ -14,6 +14,7 @@ module.exports = {
'jest-html-reporters',
{ publicPath: './reports/e2e', filename: 'index.html' },
],
'<rootDir>/tests/e2e/config/performance-reporter.js',
],
testEnvironment: '<rootDir>/tests/e2e/config/environment.js',

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
const { readFileSync, statSync } = require( 'fs' );
const chalk = require( 'chalk' );
const { PERFORMANCE_REPORT_FILENAME } = require( '../../utils/constants' );
class PerformanceReporter {
onRunComplete() {
if ( statSync( PERFORMANCE_REPORT_FILENAME ).size === 0 ) {
return;
}
const reportFileContents = readFileSync( PERFORMANCE_REPORT_FILENAME )
.toString()
.split( '\n' )
.slice( 0, -1 )
.map( ( line ) => JSON.parse( line ) );
reportFileContents.forEach( ( testReport ) => {
// eslint-disable-next-line no-console
console.log(
chalk.black.bgGreen.underline.bold( testReport.description )
);
// eslint-disable-next-line no-console
console.log( chalk.red( `Longest: ${ testReport.longest }ms` ) );
// eslint-disable-next-line no-console
console.log(
chalk.green( `Shortest: ${ testReport.shortest }ms` )
);
// eslint-disable-next-line no-console
console.log(
chalk.yellow( `Average: ${ testReport.average.toFixed() }ms` )
);
// eslint-disable-next-line no-console
console.log( '' );
} );
}
}
module.exports = PerformanceReporter;

View File

@ -3,6 +3,7 @@
* External dependencies
*/
import { setup as setupPuppeteer } from 'jest-environment-puppeteer';
import fs from 'fs';
/**
* Internal dependencies
*/
@ -20,6 +21,7 @@ import {
enablePaymentGateways,
createProductAttributes,
} from '../fixtures/fixture-loaders';
import { PERFORMANCE_REPORT_FILENAME } from '../../utils/constants';
module.exports = async ( globalConfig ) => {
// we need to load puppeteer global setup here.
@ -64,6 +66,9 @@ module.exports = async ( globalConfig ) => {
await createReviews( productId );
} );
// Wipe the performance e2e file at the start of every run
fs.truncateSync( PERFORMANCE_REPORT_FILENAME );
global.fixtureData = {
taxes,
coupons,

View File

@ -0,0 +1,126 @@
/**
* Internal dependencies
*/
import { shopper, getLoadingDurations } from '../../../utils';
import { SIMPLE_PHYSICAL_PRODUCT_NAME } from '../../../utils/constants';
import { logPerformanceResult } from '../../utils';
describe( 'Cart performance', () => {
beforeAll( async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PHYSICAL_PRODUCT_NAME );
} );
it( 'Loading', async () => {
await shopper.block.goToCart();
const results = {
serverResponse: [],
firstPaint: [],
domContentLoaded: [],
loaded: [],
firstContentfulPaint: [],
firstBlock: [],
type: [],
focus: [],
inserterOpen: [],
inserterHover: [],
inserterSearch: [],
listViewOpen: [],
};
let i = 3;
// Measuring loading time.
while ( i-- ) {
await page.reload();
await page.waitForSelector( '.wc-block-cart' );
const {
serverResponse,
firstPaint,
domContentLoaded,
loaded,
firstContentfulPaint,
firstBlock,
} = await getLoadingDurations();
// Multiply by 1000 to get time in ms
results.serverResponse.push( serverResponse * 1000 );
results.firstPaint.push( firstPaint * 1000 );
results.domContentLoaded.push( domContentLoaded * 1000 );
results.loaded.push( loaded * 1000 );
results.firstContentfulPaint.push( firstContentfulPaint * 1000 );
results.firstBlock.push( firstBlock * 1000 );
}
Object.entries( results ).forEach( ( [ name, value ] ) => {
if (
Array.isArray( value ) &&
value.every( ( x ) => typeof x === 'number' ) &&
value.length === 0
) {
return;
}
logPerformanceResult( `Cart block loading: (${ name })`, value );
} );
// To stop warning about no assertions.
expect( true ).toBe( true );
} );
it( 'Quantity change', async () => {
await shopper.block.goToCart();
await page.waitForSelector(
'button.wc-block-components-quantity-selector__button--plus'
);
let i = 3;
const timesForResponse = [];
while ( i-- ) {
const start = performance.now();
await expect( page ).toClick(
'button.wc-block-components-quantity-selector__button--plus'
);
await page.waitForResponse(
( response ) =>
response.url().indexOf( '/wc/store/v1/batch' ) !== -1 &&
response.status() === 207
);
const end = performance.now();
timesForResponse.push( end - start );
}
logPerformanceResult(
'Cart block: Change cart item quantity',
timesForResponse
);
} );
it( 'Coupon entry', async () => {
await shopper.block.goToCart();
await page.waitForSelector(
'button.wc-block-components-quantity-selector__button--plus'
);
let i = 3;
const timesForResponse = [];
while ( i-- ) {
const start = performance.now();
await expect( page ).toClick( 'button', { text: 'Coupon code' } );
await expect( page ).toFill(
'[aria-label="Enter code"]',
'test_coupon'
);
await expect( page ).toClick( 'button', { text: 'Apply' } );
await page.waitForResponse(
( response ) =>
response.url().indexOf( '/wc/store/v1/batch' ) !== -1 &&
response.status() === 207
);
const end = performance.now();
// Close the coupon panel.
await expect( page ).toClick( 'button', { text: 'Coupon code' } );
timesForResponse.push( end - start );
}
logPerformanceResult( 'Cart block: Coupon entry', timesForResponse );
} );
} );

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import { shopper } from '../../../utils';
import { SIMPLE_PRODUCT_NAME } from '../../../utils/constants';
import { SIMPLE_VIRTUAL_PRODUCT_NAME } from '../../../utils/constants';
const block = {
name: 'Cart',
@ -23,7 +23,7 @@ describe( 'Shopper → Cart → Can proceed to checkout', () => {
it( 'allows customer to proceed to checkout', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCart();
// Click on "Proceed to Checkout" button

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import { shopper } from '../../../utils';
import { SIMPLE_PRODUCT_NAME } from '../../../utils/constants';
import { SIMPLE_VIRTUAL_PRODUCT_NAME } from '../../../utils/constants';
if ( process.env.WOOCOMMERCE_BLOCKS_PHASE < 2 )
// eslint-disable-next-line jest/no-focused-tests
@ -15,7 +15,7 @@ describe( 'Shopper → Cart → Can remove product', () => {
it( 'Can remove product from cart', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCart();
const removeProductLink = await page.$(
'.wc-block-cart-item__remove-link'

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import { shopper } from '../../../utils';
import { SIMPLE_PRODUCT_NAME } from '../../../utils/constants';
import { SIMPLE_VIRTUAL_PRODUCT_NAME } from '../../../utils/constants';
const block = {
name: 'Cart',
@ -23,10 +23,10 @@ describe( 'Shopper → Cart → Can update product quantity', () => {
it( 'allows customer to update product quantity via the input field', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCart();
await shopper.block.setCartQuantity( SIMPLE_VIRTUAL_PRODUCT_NAME, 4 );
await shopper.block.setCartQuantity( SIMPLE_PRODUCT_NAME, 4 );
await expect( page ).toMatchElement(
'button.wc-block-cart__submit-button[disabled]'
);
@ -35,11 +35,13 @@ describe( 'Shopper → Cart → Can update product quantity', () => {
await page.waitForNetworkIdle( { idleTime: 1000 } );
await expect( page ).toMatchElement( 'a.wc-block-cart__submit-button' );
await shopper.block.productIsInCart( SIMPLE_PRODUCT_NAME, 4 );
await shopper.block.productIsInCart( SIMPLE_VIRTUAL_PRODUCT_NAME, 4 );
} );
it( 'allows customer to increase product quantity via the plus button', async () => {
await shopper.block.increaseCartQuantityByOne( SIMPLE_PRODUCT_NAME );
await shopper.block.increaseCartQuantityByOne(
SIMPLE_VIRTUAL_PRODUCT_NAME
);
await expect( page ).toMatchElement(
'button.wc-block-cart__submit-button[disabled]'
);
@ -48,11 +50,13 @@ describe( 'Shopper → Cart → Can update product quantity', () => {
await page.waitForNetworkIdle( { idleTime: 1000 } );
await expect( page ).toMatchElement( 'a.wc-block-cart__submit-button' );
await shopper.block.productIsInCart( SIMPLE_PRODUCT_NAME, 5 );
await shopper.block.productIsInCart( SIMPLE_VIRTUAL_PRODUCT_NAME, 5 );
} );
it( 'allows customer to decrease product quantity via the minus button', async () => {
await shopper.block.decreaseCartQuantityByOne( SIMPLE_PRODUCT_NAME );
await shopper.block.decreaseCartQuantityByOne(
SIMPLE_VIRTUAL_PRODUCT_NAME
);
await expect( page ).toMatchElement(
'button.wc-block-cart__submit-button[disabled]'
);
@ -61,6 +65,6 @@ describe( 'Shopper → Cart → Can update product quantity', () => {
await page.waitForNetworkIdle( { idleTime: 1000 } );
await expect( page ).toMatchElement( 'a.wc-block-cart__submit-button' );
await shopper.block.productIsInCart( SIMPLE_PRODUCT_NAME, 4 );
await shopper.block.productIsInCart( SIMPLE_VIRTUAL_PRODUCT_NAME, 4 );
} );
} );

View File

@ -3,7 +3,7 @@
*/
import { shopper } from '../../../utils';
import { SIMPLE_PRODUCT_NAME } from '../../../utils/constants';
import { SIMPLE_VIRTUAL_PRODUCT_NAME } from '../../../utils/constants';
const PAYMENT_COD = 'Cash on delivery';
const PAYMENT_BACS = 'Direct bank transfer';
@ -24,7 +24,7 @@ describe( 'Shopper → Checkout → Can choose payment option', () => {
it( 'allows customer to pay using Direct bank transfer', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCheckout();
await shopper.block.selectPayment( PAYMENT_BACS );
await shopper.block.fillInCheckoutWithTestData();
@ -35,7 +35,7 @@ describe( 'Shopper → Checkout → Can choose payment option', () => {
it( 'allows customer to pay using Cash on delivery', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCheckout();
await shopper.block.selectPayment( PAYMENT_COD );
await shopper.block.fillInCheckoutWithTestData();
@ -46,7 +46,7 @@ describe( 'Shopper → Checkout → Can choose payment option', () => {
it( 'allows customer to pay using Check payments', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCheckout();
await shopper.block.selectPayment( PAYMENT_CHEQUE );
await shopper.block.fillInCheckoutWithTestData();

View File

@ -2,7 +2,10 @@
* Internal dependencies
*/
import { shopper } from '../../../utils';
import { SIMPLE_PRODUCT_NAME, BILLING_DETAILS } from '../../../utils/constants';
import {
SIMPLE_VIRTUAL_PRODUCT_NAME,
BILLING_DETAILS,
} from '../../../utils/constants';
if ( process.env.WOOCOMMERCE_BLOCKS_PHASE < 2 )
// eslint-disable-next-line jest/no-focused-tests
@ -12,7 +15,7 @@ describe( 'Shopper → Checkout → Can place an order', () => {
it( 'allows customer to place an order as a guest', async () => {
await shopper.logout();
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCheckout();
await shopper.block.fillBillingDetails( BILLING_DETAILS );
await shopper.block.placeOrder();
@ -22,7 +25,7 @@ describe( 'Shopper → Checkout → Can place an order', () => {
it( 'allows customer to place an order as a logged in user', async () => {
await shopper.login();
await shopper.goToShop();
await shopper.addToCartFromShopPage( SIMPLE_PRODUCT_NAME );
await shopper.addToCartFromShopPage( SIMPLE_VIRTUAL_PRODUCT_NAME );
await shopper.block.goToCheckout();
await shopper.block.fillBillingDetails( BILLING_DETAILS );
await shopper.block.placeOrder();

View File

@ -14,11 +14,13 @@ import {
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';
import { WP_ADMIN_DASHBOARD } from '@woocommerce/e2e-utils';
import fs from 'fs';
/**
* Internal dependencies
*/
import { elementExists, getElementData, getTextContent } from './page-utils';
import { PERFORMANCE_REPORT_FILENAME } from '../utils/constants';
/**
* @typedef {import('@types/puppeteer').ElementHandle} ElementHandle
@ -345,7 +347,35 @@ export function useTheme( themeSlug ) {
}
/**
* Add a block to Full Site Editing.
* Takes an average value of all items in an array.
*
* @param {Array} array An array of numbers to take an average from.
* @return {number} The average value of all members of the array.
*/
const average = ( array ) => array.reduce( ( a, b ) => a + b ) / array.length;
/**
* Writes a line to the e2e performance result for the current test containing longest, shortest, and average run times.
*
* @param {string} description Message to describe what you're logging the performance of.
* @param {Array} times array of times to record.
*/
export const logPerformanceResult = ( description, times ) => {
const roundedTimes = times.map(
( time ) => Math.round( time + Number.EPSILON * 100 ) / 100
);
fs.appendFileSync(
PERFORMANCE_REPORT_FILENAME,
JSON.stringify( {
description,
longest: Math.max( ...roundedTimes ),
shortest: Math.min( ...roundedTimes ),
average: average( roundedTimes ),
} ) + '\n'
);
};
/* Add a block to Full Site Editing.
*
* *Note:* insertBlock function gets focused on the canvas, this could prevent some dialogs from being displayed. e.g. compatibility notice.
*

View File

@ -8,8 +8,10 @@ const config = require( 'config' );
*
* @type {string}
*/
export const SIMPLE_PRODUCT_NAME = 'Woo Single #1';
export const SIMPLE_VIRTUAL_PRODUCT_NAME = 'Woo Single #1';
export const SIMPLE_PHYSICAL_PRODUCT_NAME = '128GB USB Stick';
export const BILLING_DETAILS = config.get( 'addresses.customer.billing' );
export const PERFORMANCE_REPORT_FILENAME = 'reports/e2e-performance.json';
export const SHIPPING_DETAILS = config.get( 'addresses.customer.shipping' );
export const CUSTOMER_USERNAME = config.get( 'users.customer.username' );
export const CUSTOMER_PASSWORD = config.get( 'users.customer.password' );

View File

@ -9,4 +9,5 @@ export {
reactivateCompatibilityNotice,
} from './compatibility-notice';
export { shopper } from './shopper';
export { getLoadingDurations } from './performance';
export { selectBlockByName } from './select-block-by-name';

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import type { Page } from 'puppeteer';
export async function getLoadingDurations(): Promise<
ReturnType< Page[ 'evaluate' ] >
> {
return await page.evaluate( () => {
const [
{
requestStart,
responseStart,
responseEnd,
domContentLoadedEventEnd,
loadEventEnd,
},
] = performance.getEntriesByType(
'navigation'
) as PerformanceNavigationTiming[];
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 Puppeteer found the block selector.
firstBlock: performance.now() - responseEnd,
};
} );
}