diff --git a/tests/qit/e2e/bootstrap/setup.sh b/tests/qit/e2e/bootstrap/setup.sh index c5730003eef..de37abe05b8 100755 --- a/tests/qit/e2e/bootstrap/setup.sh +++ b/tests/qit/e2e/bootstrap/setup.sh @@ -11,13 +11,6 @@ echo "Setting up WooPayments for E2E testing..." # Ensure environment is marked as development so dev-only CLI commands are available wp config set WP_ENVIRONMENT_TYPE development --quiet 2>/dev/null || true -echo "Installing WordPress importer for sample data..." -if ! wp plugin is-installed wordpress-importer >/dev/null 2>&1; then - wp plugin install wordpress-importer --activate -else - wp plugin activate wordpress-importer -fi - WC_SAMPLE_DATA_PATH=$(wp eval 'echo trailingslashit( WP_CONTENT_DIR ) . "plugins/woocommerce/sample-data/sample_products.xml";' 2>/dev/null) if [ -z "$WC_SAMPLE_DATA_PATH" ]; then echo "Unable to resolve WooCommerce sample data path; skipping import." @@ -30,6 +23,31 @@ else fi fi +# Import WooCommerce Subscriptions products if the plugin is installed +echo "Checking for WooCommerce Subscriptions plugin..." +if wp plugin is-installed woocommerce-subscriptions 2>/dev/null; then + echo "WooCommerce Subscriptions detected - configuring settings..." + + # Allow multiple subscriptions to be purchased in a single order + # This is required for testing scenarios where customers buy multiple subscription products + wp option update woocommerce_subscriptions_multiple_purchase "yes" + echo "✅ Enabled multiple subscription purchases" + + # Import subscription products + echo "Importing subscription products..." + # Note: /qit/bootstrap is a volume mount defined in qit.yml pointing to ./e2e/bootstrap + WC_SUBSCRIPTIONS_DATA_PATH="/qit/bootstrap/wc-subscription-products.xml" + + if [ -f "$WC_SUBSCRIPTIONS_DATA_PATH" ]; then + wp import "$WC_SUBSCRIPTIONS_DATA_PATH" --authors=skip + echo "✅ Subscription products imported successfully" + else + echo "Warning: Subscription products XML not found at $WC_SUBSCRIPTIONS_DATA_PATH" + fi +else + echo "WooCommerce Subscriptions not installed - skipping subscription products import" +fi + # Ensure WooCommerce core pages exist and capture IDs echo "Ensuring WooCommerce core pages exist..." wp wc --user=admin tool run install_pages >/dev/null 2>&1 || true @@ -88,14 +106,6 @@ wp option set woocommerce_checkout_company_field "optional" --quiet 2>/dev/null wp option set woocommerce_coming_soon "no" --quiet 2>/dev/null || true wp option set woocommerce_store_pages_only "no" --quiet 2>/dev/null || true -# Ensure Storefront theme is active for consistent storefront markup -if ! wp theme is-installed storefront > /dev/null 2>&1; then - wp theme install storefront --force -fi -wp theme activate storefront - - - # Create a test customer wp user create testcustomer test@example.com \ --role=customer \ diff --git a/tests/qit/e2e/bootstrap/wc-subscription-products.xml b/tests/qit/e2e/bootstrap/wc-subscription-products.xml new file mode 100644 index 00000000000..8644d90272b --- /dev/null +++ b/tests/qit/e2e/bootstrap/wc-subscription-products.xml @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + WooPayments E2E site + http://localhost:8084 + Just another WordPress site + Fri, 26 Aug 2022 14:13:50 +0000 + en-US + 1.2 + http://localhost:8084 + http://localhost:8084 + + 1 + + + https://wordpress.org/?v=6.0.1 + + + <![CDATA[Subscription free trial product]]> + http://localhost:8084/product/subscription-free-trial-product/ + Thu, 25 Aug 2022 15:22:43 +0000 + + http://localhost:8084/?post_type=product&p=67 + + + + 67 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <![CDATA[Subscription signup fee product]]> + http://localhost:8084/product/subscription-signup-fee-product/ + Thu, 25 Aug 2022 15:28:35 +0000 + + http://localhost:8084/?post_type=product&p=70 + + + + 70 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <![CDATA[Subscription no signup fee product]]> + http://localhost:8084/product/subscription-no-signup-fee-product/ + Fri, 26 Aug 2022 05:36:36 +0000 + + http://localhost:8084/?post_type=product&p=88 + + + + 88 + + + + + + + + + 0 + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew-action-scheduler.spec.ts b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew-action-scheduler.spec.ts new file mode 100644 index 00000000000..c87ff942afa --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew-action-scheduler.spec.ts @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { test, expect } from '../../../fixtures/auth'; + +/** + * Internal dependencies + */ +import { config } from '../../../config/default'; +import { + addToCartFromShopPage, + emptyCart, + fillCardDetails, + placeOrder, + setupCheckout, +} from '../../../utils/shopper'; +import { + goToActionScheduler, + goToSubscriptions, +} from '../../../utils/merchant'; + +test.describe( + 'Subscriptions > Renew a subscription via Action Scheduler', + { tag: [ '@critical', '@subscriptions', '@merchant' ] }, + () => { + const actionSchedulerHook = + 'woocommerce_scheduled_subscription_payment'; + + const customerBillingConfig = + config.addresses[ 'subscriptions-customer' ].billing; + + test( 'should renew a subscription with action scheduler', async ( { + customerPage, + adminPage, + } ) => { + // Step 1: Customer creates a subscription + await emptyCart( customerPage ); + await addToCartFromShopPage( + customerPage, + config.products.subscription_signup_fee + ); + await setupCheckout( customerPage, customerBillingConfig ); + await fillCardDetails( customerPage, config.cards.basic ); + await placeOrder( customerPage ); + await expect( + customerPage.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // Step 2: Merchant goes to Action Scheduler + await goToActionScheduler( adminPage, 'pending' ); + + // Search for the subscription payment hook + await adminPage + .getByLabel( 'Search hook, args and claim' ) + .fill( actionSchedulerHook ); + + await adminPage + .getByRole( 'button', { + name: 'Search hook, args and claim ID', + } ) + .click(); + + // Step 3: Run the scheduled action + await adminPage.getByRole( 'link', { name: 'Run' } ).focus(); + await adminPage.getByRole( 'link', { name: 'Run' } ).click(); + + // Verify the action ran + await expect( + adminPage.getByText( actionSchedulerHook, { exact: true } ) + ).toBeVisible(); + + // Step 4: Go to Subscriptions and verify the renewal + await goToSubscriptions( adminPage ); + + // Verify that the subscription has 2 related orders now (original + renewal) + await expect( + adminPage.getByRole( 'cell', { name: '2', exact: true } ) + ).toBeVisible(); + } ); + } +); diff --git a/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew.spec.ts b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew.spec.ts new file mode 100644 index 00000000000..23c08b8e055 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-renew.spec.ts @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { test, expect } from '../../../fixtures/auth'; + +/** + * Internal dependencies + */ +import { config } from '../../../config/default'; +import { + emptyCart, + fillCardDetails, + focusPlaceOrderButton, + placeOrder, + setupProductCheckout, +} from '../../../utils/shopper'; +import { goToSubscriptions, dataHasLoaded } from '../../../utils/merchant'; + +test.describe( + 'Subscriptions > Renew a subscription as a merchant', + { tag: [ '@critical', '@subscriptions', '@merchant' ] }, + () => { + const customerBillingConfig = + config.addresses[ 'subscriptions-customer' ].billing; + + test( 'should be able to renew a subscription', async ( { + customerPage, + adminPage, + } ) => { + // Step 1: Customer creates a subscription + await emptyCart( customerPage ); + await setupProductCheckout( + customerPage, + [ [ config.products.subscription_signup_fee, 1 ] ], + customerBillingConfig + ); + await fillCardDetails( customerPage, config.cards.basic ); + await focusPlaceOrderButton( customerPage ); + await placeOrder( customerPage ); + await customerPage.waitForURL( /\/order-received\//, { + waitUntil: 'load', + } ); + await expect( + customerPage.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // Extract subscription ID from the order confirmation page + const subscriptionId = ( + await customerPage + .getByRole( 'link', { name: 'View subscription number' } ) + .textContent() + ) + .trim() + .replace( '#', '' ); + + // Step 2: Merchant navigates to the subscription page + await goToSubscriptions( adminPage ); + await adminPage + .getByRole( 'link', { name: `#${ subscriptionId }` } ) + .click(); + await dataHasLoaded( adminPage ); + + await expect( + adminPage.getByRole( 'heading', { + name: 'Edit Subscription', + } ) + ).toBeVisible(); + + // Step 3: Merchant processes renewal + const orderActions = adminPage.locator( + 'select[name="wc_order_action"]' + ); + await orderActions.selectOption( { label: 'Process renewal' } ); + + // Prepare to accept the dialog before clicking the submit button + adminPage.on( 'dialog', async ( dialog ) => { + await dialog.accept(); + } ); + + await adminPage + .locator( '#actions' ) + .getByRole( 'button', { name: /Apply.+/i } ) + .click(); + await adminPage.waitForLoadState( 'networkidle' ); + + // Step 4: Verify renewal order was created + await expect( + adminPage.getByRole( 'cell', { name: 'Renewal Order' } ) + ).toBeVisible(); + } ); + } +); diff --git a/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-settings.spec.ts b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-settings.spec.ts new file mode 100644 index 00000000000..ad92563ccab --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/merchant/merchant-subscriptions-settings.spec.ts @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { test, expect } from '../../../fixtures/auth'; + +test.describe( 'WooCommerce > Settings > Subscriptions', () => { + test( + 'Merchant should be able to load WooCommerce Subscriptions settings tab', + { tag: [ '@merchant', '@subscriptions' ] }, + async ( { adminPage } ) => { + // Navigate to WooCommerce Settings > Subscriptions tab + await adminPage.goto( + '/wp-admin/admin.php?page=wc-settings&tab=subscriptions' + ); + + // Verify the Subscriptions menu item is visible + const menuItem = adminPage.getByRole( 'main' ).getByRole( 'link', { + name: 'Subscriptions', + exact: true, + } ); + await expect( menuItem ).toBeVisible(); + + // Verify the Subscriptions heading is visible (alternative verification) + const heading = adminPage + .getByRole( 'heading', { + name: 'Subscriptions', + } ) + .first(); + await expect( heading ).toBeVisible(); + } + ); +} ); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.ts new file mode 100644 index 00000000000..8b742a09067 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-myaccount-renew-subscription.spec.ts @@ -0,0 +1,107 @@ +/** + * External dependencies + */ +import { expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { test } from '../../../fixtures/auth'; +import { config } from '../../../config/default'; +import { + fillCardDetails, + focusPlaceOrderButton, + placeOrder, + setupCheckout, +} from '../../../utils/shopper'; +import { + goToProductPageBySlug, + goToSubscriptions, +} from '../../../utils/shopper-navigation'; + +test.describe( + 'Subscriptions > Renew a subscription in my account', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + () => { + const customerBillingAddress = + config.addresses[ 'subscriptions-customer' ].billing; + + let subscriptionId: string; + + test( 'should be able to purchase a subscription', async ( { + customerPage, + } ) => { + // Navigate directly to the subscription product page + await goToProductPageBySlug( + customerPage, + 'subscription-signup-fee-product' + ); + + // Add to cart from product page - target the main product form + const addToCartButton = customerPage + .locator( '.summary.entry-summary' ) + .getByRole( 'button', { + name: /Sign up now|Add to cart/i, + exact: false, + } ); + + await addToCartButton.click(); + + // Wait for product to be added + await expect( + customerPage.getByText( /has been added to your cart/i ) + ).toBeVisible(); + + // Proceed to checkout + await setupCheckout( customerPage, customerBillingAddress ); + + // Fill card details + await fillCardDetails( customerPage, config.cards.basic ); + + // Place order + await focusPlaceOrderButton( customerPage ); + await placeOrder( customerPage ); + + // Wait for order confirmation + await expect( + customerPage.getByRole( 'heading', { + name: 'Order received', + } ) + ).toBeVisible(); + + // Extract subscription ID from the order confirmation page + subscriptionId = await customerPage + .getByLabel( 'View subscription number' ) + .innerText(); + } ); + + test( 'should be able to renew a subscription in my account', async ( { + customerPage, + } ) => { + await goToSubscriptions( customerPage ); + + if ( ! subscriptionId ) { + throw new Error( 'Subscription ID is not set' ); + } + + const numericSubscriptionId = subscriptionId.substring( 1 ); + + await customerPage + .getByLabel( + `View subscription number ${ numericSubscriptionId }` + ) + .click(); + + await customerPage.getByText( 'Renew now' ).click(); + await expect( + customerPage.getByText( 'Complete checkout to renew now.' ) + ).toBeVisible(); + await focusPlaceOrderButton( customerPage ); + await placeOrder( customerPage ); + await customerPage.waitForURL( /\/order-received\// ); + await expect( + customerPage.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + } ); + } +); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-manage-payments.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-manage-payments.spec.ts new file mode 100644 index 00000000000..a99a1306106 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-manage-payments.spec.ts @@ -0,0 +1,156 @@ +/** + * External dependencies + */ +import { test, expect } from '../../../fixtures/auth'; +import type { Page } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { config } from '../../../config/default'; +import { + fillCardDetails, + focusPlaceOrderButton, + placeOrderWithOptions, +} from '../../../utils/shopper'; +import { goToSubscriptions } from '../../../utils/shopper-navigation'; + +/** + * Navigate to the subscription change payment method page. + * + * @param {Page} page The Playwright page object. + * @param {string} subscriptionId The subscription ID. + */ +const navigateToSubscriptionDetails = async ( + page: Page, + subscriptionId: string +) => { + await goToSubscriptions( page ); + await page + .getByLabel( `View subscription number ${ subscriptionId }` ) + .click(); + + await page.getByRole( 'link', { name: 'Change payment' } ).click(); + + await expect( + page.getByRole( 'heading', { + name: 'Change payment method', + } ) + ).toBeVisible(); + + await expect( + page.getByText( 'Choose a new payment method' ) + ).toBeVisible(); +}; + +test.describe( 'Subscriptions > Manage payment methods', () => { + const customerBillingAddress = + config.addresses[ 'subscriptions-customer' ].billing; + + test( + 'should change a default payment method to a new one', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + // Purchase a subscription first + await placeOrderWithOptions( customerPage, { + product: config.products.subscription_no_signup_fee, + billingAddress: customerBillingAddress, + } ); + + // Extract subscription ID from the order confirmation page + const subscriptionId = ( + await customerPage + .getByLabel( 'View subscription number' ) + .innerText() + ) + .trim() + .replace( '#', '' ); + + // Navigate to change payment method page + await navigateToSubscriptionDetails( customerPage, subscriptionId ); + + // Select "Use a new payment method" option + await customerPage.getByLabel( 'Use a new payment method' ).check(); + + // Fill in new card details + await fillCardDetails( customerPage, config.cards.basic2 ); + + // Focus and submit the form - for subscription payment changes, we just click + await focusPlaceOrderButton( customerPage ); + await customerPage.locator( '#place_order' ).click(); + + // Wait for navigation back to subscription page + await customerPage.waitForURL( + /\/my-account\/view-subscription\// + ); + await customerPage.waitForLoadState( 'networkidle' ); + + // Verify success message - can be in different notice containers + await expect( + customerPage + .locator( + '.woocommerce-message, .woocommerce-notice--success' + ) + .filter( { hasText: 'Payment method updated' } ) + ).toBeVisible(); + + // Verify we're back on the subscription view page + await expect( + customerPage.getByRole( 'heading', { + name: `Subscription #${ subscriptionId }`, + } ) + ).toBeVisible(); + } + ); + + test( + 'should set a payment method to an already saved card', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + // Purchase a subscription first + await placeOrderWithOptions( customerPage, { + product: config.products.subscription_no_signup_fee, + billingAddress: customerBillingAddress, + } ); + + // Extract subscription ID from the order confirmation page + const subscriptionId = ( + await customerPage + .getByLabel( 'View subscription number' ) + .innerText() + ) + .trim() + .replace( '#', '' ); + + // Navigate to change payment method page + await navigateToSubscriptionDetails( customerPage, subscriptionId ); + + // The first saved card should already be selected + // Focus and submit the form to use the already saved card + await focusPlaceOrderButton( customerPage ); + await customerPage.locator( '#place_order' ).click(); + + // Wait for navigation back to subscription page + await customerPage.waitForURL( + /\/my-account\/view-subscription\// + ); + await customerPage.waitForLoadState( 'networkidle' ); + + // Verify success message - can be in different notice containers + await expect( + customerPage + .locator( + '.woocommerce-message, .woocommerce-notice--success' + ) + .filter( { hasText: 'Payment method updated' } ) + ).toBeVisible(); + + // Verify we're back on the subscription view page + await expect( + customerPage.getByRole( 'heading', { + name: `Subscription #${ subscriptionId }`, + } ) + ).toBeVisible(); + } + ); +} ); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.ts new file mode 100644 index 00000000000..0f8c574bb1c --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-free-trial.spec.ts @@ -0,0 +1,176 @@ +/** + * External dependencies + */ +import { expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { test } from '../../../fixtures/auth'; +import { config } from '../../../config/default'; +import { + confirmCardAuthentication, + emptyCart, + fillCardDetails, + setupCheckout, +} from '../../../utils/shopper'; +import { + goToCart, + goToProductPageBySlug, +} from '../../../utils/shopper-navigation'; +import { goToOrder, goToSubscriptions } from '../../../utils/merchant'; + +// Calculate dates for 14-day free trial +const nowLocal = new Date(); +const nowUTC = new Date( + nowLocal.getUTCFullYear(), + nowLocal.getUTCMonth(), + nowLocal.getUTCDate() +); +const formatter = new Intl.DateTimeFormat( 'en-US', { + dateStyle: 'long', +} ); +const renewalDate = nowUTC.setDate( nowUTC.getDate() + 14 ); +const renewalDateFormatted = formatter.format( renewalDate ); + +const productName = 'Subscription free trial product'; +const productSlug = 'subscription-free-trial-product'; + +test.describe( 'Subscriptions > Purchase Free Trial', () => { + let orderId: string; + let subscriptionId: string; + + test( + 'Shopper should be able to purchase a free trial subscription', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + const customerBilling = config.addresses.customer.billing; + + // Empty cart to ensure clean state + await emptyCart( customerPage ); + + // Open the subscription product and verify free trial is shown + await goToProductPageBySlug( customerPage, productSlug ); + await expect( + customerPage + .locator( '.product' ) + .getByText( '/ month with a 14-day free trial' ) + ).toBeVisible(); + + // Add to cart and verify cart shows free trial details + await customerPage + .getByRole( 'button', { name: 'Add to cart', exact: true } ) + .click(); + await goToCart( customerPage ); + await expect( + customerPage + .getByText( '/ month with a 14-day free trial' ) + .first() + ).toBeVisible(); + + // Verify first renewal date is 14 days from now + await expect( + customerPage.getByText( + `First renewal: ${ renewalDateFormatted }` + ) + ).toBeVisible(); + + // Verify order total is $0.00 (free trial) + await expect( + customerPage + .getByRole( 'row', { + name: 'Total $0.00', + exact: true, + } ) + .locator( 'td' ) + ).toBeVisible(); + + // Proceed to checkout and verify free trial details + await setupCheckout( customerPage, customerBilling ); + await expect( + customerPage + .locator( '#order_review' ) + .getByText( '/ month with a 14-day free trial' ) + ).toBeVisible(); + await expect( + customerPage.getByText( + `First renewal: ${ renewalDateFormatted }` + ) + ).toBeVisible(); + + // Pay using a 3DS card + const card = config.cards[ '3dsOTP' ]; + await fillCardDetails( customerPage, card ); + await customerPage + .getByRole( 'button', { name: 'Place order', exact: true } ) + .click(); + + // Handle 3DS authentication + await customerPage.frames()[ 0 ].waitForLoadState( 'load' ); + await confirmCardAuthentication( customerPage, true ); + await customerPage.frames()[ 0 ].waitForLoadState( 'networkidle' ); + await customerPage.waitForLoadState( 'networkidle' ); + + // Verify order received + await expect( + customerPage.getByRole( 'heading', { + name: 'Order received', + } ) + ).toBeVisible(); + + // Extract order and subscription IDs for merchant verification + orderId = ( + await customerPage.getByText( 'Order number:' ).innerText() + ) + .replace( /[^0-9]/g, '' ) + .trim(); + subscriptionId = ( + await customerPage + .getByLabel( 'View subscription number' ) + .textContent() + ) + .trim() + .replace( '#', '' ); + } + ); + + test( + 'Merchant should see active subscription with Setup Intent', + { tag: [ '@subscriptions', '@merchant' ] }, + async ( { adminPage } ) => { + // Verify order has Setup Intent (seti_) for free trial + await goToOrder( adminPage, orderId ); + await expect( + adminPage.locator( '.woocommerce-order-data__meta' ) + ).toContainText( 'seti_' ); + + // Navigate to subscriptions and verify subscription details + await goToSubscriptions( adminPage ); + const subscriptionRow = adminPage.getByRole( 'row', { + name: '#' + subscriptionId, + } ); + + // Verify subscription is active + await expect( subscriptionRow.locator( 'mark' ) ).toHaveText( + 'Active' + ); + + // Verify product name + await expect( + subscriptionRow.getByRole( 'cell', { name: productName } ) + ).toBeVisible(); + + // Verify recurring amount + await expect( + subscriptionRow.getByRole( 'cell', { + name: /\$9\.99 \/ month/, + } ) + ).toBeVisible(); + + // Verify renewal date appears twice (next payment + end date) + await expect( + subscriptionRow.getByText( renewalDateFormatted ) + ).toHaveCount( 2 ); + } + ); +} ); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-multiple-subscriptions.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-multiple-subscriptions.spec.ts new file mode 100644 index 00000000000..31484b42103 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-multiple-subscriptions.spec.ts @@ -0,0 +1,112 @@ +/** + * External dependencies + */ +import { expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { test } from '../../../fixtures/auth'; +import { config } from '../../../config/default'; +import { + emptyCart, + fillCardDetails, + placeOrder, + setupProductCheckout, +} from '../../../utils/shopper'; +import { goToSubscriptions } from '../../../utils/shopper-navigation'; + +test.describe( 'Subscriptions > Purchase multiple subscriptions', () => { + test( + 'should be able to purchase multiple subscriptions', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + const customerBillingAddress = config.addresses.customer.billing; + + // Empty cart to ensure clean state + await emptyCart( customerPage ); + + // Add both subscription products to cart and proceed to checkout + // Using setupProductCheckout which adds products from the shop page + await setupProductCheckout( + customerPage, + [ + [ config.products.subscription_no_signup_fee, 1 ], + [ config.products.subscription_signup_fee, 1 ], + ], + customerBillingAddress, + 'USD' + ); + + // Fill card details and place order + await fillCardDetails( customerPage, config.cards.basic ); + await placeOrder( customerPage ); + + // Wait for order confirmation + await expect( + customerPage.getByRole( 'heading', { + name: 'Order received', + } ) + ).toBeVisible(); + + // Get the subscription ID from the order confirmation page + const subscriptionId = ( + await customerPage + .getByLabel( 'View subscription number' ) + .innerText() + ) + .trim() + .replace( '#', '' ); + + // Navigate to subscriptions page + await goToSubscriptions( customerPage ); + + // Find the subscription row by ID + const latestSubscriptionRow = customerPage.getByRole( 'row', { + name: `subscription number ${ subscriptionId }`, + } ); + + await expect( latestSubscriptionRow ).toBeVisible(); + + // Click to view subscription details + await latestSubscriptionRow + .getByRole( 'link', { + name: 'View', + } ) + .nth( 0 ) + .click(); + + await customerPage.waitForLoadState( 'networkidle' ); + + // Verify the subscription details page shows both products + // Check for the order_details table with line items + const subTotalsRows = customerPage.locator( + '.order_details tr.order_item' + ); + + // Verify we have 2 products in one subscription + await expect( subTotalsRows ).toHaveCount( 2 ); + + // Verify both products show $9.99/month + for ( let i = 0; i < ( await subTotalsRows.count() ); i++ ) { + const row = subTotalsRows.nth( i ); + await expect( row.locator( '.product-total' ) ).toContainText( + '$9.99 / month' + ); + } + + // Verify total recurring amount ($19.98/month for both products) + await expect( + customerPage + .getByRole( 'row', { name: /total:/i } ) + .getByRole( 'cell' ) + .nth( 1 ) + ).toContainText( /\$19\.98.*\/ month/i ); + + // Verify related order total (recurring + signup fee) + await expect( + customerPage.getByText( /\$21\.97.*for 2 items/i ) + ).toBeVisible(); + } + ); +} ); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-no-signup-fee.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-no-signup-fee.spec.ts new file mode 100644 index 00000000000..5554f314a68 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-no-signup-fee.spec.ts @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { test } from '../../../fixtures/auth'; +import { config } from '../../../config/default'; +import { + fillCardDetails, + placeOrder, + setupCheckout, +} from '../../../utils/shopper'; +import { goToProductPageBySlug } from '../../../utils/shopper-navigation'; +import { goToOrder, goToPaymentDetails } from '../../../utils/merchant'; + +test.describe( + 'Subscriptions > Purchase subscription without signup fee', + () => { + let orderId: string; + + const productName = 'Subscription no signup fee product'; + const productSlug = 'subscription-no-signup-fee-product'; + const customerBillingAddress = + config.addresses[ 'subscriptions-customer' ].billing; + + test( + 'should be able to purchase a subscription without a signup fee', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + // Navigate directly to the subscription product page + await goToProductPageBySlug( customerPage, productSlug ); + + // Add to cart from product page + await customerPage + .getByRole( 'button', { name: 'Add to cart', exact: true } ) + .click(); + + // Wait for product to be added - check for success message + await expect( + customerPage.getByText( /has been added to your cart/i ) + ).toBeVisible(); + + // Proceed to checkout + await setupCheckout( customerPage, customerBillingAddress ); + + // Fill card details + await fillCardDetails( customerPage, config.cards.basic ); + + // Place order + await placeOrder( customerPage ); + + // Wait for order confirmation + await expect( + customerPage.getByRole( 'heading', { + name: 'Order received', + } ) + ).toBeVisible(); + + // Extract order ID from URL + const url = await customerPage.url(); + orderId = url.match( /\/order-received\/(\d+)\// )?.[ 1 ] ?? ''; + } + ); + + test( + 'should have a charge for subscription cost without fee & an active subscription', + { tag: [ '@subscriptions', '@merchant' ] }, + async ( { adminPage } ) => { + await goToOrder( adminPage, orderId ); + + // Verify we have an active subscription in the "Related Orders" section + // In HPOS (High-Performance Order Storage), subscriptions appear as related orders + await expect( + adminPage.getByRole( 'row', { + name: /Subscription.*Active.*\$9\.99/, + } ) + ).toBeVisible(); + + // Get the payment intent ID - for subscriptions without signup fee, this should be a payment intent (pi_) + // Use .first() to handle multiple payment intent links (appears in both order data and notes) + const paymentIntentLink = adminPage + .getByRole( 'link', { + name: /pi_/, + } ) + .first(); + + // Verify payment intent exists and get its ID + await expect( paymentIntentLink ).toBeVisible(); + const paymentIntentId = await paymentIntentLink.innerText(); + + // Navigate to payment details page + await goToPaymentDetails( adminPage, paymentIntentId ); + + // Verify the payment was successful with correct amount (no signup fee, so just $9.99) + await expect( + adminPage.getByText( + /A payment of \$9\.99( USD)? was successfully charged./ + ) + ).toBeVisible(); + } + ); + } +); diff --git a/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-sign-up-fee.spec.ts b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-sign-up-fee.spec.ts new file mode 100644 index 00000000000..1418e668e71 --- /dev/null +++ b/tests/qit/e2e/specs/subscriptions/shopper/shopper-subscriptions-purchase-sign-up-fee.spec.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { expect } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { test } from '../../../fixtures/auth'; +import { config } from '../../../config/default'; +import { + fillCardDetails, + placeOrder, + setupCheckout, +} from '../../../utils/shopper'; +import { goToProductPageBySlug } from '../../../utils/shopper-navigation'; +import { goToOrder, goToPaymentDetails } from '../../../utils/merchant'; + +test.describe( 'Subscriptions > Purchase subscription with signup fee', () => { + let orderId: string; + + const customerBillingAddress = + config.addresses[ 'subscriptions-customer' ].billing; + + test( + 'should be able to purchase a subscription with signup fee', + { tag: [ '@critical', '@subscriptions', '@shopper' ] }, + async ( { customerPage } ) => { + // Navigate directly to the subscription product page + await goToProductPageBySlug( + customerPage, + 'subscription-signup-fee-product' + ); + + // Add to cart from product page - target the main product form, not sidebar widgets + // Subscription products may have "Sign up now" button instead of "Add to cart" + const addToCartButton = customerPage + .locator( '.summary.entry-summary' ) + .getByRole( 'button', { + name: /Sign up now|Add to cart/i, + exact: false, + } ); + + await addToCartButton.click(); + + // Wait for product to be added - check for success message + await expect( + customerPage.getByText( /has been added to your cart/i ) + ).toBeVisible(); + + // Proceed to checkout + await setupCheckout( customerPage, customerBillingAddress ); + + // Fill card details + await fillCardDetails( customerPage, config.cards.basic ); + + // Place order + await placeOrder( customerPage ); + + // Wait for order confirmation + await expect( + customerPage.getByRole( 'heading', { + name: 'Order received', + } ) + ).toBeVisible(); + + // Extract order ID from URL + const url = await customerPage.url(); + orderId = url.match( /\/order-received\/(\d+)\// )?.[ 1 ] ?? ''; + } + ); + + test( + 'should have a charge for subscription cost with fee & an active subscription', + { tag: [ '@subscriptions', '@merchant' ] }, + async ( { adminPage } ) => { + await goToOrder( adminPage, orderId ); + + // Verify we have an active subscription in the "Related Orders" section + // In HPOS (High-Performance Order Storage), subscriptions appear as related orders + await expect( + adminPage.getByRole( 'row', { + name: /Subscription.*Active.*\$9\.99/, + } ) + ).toBeVisible(); + + // Get the payment intent ID - for subscriptions with signup fee, this should be a payment intent (pi_) + // Use .first() to handle multiple payment intent links (appears in both order data and notes) + const paymentIntentLink = adminPage + .getByRole( 'link', { + name: /pi_/, + } ) + .first(); + + // Verify payment intent exists and get its ID + await expect( paymentIntentLink ).toBeVisible(); + const paymentIntentId = await paymentIntentLink.innerText(); + + // Navigate to payment details page + await goToPaymentDetails( adminPage, paymentIntentId ); + + // Verify the payment was successful with correct amount + await expect( + adminPage.getByText( + /A payment of \$11\.98( USD)? was successfully charged./ + ) + ).toBeVisible(); + } + ); +} ); diff --git a/tests/qit/e2e/utils/merchant.ts b/tests/qit/e2e/utils/merchant.ts index 3abd80a27e6..3390503856e 100644 --- a/tests/qit/e2e/utils/merchant.ts +++ b/tests/qit/e2e/utils/merchant.ts @@ -807,3 +807,24 @@ export const activateTheme = async ( slug: string ) => { // Theme activation failed, but we don't want to crash the test } }; + +export const goToSubscriptions = async ( page: Page ) => { + const wooCoreVersion = process.env.E2E_WC_VERSION; + const subscriptionsUrl = + wooCoreVersion === '7.7.0' + ? '/wp-admin/edit.php?post_type=shop_subscription' + : '/wp-admin/admin.php?page=wc-orders--shop_subscription'; + await page.goto( subscriptionsUrl, { + waitUntil: 'load', + } ); + await dataHasLoaded( page ); +}; + +export const goToActionScheduler = async ( page: Page, status = 'pending' ) => { + await page.goto( + `/wp-admin/tools.php?page=action-scheduler&status=${ status }`, + { + waitUntil: 'load', + } + ); +}; diff --git a/tests/qit/e2e/utils/shopper.ts b/tests/qit/e2e/utils/shopper.ts index d81bf54a74a..5d482e5365b 100644 --- a/tests/qit/e2e/utils/shopper.ts +++ b/tests/qit/e2e/utils/shopper.ts @@ -415,11 +415,13 @@ export const addToCartFromShopPage = async ( currency, } ); - // This generic regex will match the aria-label for the "Add to cart" button for any product. + // This generic regex will match the aria-label for the "Add to cart" button for any product, + // including subscription products which may use "Sign up now" instead. // It should work for WC 7.7.0 and later. - // These unicode characters are the smart (or curly) quotes: “ ”. + // These unicode characters are the smart (or curly) quotes: " ". const addToCartRegex = new RegExp( - `Add\\s+(?:to\\s+cart:\\s*)?\u201C${ product.name }\u201D(?:\\s+to\\s+your\\s+cart)?` + `(?:Add\\s+(?:to\\s+cart:|Sign\\s+up\\s+now:)\\s*)?\u201C${ product.name }\u201D(?:\\s+to\\s+your\\s+cart)?`, + 'i' ); const addToCartButton = page.getByLabel( addToCartRegex ); diff --git a/tests/qit/qit.yml b/tests/qit/qit.yml index 57092a4dd9e..9f46eae8a43 100644 --- a/tests/qit/qit.yml +++ b/tests/qit/qit.yml @@ -13,9 +13,15 @@ php_version: "8.3" plugin: - "woocommerce" - "jetpack" + - "woocommerce-subscriptions" + - "wordpress-importer" + +# Theme dependency +theme: + - "storefront" # Mount bootstrap directory for easier access in setup scripts. -# This mounts ./e2e/bootstrap (relative to this qit.yml file) to /qit/bootstrap +# This mounts tests/qit/e2e/bootstrap (relative to project root) to /qit/bootstrap # inside the QIT test container (read-only for safety). volumes: - "./tests/qit/e2e/bootstrap:/qit/bootstrap:ro"