From a3ec316b63b4a0f4183bc488d8b27e05523a9739 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:15:36 -0300 Subject: [PATCH 01/11] Update subscriptions customer config --- tests/e2e-pw/config/default.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e-pw/config/default.ts b/tests/e2e-pw/config/default.ts index a3742804047..d370c84d25f 100644 --- a/tests/e2e-pw/config/default.ts +++ b/tests/e2e-pw/config/default.ts @@ -76,12 +76,12 @@ export const config = { }, 'subscriptions-customer': { billing: { - first_name: 'I am', - last_name: 'Subscriptions Customer', + firstname: 'I am', + lastname: 'Subscriptions Customer', company: 'Automattic', country: 'United States (US)', - address_1: '60 29th Street #343', - address_2: 'billing', + addressfirstline: '60 29th Street #343', + addresssecondline: 'billing', city: 'San Francisco', state: 'CA', postcode: '94110', @@ -89,12 +89,12 @@ export const config = { email: 'e2e-wcpay-subscriptions-customer@woo.com', }, shipping: { - first_name: 'I am', - last_name: 'Subscriptions Recipient', + firstname: 'I am', + lastname: 'Subscriptions Recipient', company: 'Automattic', country: 'United States (US)', - address_1: '60 29th Street #343', - address_2: 'shipping', + addressfirstline: '60 29th Street #343', + addresssecondline: 'shipping', city: 'San Francisco', state: 'CA', postcode: '94110', From 9437c51e4daf643bfd996626124846026f2bc9b7 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:25:51 -0300 Subject: [PATCH 02/11] Add constants file --- tests/e2e-pw/utils/constants.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/e2e-pw/utils/constants.ts diff --git a/tests/e2e-pw/utils/constants.ts b/tests/e2e-pw/utils/constants.ts new file mode 100644 index 00000000000..94f4a02d53d --- /dev/null +++ b/tests/e2e-pw/utils/constants.ts @@ -0,0 +1,6 @@ +export const shouldRunSubscriptionsTests = + process.env.SKIP_WC_SUBSCRIPTIONS_TESTS !== '1'; + +export const products = { + SUBSCRIPTION_SIGNUP_FEE: 70, +}; From 46f6fccda722c3082561c44ca5b714730038e24e Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:29:20 -0300 Subject: [PATCH 03/11] Add a helper to conditionally skip a test suite --- tests/e2e-pw/utils/helpers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/e2e-pw/utils/helpers.ts b/tests/e2e-pw/utils/helpers.ts index 8e0c6881300..6fb1b9d779a 100644 --- a/tests/e2e-pw/utils/helpers.ts +++ b/tests/e2e-pw/utils/helpers.ts @@ -93,3 +93,9 @@ export const getAnonymousShopper = async ( const shopperPage = await shopperContext.newPage(); return { shopperPage, shopperContext }; }; + +/** + * Conditionally determine whether or not to skip a test suite. + */ +export const describeif = ( condition: boolean ) => + condition ? test.describe : test.describe.skip; From defe4bdc24a0136e00d36d1cdf36638e7645adf5 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:30:06 -0300 Subject: [PATCH 04/11] Add function to access the subscriptions page as a shopper --- tests/e2e-pw/utils/shopper-navigation.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e-pw/utils/shopper-navigation.ts b/tests/e2e-pw/utils/shopper-navigation.ts index 7f96a1e9055..8343bcc5cee 100644 --- a/tests/e2e-pw/utils/shopper-navigation.ts +++ b/tests/e2e-pw/utils/shopper-navigation.ts @@ -43,3 +43,8 @@ export const goToOrder = async ( page: Page, orderId: string ) => { waitUntil: 'load', } ); }; + +export const goToSubscriptions = ( page: Page ) => + page.goto( '/my-account/subscriptions/', { + waitUntil: 'load', + } ); From b164e97e90cd15c68e36e8d00dcc294dcebb945c Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:31:27 -0300 Subject: [PATCH 05/11] Add function to delete customers by email address --- tests/e2e-pw/utils/rest-api.ts | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/e2e-pw/utils/rest-api.ts diff --git a/tests/e2e-pw/utils/rest-api.ts b/tests/e2e-pw/utils/rest-api.ts new file mode 100644 index 00000000000..ea7730913ad --- /dev/null +++ b/tests/e2e-pw/utils/rest-api.ts @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { HTTPClientFactory } from '@woocommerce/api'; + +/** + * Internal dependencies + */ +import { config } from '../config/default'; + +const userEndpoint = '/wp/v2/users'; + +const getAdminClient = () => + HTTPClientFactory.build( process.env.BASE_URL ) + .withBasicAuth( + config.users.admin.username, + config.users.admin.password + ) + .create(); + +/** + * Deletes a customer account by their email address if the user exists. + * + * Copied from https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/e2e-utils/src/flows/with-rest-api.js#L374 + * + * @param {string} emailAddress Customer user account email address. + * @return {Promise} + */ +export const deleteCustomerByEmailAddress = async ( emailAddress: string ) => { + const client = getAdminClient(); + + const query = { + search: emailAddress, + context: 'edit', + }; + const customers = await client.get( userEndpoint, query ); + + if ( customers.data && customers.data.length ) { + for ( let c = 0; c < customers.data.length; c++ ) { + const deleteUser = { + id: customers.data[ c ].id, + force: true, + reassign: 1, + }; + + await client.delete( + `${ userEndpoint }/${ deleteUser.id }`, + deleteUser + ); + } + } +}; From 93aa149b2f08b862860d4a9c4582625c406703c9 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:34:43 -0300 Subject: [PATCH 06/11] Convert the shopper renew subscription spec to Playwright --- ...opper-myaccount-renew-subscription.spec.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts diff --git a/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts new file mode 100644 index 00000000000..6123e7dccef --- /dev/null +++ b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import { test, expect, Page } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { config } from '../../config/default'; +import { describeif, getAnonymousShopper } from '../../utils/helpers'; +import * as shopper from '../../utils/shopper'; +import * as navigation from '../../utils/shopper-navigation'; +import { products, shouldRunSubscriptionsTests } from '../../utils/constants'; +import { deleteCustomerByEmailAddress } from '../../utils/rest-api'; + +describeif( shouldRunSubscriptionsTests )( + 'Subscriptions > Renew a subscription in my account', + () => { + const customerBillingConfig = + config.addresses[ 'subscriptions-customer' ].billing; + + let subscriptionId: string; + let page: Page; + + test.beforeAll( async ( { browser } ) => { + await deleteCustomerByEmailAddress( customerBillingConfig.email ); + + const { shopperPage } = await getAnonymousShopper( browser ); + page = shopperPage; + } ); + + test( 'should be able to purchase a subscription', async () => { + await shopper.addCartProduct( + page, + products.SUBSCRIPTION_SIGNUP_FEE + ); + await shopper.setupCheckout( page, customerBillingConfig ); + await shopper.fillCardDetails( page, config.cards.basic ); + await shopper.placeOrder( page ); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + subscriptionId = await page + .getByLabel( 'View subscription number' ) + .innerText(); + } ); + + test( 'should be able to renew a subscription in my account', async () => { + await navigation.goToSubscriptions( page ); + + if ( ! subscriptionId ) { + throw new Error( 'Subscription ID is not set' ); + } + + const numericSubscriptionId = subscriptionId.substring( 1 ); + + await page + .getByLabel( + `View subscription number ${ numericSubscriptionId }` + ) + .click(); + + await page.getByText( 'Renew now' ).click(); + await page + .getByText( 'Complete checkout to renew now.' ) + .isVisible(); + await page.waitForLoadState( 'networkidle' ); + + await shopper.placeOrder( page ); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + } ); + } +); From 9bfe188df7d9c302353e6f08b61516eb7a4a3e47 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:37:57 -0300 Subject: [PATCH 07/11] Remove Puppeteer spec --- ...opper-myaccount-renew-subscription.spec.js | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 tests/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.js diff --git a/tests/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.js b/tests/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.js deleted file mode 100644 index e44f026cc42..00000000000 --- a/tests/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * External dependencies - */ -import config from 'config'; -const { shopper, withRestApi } = require( '@woocommerce/e2e-utils' ); -import { - RUN_SUBSCRIPTIONS_TESTS, - describeif, - shopperWCP, -} from '../../../utils'; -import { fillCardDetails, setupCheckout } from '../../../utils/payments'; - -const productSlug = 'subscription-signup-fee-product'; -const customerBilling = config.get( - 'addresses.subscriptions-customer.billing' -); -let subscriptionId; - -const testSelectors = { - subscriptionIdField: '.woocommerce-orders-table__cell-subscription-id > a', - subscriptionRenewButton: 'a.button.subscription_renewal_early', - wcNotice: 'div.wc-block-components-notice-banner', - wcOldNotice: - '.woocommerce .woocommerce-notices-wrapper .woocommerce-message', -}; - -describeif( RUN_SUBSCRIPTIONS_TESTS )( - 'Subscriptions > Renew a subscription in my account', - () => { - beforeAll( async () => { - // Delete the user, if present - await withRestApi.deleteCustomerByEmail( customerBilling.email ); - } ); - afterAll( async () => { - await shopper.logout(); - } ); - - it( 'should be able to purchase a subscription', async () => { - // Open the subscription product & add to cart - await shopperWCP.addToCartBySlug( productSlug ); - - // Checkout - await setupCheckout( customerBilling ); - const card = config.get( 'cards.basic' ); - await fillCardDetails( page, card ); - await shopper.placeOrder(); - await expect( page ).toMatchTextContent( 'Order received' ); - - // Get the subscription ID - const subscriptionIdField = await page.$( - testSelectors.subscriptionIdField - ); - subscriptionId = await subscriptionIdField.evaluate( - ( el ) => el.innerText - ); - } ); - - it( 'should be able to renew a subscription in my account', async () => { - // Go to my account and click to renew a subscription - await shopperWCP.goToSubscriptions(); - await expect( page ).toClick( testSelectors.subscriptionIdField, { - text: subscriptionId, - } ); - await page.waitForNavigation( { waitUntil: 'networkidle0' } ); - await expect( page ).toClick( - testSelectors.subscriptionRenewButton - ); - - // Place an order to renew a subscription - await shopperWCP.waitForSubscriptionsErrorBanner( - 'Complete checkout to renew now.', - testSelectors.wcNotice, - testSelectors.wcOldNotice - ); - await page.waitForNavigation( { waitUntil: 'networkidle0' } ); - - await shopper.placeOrder(); - await expect( page ).toMatchTextContent( 'Order received' ); - } ); - } -); From 9554e25772b1c769a96c77cf93daf16e40d6384f Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 18:40:34 -0300 Subject: [PATCH 08/11] Add changelog entry --- .../dev-10084-shopper-myaccount-renew-subscription-e2e-test | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/dev-10084-shopper-myaccount-renew-subscription-e2e-test diff --git a/changelog/dev-10084-shopper-myaccount-renew-subscription-e2e-test b/changelog/dev-10084-shopper-myaccount-renew-subscription-e2e-test new file mode 100644 index 00000000000..0bf90c28107 --- /dev/null +++ b/changelog/dev-10084-shopper-myaccount-renew-subscription-e2e-test @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate the Shopper Renew Subscription spec to Playwright and remove the corresponding Puppeteer test. From 8e911d33efbedcaec038125c61208a11a2a28b67 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Mon, 13 Jan 2025 19:18:43 -0300 Subject: [PATCH 09/11] Refactor rest api helper to receive the client base URL dynamically --- ...opper-myaccount-renew-subscription.spec.ts | 9 +- tests/e2e-pw/utils/rest-api.ts | 90 +++++++++++-------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts index 6123e7dccef..1ff2db72e9f 100644 --- a/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts +++ b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts @@ -11,7 +11,7 @@ import { describeif, getAnonymousShopper } from '../../utils/helpers'; import * as shopper from '../../utils/shopper'; import * as navigation from '../../utils/shopper-navigation'; import { products, shouldRunSubscriptionsTests } from '../../utils/constants'; -import { deleteCustomerByEmailAddress } from '../../utils/rest-api'; +import RestAPI from '../../utils/rest-api'; describeif( shouldRunSubscriptionsTests )( 'Subscriptions > Renew a subscription in my account', @@ -22,8 +22,11 @@ describeif( shouldRunSubscriptionsTests )( let subscriptionId: string; let page: Page; - test.beforeAll( async ( { browser } ) => { - await deleteCustomerByEmailAddress( customerBillingConfig.email ); + test.beforeAll( async ( { browser }, { project } ) => { + const restApi = new RestAPI( project.use.baseURL ); + await restApi.deleteCustomerByEmailAddress( + customerBillingConfig.email + ); const { shopperPage } = await getAnonymousShopper( browser ); page = shopperPage; diff --git a/tests/e2e-pw/utils/rest-api.ts b/tests/e2e-pw/utils/rest-api.ts index ea7730913ad..400262422d2 100644 --- a/tests/e2e-pw/utils/rest-api.ts +++ b/tests/e2e-pw/utils/rest-api.ts @@ -10,43 +10,59 @@ import { config } from '../config/default'; const userEndpoint = '/wp/v2/users'; -const getAdminClient = () => - HTTPClientFactory.build( process.env.BASE_URL ) - .withBasicAuth( - config.users.admin.username, - config.users.admin.password - ) - .create(); +class RestAPI { + private baseUrl: string; -/** - * Deletes a customer account by their email address if the user exists. - * - * Copied from https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/e2e-utils/src/flows/with-rest-api.js#L374 - * - * @param {string} emailAddress Customer user account email address. - * @return {Promise} - */ -export const deleteCustomerByEmailAddress = async ( emailAddress: string ) => { - const client = getAdminClient(); - - const query = { - search: emailAddress, - context: 'edit', - }; - const customers = await client.get( userEndpoint, query ); - - if ( customers.data && customers.data.length ) { - for ( let c = 0; c < customers.data.length; c++ ) { - const deleteUser = { - id: customers.data[ c ].id, - force: true, - reassign: 1, - }; - - await client.delete( - `${ userEndpoint }/${ deleteUser.id }`, - deleteUser - ); + constructor( baseUrl: string ) { + if ( ! baseUrl ) { + throw new Error( 'Base URL is required.' ); } + this.baseUrl = baseUrl; + } + + private getAdminClient() { + return HTTPClientFactory.build( this.baseUrl ) + .withBasicAuth( + config.users.admin.username, + config.users.admin.password + ) + .create(); } -}; + + /** + * Deletes a customer account by their email address if the user exists. + * + * Copied from https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/e2e-utils/src/flows/with-rest-api.js#L374 + * + * @param {string} emailAddress Customer user account email address. + * @return {Promise} + */ + async deleteCustomerByEmailAddress( + emailAddress: string + ): Promise< void > { + const client = this.getAdminClient(); + + const query = { + search: emailAddress, + context: 'edit', + }; + const customers = await client.get( userEndpoint, query ); + + if ( customers.data && customers.data.length ) { + for ( let c = 0; c < customers.data.length; c++ ) { + const deleteUser = { + id: customers.data[ c ].id, + force: true, + reassign: 1, + }; + + await client.delete( + `${ userEndpoint }/${ deleteUser.id }`, + deleteUser + ); + } + } + } +} + +export default RestAPI; From fba0e5df93f56bf189e76c43b01a7c1065f73fc4 Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Tue, 14 Jan 2025 08:52:21 -0300 Subject: [PATCH 10/11] Extract shopper helper functions --- tests/e2e-pw/utils/shopper.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts index b39869b8426..8f520ea38fe 100644 --- a/tests/e2e-pw/utils/shopper.ts +++ b/tests/e2e-pw/utils/shopper.ts @@ -12,6 +12,24 @@ export const isUIUnblocked = async ( page: Page ) => { await expect( page.locator( '.blockUI' ) ).toHaveCount( 0 ); }; +/** + * Waits for the UI to refresh after a user interaction. + * + * Woo core blocks and refreshes the UI after 1s after each key press + * in a text field or immediately after a select field changes. + * We need to wait to make sure that all key presses were processed by that mechanism. + */ +export const waitForUiRefresh = ( page: Page ) => page.waitForTimeout( 1000 ); + +/** + * Takes off the focus out of the Stripe elements to let Stripe logic + * wrap up and make sure the Place Order button is clickable. + */ +export const focusPlaceOrderButton = async ( page: Page ) => { + await page.locator( '#place_order' ).focus(); + await waitForUiRefresh( page ); +}; + export const fillBillingAddress = async ( page: Page, billingAddress: CustomerAddress @@ -188,10 +206,7 @@ export const setupCheckout = async ( ) => { await navigation.goToCheckout( page ); await fillBillingAddress( page, billingAddress ); - // Woo core blocks and refreshes the UI after 1s after each key press - // in a text field or immediately after a select field changes. - // We need to wait to make sure that all key presses were processed by that mechanism. - await page.waitForTimeout( 1000 ); + await waitForUiRefresh( page ); await isUIUnblocked( page ); await page .locator( '.wc_payment_method.payment_method_woocommerce_payments' ) @@ -252,10 +267,7 @@ export const placeOrderWithCurrency = async ( await navigation.goToShopWithCurrency( page, currency ); await setupProductCheckout( page, [ [ config.products.simple.name, 1 ] ] ); await fillCardDetails( page, config.cards.basic ); - // Takes off the focus out of the Stripe elements to let Stripe logic - // wrap up and make sure the Place Order button is clickable. - await page.locator( '#place_order' ).focus(); - await page.waitForTimeout( 1000 ); + await focusPlaceOrderButton( page ); await placeOrder( page ); await page.waitForURL( /\/order-received\//, { waitUntil: 'load' } ); await expect( From f8685fb95e37731880405318cce38f5a15c911fb Mon Sep 17 00:00:00 2001 From: Eduardo Umpierre Date: Tue, 14 Jan 2025 08:54:07 -0300 Subject: [PATCH 11/11] Update the approach to ensure the checkout form is ready to be submitted --- .../specs/shopper/shopper-myaccount-renew-subscription.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts index 1ff2db72e9f..1388b66d305 100644 --- a/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts +++ b/tests/e2e-pw/specs/shopper/shopper-myaccount-renew-subscription.spec.ts @@ -68,8 +68,7 @@ describeif( shouldRunSubscriptionsTests )( await page .getByText( 'Complete checkout to renew now.' ) .isVisible(); - await page.waitForLoadState( 'networkidle' ); - + await shopper.focusPlaceOrderButton( page ); await shopper.placeOrder( page ); await expect( page.getByRole( 'heading', { name: 'Order received' } )