diff --git a/packages/js/e2e-utils-playwright/package.json b/packages/js/e2e-utils-playwright/package.json new file mode 100644 index 00000000000..785ca09f6c8 --- /dev/null +++ b/packages/js/e2e-utils-playwright/package.json @@ -0,0 +1,16 @@ +{ + "name": "@woocommerce/e2e-utils-playwright", + "version": "0.1.0", + "description": "End-To-End (E2E) test Playwright utils for WooCommerce", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "license": "GPL-3.0+", + "engines": { + "node": "^20.11.1", + "pnpm": "9.1.3" + }, + "main": "src/index.js", + "module": "build-module/index.js" +} diff --git a/packages/js/e2e-utils-playwright/src/cart.js b/packages/js/e2e-utils-playwright/src/cart.js new file mode 100644 index 00000000000..44d37a37fd5 --- /dev/null +++ b/packages/js/e2e-utils-playwright/src/cart.js @@ -0,0 +1,32 @@ +/** + * Adds a specified quantity of a product by ID to the WooCommerce cart. + * + * @param page + * @param productId + * @param quantity + */ +export const addAProductToCart = async ( page, productId, quantity = 1 ) => { + for ( let i = 0; i < quantity; i++ ) { + const responsePromise = page.waitForResponse( + '**/wp-json/wc/store/v1/cart?**' + ); + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await responsePromise; + await page.getByRole( 'alert' ).waitFor( { state: 'visible' } ); + } +}; + +/** + * Util helper made for adding multiple same products to cart + * + * @param page + * @param productName + * @param quantityCount + */ +export async function addOneOrMoreProductToCart( page, productName, quantityCount ) { + await page.goto( + `product/${ productName.replace( / /gi, '-' ).toLowerCase() }` + ); + await page.getByLabel( 'Product quantity' ).fill( quantityCount ); + await page.locator( 'button[name="add-to-cart"]' ).click(); +} diff --git a/packages/js/e2e-utils-playwright/src/checkout.js b/packages/js/e2e-utils-playwright/src/checkout.js new file mode 100644 index 00000000000..dba0142f3b3 --- /dev/null +++ b/packages/js/e2e-utils-playwright/src/checkout.js @@ -0,0 +1,79 @@ +/** + * Util helper made for filling shipping details in the block-based checkout + * + * @param {Object} page + * @param {string} firstName + * @param {string} lastName + * @param {string} address + * @param {string} city + * @param {string} zip + */ +export async function fillShippingCheckoutBlocks( + page, + firstName = 'Homer', + lastName = 'Simpson', + address = '123 Evergreen Terrace', + city = 'Springfield', + zip = '97403' +) { + await page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'First name' ) + .fill( firstName ); + await page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'Last name' ) + .fill( lastName ); + await page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'Address', { exact: true } ) + .fill( address ); + await page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'City' ) + .fill( city ); + await page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'ZIP Code' ) + .fill( zip ); +} + +/** + * Util helper made for filling billing details in the block-based checkout + * + * @param {Object} page + * @param {string} firstName + * @param {string} lastName + * @param {string} address + * @param {string} city + * @param {string} zip + */ +export async function fillBillingCheckoutBlocks( + page, + firstName = 'Mister', + lastName = 'Burns', + address = '156th Street', + city = 'Springfield', + zip = '98500' +) { + await page + .getByRole( 'group', { name: 'Billing address' } ) + .getByLabel( 'First name' ) + .fill( firstName ); + await page + .getByRole( 'group', { name: 'Billing address' } ) + .getByLabel( 'Last name' ) + .fill( lastName ); + await page + .getByRole( 'group', { name: 'Billing address' } ) + .getByLabel( 'Address', { exact: true } ) + .fill( address ); + await page + .getByRole( 'group', { name: 'Billing address' } ) + .getByLabel( 'City' ) + .fill( city ); + await page + .getByRole( 'group', { name: 'Billing address' } ) + .getByLabel( 'ZIP Code' ) + .fill( zip ); +} diff --git a/packages/js/e2e-utils-playwright/src/editor.js b/packages/js/e2e-utils-playwright/src/editor.js new file mode 100644 index 00000000000..fca646b39cc --- /dev/null +++ b/packages/js/e2e-utils-playwright/src/editor.js @@ -0,0 +1,119 @@ +export const closeChoosePatternModal = async ( { page } ) => { + const closeModal = page + .getByLabel( 'Scrollable section' ) + .filter() + .getByRole( 'button', { + name: 'Close', + exact: true, + } ); + await page.addLocatorHandler( closeModal, async () => { + await closeModal.click(); + } ); +}; + +export const disableWelcomeModal = async ( { page } ) => { + // Further info: https://github.com/woocommerce/woocommerce/pull/45856/ + await page.waitForLoadState( 'domcontentloaded' ); + + const isWelcomeGuideActive = await page.evaluate( () => + window.wp.data.select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ) + ); + + if ( isWelcomeGuideActive ) { + await page.evaluate( () => + window.wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ) + ); + } +}; + +export const openEditorSettings = async ( { page } ) => { + // Open Settings sidebar if closed + if ( await page.getByLabel( 'Editor Settings' ).isVisible() ) { + console.log( 'Editor Settings is open, skipping action.' ); + } else { + await page.getByLabel( 'Settings', { exact: true } ).click(); + } +}; + +export const getCanvas = async ( page ) => { + return page.frame( 'editor-canvas' ) || page; +}; + +export const goToPageEditor = async ( { page } ) => { + await page.goto( 'wp-admin/post-new.php?post_type=page' ); + await disableWelcomeModal( { page } ); + await closeChoosePatternModal( { page } ); +}; + +export const goToPostEditor = async ( { page } ) => { + await page.goto( 'wp-admin/post-new.php' ); + await disableWelcomeModal( { page } ); +}; + +export const insertBlock = async ( page, blockName ) => { + await page + .getByRole( 'button', { + name: 'Toggle block inserter', + expanded: false, + } ) + .click(); + await page.getByPlaceholder( 'Search', { exact: true } ).fill( blockName ); + await page.getByRole( 'option', { name: blockName, exact: true } ).click(); + + await page + .getByRole( 'button', { + name: 'Close block inserter', + } ) + .click(); +}; + +export const insertBlockByShortcut = async ( page, blockName ) => { + const canvas = await getCanvas( page ); + await canvas.getByRole( 'button', { name: 'Add default block' } ).click(); + await canvas + .getByRole( 'document', { + name: 'Empty block; start writing or type forward slash to choose a block', + } ) + .pressSequentially( `/${ blockName }` ); + await page.getByRole( 'option', { name: blockName, exact: true } ).click(); +}; + +export const transformIntoBlocks = async ( page ) => { + const canvas = await getCanvas( page ); + + await canvas + .getByRole( 'button' ) + .filter( { hasText: 'Transform into blocks' } ) + .click(); +}; + +export const publishPage = async ( page, pageTitle, isPost = false ) => { + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .dispatchEvent( 'click' ); + + const createPageResponse = page.waitForResponse( ( response ) => { + return ( + response.url().includes( isPost ? '/posts/' : '/pages/' ) && + response.ok() && + response.request().method() === 'POST' && + response + .json() + .then( + ( json ) => + json.title.rendered === pageTitle && + json.status === 'publish' + ) + ); + } ); + + await page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + + // Validating that page was published via UI elements is not reliable, + // installed plugins (e.g. WooCommerce PayPal Payments) can interfere and add flakiness to the flow. + // In WC context, checking the API response is possibly the most reliable way to ensure the page was published. + await createPageResponse; +}; diff --git a/packages/js/e2e-utils-playwright/src/index.js b/packages/js/e2e-utils-playwright/src/index.js new file mode 100644 index 00000000000..9eebc8fe881 --- /dev/null +++ b/packages/js/e2e-utils-playwright/src/index.js @@ -0,0 +1,4 @@ +export * from './cart'; +export * from './checkout'; +export * from './editor'; +export * from './order'; diff --git a/packages/js/e2e-utils-playwright/src/order.js b/packages/js/e2e-utils-playwright/src/order.js new file mode 100644 index 00000000000..a8affdc3aa1 --- /dev/null +++ b/packages/js/e2e-utils-playwright/src/order.js @@ -0,0 +1,4 @@ +export function getOrderIdFromUrl( page ) { + const regex = /order-received\/(\d+)/; + return page.url().match( regex )[ 1 ]; +} diff --git a/plugins/woocommerce/changelog/51982-upkeep-e2e-utils-playwright b/plugins/woocommerce/changelog/51982-upkeep-e2e-utils-playwright new file mode 100644 index 00000000000..d6df28d5532 --- /dev/null +++ b/plugins/woocommerce/changelog/51982-upkeep-e2e-utils-playwright @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: Expose Playwright E2E tests utils. + diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 0a7a43b047b..810eb1933bc 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -673,6 +673,7 @@ "@woocommerce/e2e-utils": "workspace:*", "@woocommerce/eslint-plugin": "workspace:*", "@woocommerce/woocommerce-rest-api": "^1.0.1", + "@woocommerce/e2e-utils-playwright": "workspace:*", "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-preset-default": "3.0.2", "@wordpress/e2e-test-utils-playwright": "wp-6.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a188d22801..0c04ab99fb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1667,6 +1667,8 @@ importers: specifier: 0.14.3 version: 0.14.3 + packages/js/e2e-utils-playwright: {} + packages/js/eslint-plugin: dependencies: '@typescript-eslint/eslint-plugin': @@ -3375,6 +3377,9 @@ importers: '@woocommerce/e2e-utils': specifier: workspace:* version: link:../../packages/js/e2e-utils + '@woocommerce/e2e-utils-playwright': + specifier: workspace:* + version: link:../../packages/js/e2e-utils-playwright '@woocommerce/eslint-plugin': specifier: workspace:* version: link:../../packages/js/eslint-plugin @@ -65336,11 +65341,11 @@ snapshots: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.12.1 + acorn-import-assertions: 1.9.0(acorn@8.12.1) browserslist: 4.19.3 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 + enhanced-resolve: 5.16.0 es-module-lexer: 1.4.1 eslint-scope: 5.1.1 events: 3.3.0