From eb493087819feee2ff48d7177753000d5edd39aa Mon Sep 17 00:00:00 2001 From: Veljko V Date: Wed, 10 Jan 2024 20:36:11 +0100 Subject: [PATCH] Add new E2E tests to cover Shopper Checkout Block flow (#41218) * Add few E2E tests to cover half of the milestone * Improve welcome modal closing * Add new test scenarios * Add more tests to cover creating account and shipping * Update shipping zone labels to fulfil latest design changes * Add tests for checkout block coupons and cart block coupons * Remove only flag leftover * Add checkout block tax tests and merge with cart test * Update selectors to match checkout page * Update checkout block test and comment scenario until issue resolved * Remove only leftover * Update checkout block test * Improve filling shipping and billing checkout * Create order via API with coupon * Include checkout filling util helper * Update test to use get by role for alerts * Remove only leftover * Adjust scenario for placing an order as a customer * Improve existing customer placing order scenario * Add util helper for adding products to cart * Remove leftover from testing --------- Co-authored-by: Jon Lane --- .../changelog/e2e-add-tests-checkout-blocks | 4 + .../tests/shopper/cart-block-coupons.spec.js | 75 +- ...cart-checkout-block-calculate-tax.spec.js} | 530 ++++++++--- .../shopper/checkout-block-coupons.spec.js | 317 +++++++ .../tests/shopper/checkout-block.spec.js | 865 ++++++++++++++++++ .../e2e-pw/tests/shopper/checkout.spec.js | 26 +- .../tests/e2e-pw/utils/checkout.js | 79 ++ plugins/woocommerce/tests/e2e-pw/utils/pdp.js | 26 + 8 files changed, 1743 insertions(+), 179 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-add-tests-checkout-blocks rename plugins/woocommerce/tests/e2e-pw/tests/shopper/{cart-block-calculate-tax.spec.js => cart-checkout-block-calculate-tax.spec.js} (54%) create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js create mode 100644 plugins/woocommerce/tests/e2e-pw/utils/checkout.js create mode 100644 plugins/woocommerce/tests/e2e-pw/utils/pdp.js diff --git a/plugins/woocommerce/changelog/e2e-add-tests-checkout-blocks b/plugins/woocommerce/changelog/e2e-add-tests-checkout-blocks new file mode 100644 index 00000000000..5795ce37ed5 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-add-tests-checkout-blocks @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Adds tests for shopper checkout block flows \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js index ee888653776..ded66031140 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js @@ -3,7 +3,7 @@ const { admin } = require( '../../test-data/data' ); const { closeWelcomeModal } = require( '../../utils/editor' ); const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; -const simpleProductName = 'A Simple Product'; +const simpleProductName = 'Cart Coupons Product'; const singleProductFullPrice = '110.00'; const singleProductSalePrice = '55.00'; const coupons = [ @@ -23,11 +23,15 @@ const coupons = [ amount: '10.00', }, ]; +const couponLimitedCode = '10fixedcartlimited'; +const customerBilling = { + email: 'john.doe.merchant.test@example.com', +}; const pageTitle = 'Cart Block'; const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); -let product1Id; +let productId, orderId, limitedCouponId; test.describe( 'Cart Block Applying Coupons', () => { const couponBatchId = new Array(); @@ -52,7 +56,7 @@ test.describe( 'Cart Block Applying Coupons', () => { sale_price: singleProductSalePrice, } ) .then( ( response ) => { - product1Id = response.data.id; + productId = response.data.id; } ); // add coupons await api @@ -64,6 +68,32 @@ test.describe( 'Cart Block Applying Coupons', () => { couponBatchId.push( response.data.create[ i ].id ); } } ); + // add limited coupon + await api + .post( 'coupons', { + code: couponLimitedCode, + discount_type: 'fixed_cart', + amount: '10.00', + usage_limit: 1, + usage_count: 1, + } ) + .then( ( response ) => { + limitedCouponId = response.data.id; + } ); + // add order with applied limited coupon + await api + .post( 'orders', { + status: 'processing', + billing: customerBilling, + coupon_lines: [ + { + code: couponLimitedCode, + }, + ], + } ) + .then( ( response ) => { + orderId = response.data.id; + } ); } ); test.afterAll( async ( { baseURL } ) => { @@ -74,9 +104,14 @@ test.describe( 'Cart Block Applying Coupons', () => { version: 'wc/v3', } ); await api.post( 'products/batch', { - delete: [ product1Id ], + delete: [ productId ], + } ); + await api.post( 'coupons/batch', { + delete: [ ...couponBatchId, limitedCouponId ], + } ); + await api.post( 'orders/batch', { + delete: [ orderId ], } ); - await api.post( 'coupons/batch', { delete: [ ...couponBatchId ] } ); } ); test.beforeEach( async ( { context } ) => { @@ -121,7 +156,7 @@ test.describe( 'Cart Block Applying Coupons', () => { } ) => { const totals = [ '$50.00', '$27.50', '$45.00' ]; // add product to cart block - await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + await page.goto( `/shop/?add-to-cart=${ productId }` ); await page.waitForLoadState( 'networkidle' ); await page.goto( pageSlug ); await expect( @@ -165,7 +200,7 @@ test.describe( 'Cart Block Applying Coupons', () => { const totalsReverse = [ '$17.50', '$45.00', '$55.00' ]; const discounts = [ '-$5.00', '-$32.50', '-$42.50' ]; // add product to cart block - await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + await page.goto( `/shop/?add-to-cart=${ productId }` ); await page.waitForLoadState( 'networkidle' ); await page.goto( pageSlug ); await expect( @@ -214,7 +249,7 @@ test.describe( 'Cart Block Applying Coupons', () => { page, } ) => { // add product to cart block - await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + await page.goto( `/shop/?add-to-cart=${ productId }` ); await page.waitForLoadState( 'networkidle' ); await page.goto( pageSlug ); await expect( @@ -247,4 +282,28 @@ test.describe( 'Cart Block Applying Coupons', () => { ) ).toBeVisible(); } ); + + test( 'prevents cart block applying coupon with usage limit', async ( { + page, + } ) => { + // add product to cart block and go to cart + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // add coupon with usage limit + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( couponLimitedCode ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .getByRole( 'alert' ) + .getByText( 'Coupon usage limit has been reached.' ) + ).toBeVisible(); + } ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-tax.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js similarity index 54% rename from plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-tax.spec.js rename to plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js index 87cf2d4e4d0..601919e0069 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-tax.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-block-calculate-tax.spec.js @@ -8,8 +8,14 @@ const productPrice = '100.00'; const messyProductPrice = '13.47'; const secondProductName = 'Second Product Cart Block Taxing'; -const pageTitle = 'Cart Block'; -const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); +const cartBlockPageTitle = 'Cart Block'; +const cartBlockPageSlug = cartBlockPageTitle + .replace( / /gi, '-' ) + .toLowerCase(); +const checkoutBlockPageTitle = 'Checkout Block'; +const checkoutBlockPageSlug = checkoutBlockPageTitle + .replace( / /gi, '-' ) + .toLowerCase(); let productId, productId2, @@ -24,7 +30,7 @@ let productId, shippingZoneId, shippingMethodId; -test.describe( 'Shopper Cart Block Tax Display', () => { +test.describe( 'Shopper Cart & Checkout Block Tax Display', () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { url: baseURL, @@ -111,7 +117,7 @@ test.describe( 'Shopper Cart Block Tax Display', () => { await page .getByRole( 'textbox', { name: 'Add title' } ) - .fill( pageTitle ); + .fill( cartBlockPageTitle ); await page.getByRole( 'button', { name: 'Add default block' } ).click(); await page .getByRole( 'document', { @@ -127,32 +133,92 @@ test.describe( 'Shopper Cart Block Tax Display', () => { .getByRole( 'button', { name: 'Publish', exact: true } ) .click(); await expect( - page.getByText( `${ pageTitle } is now live.` ) + page.getByText( `${ cartBlockPageTitle } is now live.` ) ).toBeVisible(); } ); - test( 'that inclusive tax is displayed properly in Cart Block page', async ( { + test( 'can create Checkout Block page', async ( { page } ) => { + // create a new page with checkout block + await page.goto( 'wp-admin/post-new.php?post_type=page' ); + await page.waitForLoadState( 'networkidle' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log In' ).click(); + + // Close welcome popup if prompted + try { + await page + .getByLabel( 'Close', { exact: true } ) + .click( { timeout: 5000 } ); + } catch ( error ) { + console.log( "Welcome modal wasn't present, skipping action." ); + } + + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( checkoutBlockPageTitle ); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page + .getByRole( 'document', { + name: 'Empty block; start writing or type forward slash to choose a block', + } ) + .fill( '/checkout' ); + await page.keyboard.press( 'Enter' ); + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await expect( + page.getByText( `${ checkoutBlockPageTitle } is now live.` ) + ).toBeVisible(); + } ); + + test( 'that inclusive tax is displayed properly in blockbased Cart & Checkout pages', async ( { page, } ) => { - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$125.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$125.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item-tax' ) - ).toHaveText( 'Including $25.00 Nasty Tax' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item-tax' ) + ).toHaveText( 'Including $25.00 Nasty Tax' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item-tax' ) + ).toHaveText( 'Including $25.00 Nasty Tax' ); + } ); } ); - test( 'that exclusive tax is displayed properly in Cart Block page', async ( { + test( 'that exclusive tax is displayed properly in blockbased Cart & Checkout pages', async ( { page, baseURL, } ) => { @@ -165,26 +231,48 @@ test.describe( 'Shopper Cart Block Tax Display', () => { await api.put( 'settings/tax/woocommerce_tax_display_cart', { value: 'excl', } ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$100.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$125.00' ); - await expect( - page.locator( '.wc-block-components-totals-taxes' ) - ).toContainText( '$25.00' ); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-taxes' ) + ).toContainText( '$25.00' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$125.00' ); + await expect( + page.locator( '.wc-block-components-totals-taxes' ) + ).toContainText( '$25.00' ); + } ); } ); } ); -test.describe( 'Shopper Cart Block Tax Rounding', () => { +test.describe( 'Shopper Cart & Checkout Block Tax Rounding', () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { url: baseURL, @@ -294,7 +382,7 @@ test.describe( 'Shopper Cart Block Tax Rounding', () => { } ); } ); - test( 'that tax rounding is present at subtotal level', async ( { + test( 'that tax rounding is present at subtotal level in blockbased Cart & Checkout pages', async ( { page, baseURL, } ) => { @@ -313,25 +401,47 @@ test.describe( 'Shopper Cart Block Tax Rounding', () => { await api.put( 'settings/tax/woocommerce_tax_total_display', { value: 'single', } ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$40.41' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$50.12' ); - await expect( - page.locator( '.wc-block-components-totals-taxes' ) - ).toContainText( '$9.71' ); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$40.41' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$50.12' ); + await expect( + page.locator( '.wc-block-components-totals-taxes' ) + ).toContainText( '$9.71' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$40.41' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$50.12' ); + await expect( + page.locator( '.wc-block-components-totals-taxes' ) + ).toContainText( '$9.71' ); + } ); } ); - test( 'that tax rounding is off at subtotal level', async ( { + test( 'that tax rounding is off at subtotal level in blockbased Cart & Checkout pages', async ( { page, baseURL, } ) => { @@ -350,33 +460,62 @@ test.describe( 'Shopper Cart Block Tax Rounding', () => { await api.put( 'settings/tax/woocommerce_tax_total_display', { value: 'itemized', } ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$40.41' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$50.12' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$6.87' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$2.84' ); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$40.41' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$50.12' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$6.87' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$2.84' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$40.41' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$50.12' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$6.87' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$2.84' ); + } ); } ); } ); -test.describe( 'Shopper Cart Block Tax Levels', () => { +test.describe( 'Shopper Cart & Checkout Block Tax Levels', () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { url: baseURL, @@ -558,41 +697,78 @@ test.describe( 'Shopper Cart Block Tax Levels', () => { 'Shipping costs updated.' ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$100.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$118.75' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$10.00' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$5.00' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$2.50' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$1.25' ); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$118.75' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$10.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$5.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$2.50' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$1.25' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$118.75' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$10.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$5.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$2.50' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$1.25' ); + } ); } ); - test( 'that applying taxes in Cart Block of 2 different levels (2 excluded) calculates properly', async ( { + test( 'that applying taxes in blockbased Cart & Checkout of 2 different levels (2 excluded) calculates properly', async ( { page, baseURL, } ) => { @@ -605,32 +781,60 @@ test.describe( 'Shopper Cart Block Tax Levels', () => { await api.put( 'settings/tax/woocommerce_tax_total_display', { value: 'itemized', } ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$100.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$115.00' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$10.00' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-taxes-block' - ) - ).toContainText( '$5.00' ); + + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$115.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$10.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-taxes-block' + ) + ).toContainText( '$5.00' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$100.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$115.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$10.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-taxes-block' + ) + ).toContainText( '$5.00' ); + } ); } ); } ); -test.describe( 'Shipping Cart Block Tax', () => { +test.describe( 'Shipping Cart & Checkout Block Tax', () => { test.beforeAll( async ( { baseURL } ) => { const api = new wcApi( { url: baseURL, @@ -743,23 +947,47 @@ test.describe( 'Shipping Cart Block Tax', () => { await api.put( 'settings/tax/woocommerce_tax_display_cart', { value: 'incl', } ); - await page.goto( pageSlug ); - await expect( - page.getByRole( 'heading', { name: pageTitle } ) - ).toBeVisible(); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-subtotal-block' - ) - ).toContainText( '$115.00' ); - await expect( - page.locator( - '.wp-block-woocommerce-cart-order-summary-shipping-block' - ) - ).toContainText( '$23.00' ); - await expect( - page.locator( '.wc-block-components-totals-footer-item' ) - ).toContainText( '$138.00' ); + await test.step( 'Load cart page and confirm price display', async () => { + await page.goto( cartBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: cartBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-subtotal-block' + ) + ).toContainText( '$115.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-cart-order-summary-shipping-block' + ) + ).toContainText( '$23.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$138.00' ); + } ); + + await test.step( 'Load checkout page and confirm price display', async () => { + await page.goto( checkoutBlockPageSlug ); + await expect( + page.getByRole( 'heading', { name: checkoutBlockPageTitle } ) + ).toBeVisible(); + + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-subtotal-block' + ) + ).toContainText( '$115.00' ); + await expect( + page.locator( + '.wp-block-woocommerce-checkout-order-summary-shipping-block' + ) + ).toContainText( '$23.00' ); + await expect( + page.locator( '.wc-block-components-totals-footer-item' ) + ).toContainText( '$138.00' ); + } ); } ); } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js new file mode 100644 index 00000000000..def87876657 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js @@ -0,0 +1,317 @@ +const { test, expect } = require( '@playwright/test' ); +const { admin } = require( '../../test-data/data' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +const simpleProductName = 'Checkout Coupons Product'; +const singleProductFullPrice = '110.00'; +const singleProductSalePrice = '55.00'; +const coupons = [ + { + code: '5fixedcheckout', + discount_type: 'fixed_cart', + amount: '5.00', + }, + { + code: '50percoffcheckout', + discount_type: 'percent', + amount: '50', + }, + { + code: '10fixedproductcheckout', + discount_type: 'fixed_product', + amount: '10.00', + }, +]; +const couponLimitedCode = '10fixedcheckoutlimited'; +const customerBilling = { + email: 'john.doe.merchant.test@example.com', +}; + +const pageTitle = 'Checkout Block'; +const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); + +let productId, orderId, limitedCouponId; + +test.describe( 'Checkout Block Applying Coupons', () => { + const couponBatchId = new Array(); + + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + // make sure the currency is USD + await api.put( 'settings/general/woocommerce_currency', { + value: 'USD', + } ); + // add a product + await api + .post( 'products', { + name: simpleProductName, + type: 'simple', + regular_price: singleProductFullPrice, + sale_price: singleProductSalePrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + // add coupons + await api + .post( 'coupons/batch', { + create: coupons, + } ) + .then( ( response ) => { + for ( let i = 0; i < response.data.create.length; i++ ) { + couponBatchId.push( response.data.create[ i ].id ); + } + } ); + // add limited coupon + await api + .post( 'coupons', { + code: couponLimitedCode, + discount_type: 'fixed_cart', + amount: '10.00', + usage_limit: 1, + usage_count: 1, + } ) + .then( ( response ) => { + limitedCouponId = response.data.id; + } ); + // add order with applied limited coupon + await api + .post( 'orders', { + status: 'processing', + billing: customerBilling, + coupon_lines: [ + { + code: couponLimitedCode, + }, + ], + } ) + .then( ( response ) => { + orderId = response.data.id; + } ); + } ); + + test.afterAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.post( 'products/batch', { + delete: [ productId ], + } ); + await api.post( 'coupons/batch', { + delete: [ ...couponBatchId, limitedCouponId ], + } ); + await api.post( 'orders/batch', { + delete: [ orderId ], + } ); + } ); + + test.beforeEach( async ( { context } ) => { + // Shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + } ); + + test( 'can create checkout block page', async ( { page } ) => { + // create a new page with checkout block + await page.goto( 'wp-admin/post-new.php?post_type=page' ); + await page.waitForLoadState( 'networkidle' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log In' ).click(); + + // Close welcome popup if prompted + try { + await page + .getByLabel( 'Close', { exact: true } ) + .click( { timeout: 5000 } ); + } catch ( error ) { + console.log( "Welcome modal wasn't present, skipping action." ); + } + + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( pageTitle ); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page + .getByRole( 'document', { + name: 'Empty block; start writing or type forward slash to choose a block', + } ) + .fill( '/checkout' ); + await page.keyboard.press( 'Enter' ); + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await expect( + page.getByText( `${ pageTitle } is now live.` ) + ).toBeVisible(); + } ); + + test( 'allows checkout block to apply coupon of any type', async ( { + page, + } ) => { + const totals = [ '$50.00', '$27.50', '$45.00' ]; + // add product to cart block and go to checkout + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // apply all coupon types + for ( let i = 0; i < coupons.length; i++ ) { + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ i ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .locator( '.wc-block-components-notice-banner__content' ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totals[ i ] ); + await page + .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) + .click(); + await expect( + page + .locator( '.wc-block-components-notice-banner__content' ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been removed from your cart.` + ) + ).toBeVisible(); + } + } ); + + test( 'allows checkout block to apply multiple coupons', async ( { + page, + } ) => { + const totals = [ '$50.00', '$22.50', '$12.50' ]; + const totalsReverse = [ '$17.50', '$45.00', '$55.00' ]; + const discounts = [ '-$5.00', '-$32.50', '-$42.50' ]; + // add product to cart block and go to checkout + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // add all coupons and verify prices + for ( let i = 0; i < coupons.length; i++ ) { + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ i ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .locator( '.wc-block-components-notice-banner__content' ) + .getByText( + `Coupon code "${ coupons[ i ].code }" has been applied to your cart.` + ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-discount > .wc-block-components-totals-item__value' + ) + ).toHaveText( discounts[ i ] ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totals[ i ] ); + } + + for ( let i = 0; i < coupons.length; i++ ) { + await page + .getByLabel( `Remove coupon "${ coupons[ i ].code }"` ) + .click(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toHaveText( totalsReverse[ i ] ); + } + } ); + + test( 'prevents checkout block applying same coupon twice', async ( { + page, + } ) => { + // add product to cart block and go to checkout + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // try to add two same coupons and verify the error message + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ 0 ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .locator( '.wc-block-components-notice-banner__content' ) + .getByText( + `Coupon code "${ coupons[ 0 ].code }" has been applied to your cart.` + ) + ).toBeVisible(); + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( coupons[ 0 ].code ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .getByRole( 'alert' ) + .getByText( + `Coupon code "${ coupons[ 0 ].code }" has already been applied.` + ) + ).toBeVisible(); + } ); + + test( 'prevents checkout block applying coupon with usage limit', async ( { + page, + } ) => { + // add product to cart block and go to checkout + await page.goto( `/shop/?add-to-cart=${ productId }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // add coupon with usage limit + await page.getByRole( 'button', { name: 'Add a coupon' } ).click(); + await page + .locator( '#wc-block-components-totals-coupon__input-0' ) + .fill( couponLimitedCode ); + await page.getByText( 'Apply', { exact: true } ).click(); + await expect( + page + .getByRole( 'alert' ) + .getByText( 'Coupon usage limit has been reached.' ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js new file mode 100644 index 00000000000..ca5003acb1c --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js @@ -0,0 +1,865 @@ +const { test, expect } = require( '@playwright/test' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; +const { admin, customer } = require( '../../test-data/data' ); +const { setFilterValue, clearFilters } = require( '../../utils/filters' ); +const { addProductsToCart } = require( '../../utils/pdp' ); +const { + fillShippingCheckoutBlocks, + fillBillingCheckoutBlocks, +} = require( '../../utils/checkout' ); + +const guestEmail = 'checkout-guest@example.com'; +const newAccountEmail = 'marge-test-account@example.com'; + +const simpleProductName = 'Very Simple Product'; +const simpleProductDesc = 'Lorem ipsum dolor.'; +const singleProductFullPrice = '150.00'; +const singleProductSalePrice = '75.00'; +const twoProductPrice = ( singleProductSalePrice * 2 ).toString(); +const threeProductPrice = ( singleProductSalePrice * 3 ).toString(); + +const pageTitle = 'Checkout Block'; +const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); + +let guestOrderId1, + guestOrderId2, + customerOrderId, + newAccountOrderId, + productId, + shippingZoneId; + +test.describe( 'Checkout Block page', () => { + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + // ensure store address is US + await api.post( 'settings/general/batch', { + update: [ + { + id: 'woocommerce_store_address', + value: 'addr 1', + }, + { + id: 'woocommerce_store_city', + value: 'San Francisco', + }, + { + id: 'woocommerce_default_country', + value: 'US:CA', + }, + { + id: 'woocommerce_store_postcode', + value: '94107', + }, + ], + } ); + // add product + await api + .post( 'products', { + name: simpleProductName, + description: simpleProductDesc, + type: 'simple', + regular_price: singleProductFullPrice, + sale_price: singleProductSalePrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + // enable logging through checkout + await api.put( + 'settings/account/woocommerce_enable_checkout_login_reminder', + { + value: 'yes', + } + ); + // enable creating account through checkout + await api.put( + 'settings/account/woocommerce_enable_signup_and_login_from_checkout', + { + value: 'yes', + } + ); + // add a shipping zone and method + await api + .post( 'shipping/zones', { + name: 'California and Oregon Shipping Zone', + } ) + .then( ( response ) => { + shippingZoneId = response.data.id; + } ); + await api.put( `shipping/zones/${ shippingZoneId }/locations`, [ + { + code: 'US:CA', + type: 'state', + }, + { + code: 'US:OR', + type: 'state', + }, + ] ); + await api.post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'free_shipping', + settings: { + title: 'Free shipping', + }, + } ); + await api.post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'local_pickup', + settings: { + title: 'Local pickup', + }, + } ); + await api.post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'flat_rate', + settings: { + title: 'Flat rate', + cost: singleProductSalePrice, + }, + } ); + // enable bank transfers and COD for payment + await api.put( 'payment_gateways/bacs', { + enabled: true, + } ); + await api.put( 'payment_gateways/cod', { + enabled: true, + } ); + // make sure there's no pre-existing customer that has the same email we're going to use for account creation + const { data: customersList } = await api.get( 'customers', { + email: newAccountEmail, + } ); + + if ( customersList && customersList.length ) { + const customerId = customersList[ 0 ].id; + + console.log( + `Customer with email ${ newAccountEmail } exists! Deleting it before starting test...` + ); + + await api.delete( `customers/${ customerId }`, { force: true } ); + } + } ); + + test.afterAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.delete( `products/${ productId }`, { + force: true, + } ); + await api.delete( `shipping/zones/${ shippingZoneId }`, { + force: true, + } ); + await api.put( 'payment_gateways/bacs', { + enabled: false, + } ); + await api.put( 'payment_gateways/cod', { + enabled: false, + } ); + await api.put( + 'settings/account/woocommerce_enable_checkout_login_reminder', + { + value: 'no', + } + ); + await api.put( + 'settings/account/woocommerce_enable_signup_and_login_from_checkout', + { + value: 'no', + } + ); + // delete the orders we created + if ( guestOrderId1 ) { + await api.delete( `orders/${ guestOrderId1 }`, { force: true } ); + } + if ( guestOrderId2 ) { + await api.delete( `orders/${ guestOrderId2 }`, { force: true } ); + } + if ( customerOrderId ) { + await api.delete( `orders/${ customerOrderId }`, { force: true } ); + } + if ( newAccountOrderId ) { + await api.delete( `orders/${ newAccountOrderId }`, { + force: true, + } ); + } + // clear out the customer we create during the test + await api.get( 'customers' ).then( async ( response ) => { + for ( let i = 0; i < response.data.length; i++ ) { + if ( response.data[ i ].billing.email === newAccountEmail ) { + await api.delete( `customers/${ response.data[ i ].id }`, { + force: true, + } ); + } + } + } ); + } ); + + test.beforeEach( async ( { context } ) => { + // Shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + } ); + + test( 'can see empty checkout block page', async ( { page } ) => { + // create a new page with checkout block + await page.goto( 'wp-admin/post-new.php?post_type=page' ); + await page.waitForLoadState( 'networkidle' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log In' ).click(); + + // Close welcome popup if prompted + try { + await page + .getByLabel( 'Close', { exact: true } ) + .click( { timeout: 5000 } ); + } catch ( error ) { + console.log( "Welcome modal wasn't present, skipping action." ); + } + + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( pageTitle ); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page + .getByRole( 'document', { + name: 'Empty block; start writing or type forward slash to choose a block', + } ) + .fill( '/checkout' ); + await page.keyboard.press( 'Enter' ); + await page + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await expect( + page.getByText( `${ pageTitle } is now live.` ) + ).toBeVisible(); + + // go to the page to test empty cart block + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + await expect( + page.getByText( 'Your cart is currently empty!' ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: 'Browse store' } ) + ).toBeVisible(); + await page.getByRole( 'link', { name: 'Browse store' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Shop' } ) + ).toBeVisible(); + } ); + + test( 'allows customer to choose available payment methods', async ( { + page, + } ) => { + // this time we're going to add two products to the cart + await addProductsToCart( page, simpleProductName, '2' ); + + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // check the order summary + await expect( + page.locator( '.wc-block-components-order-summary-item__quantity' ) + ).toContainText( '2' ); + await expect( + page.locator( + '.wc-block-components-order-summary-item__individual-price' + ) + ).toContainText( `$${ singleProductSalePrice }` ); + await expect( + page.locator( '.wc-block-components-product-metadata__description' ) + ).toContainText( simpleProductDesc ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( twoProductPrice ); + + // check the payment methods + await expect( page.getByLabel( 'Direct bank transfer' ) ).toBeVisible(); + await expect( page.getByLabel( 'Cash on delivery' ) ).toBeVisible(); + await page.getByLabel( 'Cash on delivery' ).check(); + await expect( page.getByLabel( 'Cash on delivery' ) ).toBeChecked(); + } ); + + test( 'allows customer to fill shipping details', async ( { page } ) => { + // this time we're going to add three products to the cart + await addProductsToCart( page, simpleProductName, '3' ); + + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // check the order summary + await expect( + page.locator( '.wc-block-components-order-summary-item__quantity' ) + ).toContainText( '3' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( threeProductPrice ); + + // asserting that you can fill in the shipping details + await expect( page.getByLabel( 'Email address' ) ).toBeEditable(); + await expect( page.getByLabel( 'First name' ) ).toBeEditable(); + await expect( page.getByLabel( 'Last name' ) ).toBeEditable(); + await expect( + page.getByLabel( 'Address', { exact: true } ) + ).toBeEditable(); + await expect( + page.getByLabel( 'Apartment, suite, etc. (optional)' ) + ).toBeEnabled(); + await expect( + page.getByLabel( 'United States (US), Country/Region' ) + ).toBeEditable(); + await expect( page.getByLabel( 'California, State' ) ).toBeEditable(); + await expect( page.getByLabel( 'City' ) ).toBeEditable(); + await expect( page.getByLabel( 'ZIP Code' ) ).toBeEnabled(); + await expect( page.getByLabel( 'Phone (optional)' ) ).toBeEditable(); + } ); + + test( 'allows customer to fill different shipping and billing details', async ( { + page, + } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + await page.getByLabel( 'Email address' ).fill( guestEmail ); + + // fill shipping address + await fillShippingCheckoutBlocks( page ); + + await page.getByLabel( 'Use same address for billing' ).click(); + + // fill billing details + await fillBillingCheckoutBlocks( page ); + + // add note to the order + await page.getByLabel( 'Add a note to your order' ).check(); + await page + .getByPlaceholder( + 'Notes about your order, e.g. special notes for delivery.' + ) + .fill( 'This is to avoid flakiness' ); + + // place an order + await page.getByRole( 'button', { name: 'Place order' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // get order ID from the page + const orderReceivedText = await page + .locator( '.woocommerce-order-overview__order.order' ) + .textContent(); + guestOrderId2 = await orderReceivedText + .split( /(\s+)/ )[ 6 ] + .toString(); + + // go again to the checkout to verify details + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // verify shipping details + await page + .getByLabel( 'Edit address', { exact: true } ) + .first() + .click(); + await expect( + page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'First name' ) + ).toHaveValue( 'Homer' ); + await expect( + page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'Last name' ) + ).toHaveValue( 'Simpson' ); + await expect( + page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'Address', { exact: true } ) + ).toHaveValue( '123 Evergreen Terrace' ); + await expect( + page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'City' ) + ).toHaveValue( 'Springfield' ); + await expect( + page + .getByRole( 'group', { name: 'Shipping address' } ) + .getByLabel( 'ZIP Code' ) + ).toHaveValue( '97403' ); + + // verify billing details + // ISSUE REPORTED #42967, please uncomment below once fixed + // await page.getByLabel( 'Edit address', { exact: true } ).last().click(); + // await expect( + // page + // .getByRole( 'group', { name: 'Billing address' } ) + // .getByLabel( 'First name' ) + // ).toHaveValue( 'Mister' ); + // await expect( + // page + // .getByRole( 'group', { name: 'Billing address' } ) + // .getByLabel( 'Last name' ) + // ).toHaveValue( 'Burns' ); + // await expect( + // page + // .getByRole( 'group', { name: 'Billing address' } ) + // .getByLabel( 'Address', { exact: true } ) + // ).toHaveValue( '156th Street' ); + // await expect( + // page + // .getByRole( 'group', { name: 'Billing address' } ) + // .getByLabel( 'City' ) + // ).toHaveValue( 'Springfield' ); + // await expect( + // page + // .getByRole( 'group', { name: 'Billing address' } ) + // .getByLabel( 'ZIP Code' ) + // ).toHaveValue( '98500' ); + } ); + + test( 'warn when customer is missing required details', async ( { + page, + } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // first try submitting the form with no fields complete + await page.getByRole( 'button', { name: 'Place order' } ).click(); + await expect( + page.getByText( 'Please enter a valid email address' ) + ).toBeVisible(); + await expect( + page.getByText( 'Please enter a valid first name' ) + ).toBeVisible(); + await expect( + page.getByText( 'Please enter a valid last name' ) + ).toBeVisible(); + await expect( + page.getByText( 'Please enter a valid address' ) + ).toBeVisible(); + await expect( + page.getByText( 'Please enter a valid city' ) + ).toBeVisible(); + await expect( + page.getByText( 'Please enter a valid zip code' ) + ).toBeVisible(); + } ); + + test( 'allows customer to fill shipping details and toggle different billing', async ( { + page, + } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + await page.getByLabel( 'Email address' ).fill( customer.email ); + + // fill shipping address and check the toggle to use a different address for billing + await fillShippingCheckoutBlocks( page ); + + await expect( + page.getByLabel( 'Use same address for billing' ) + ).toBeVisible(); + await page.getByLabel( 'Use same address for billing' ).click(); + await expect( + page + .getByRole( 'group', { name: 'Billing address' } ) + .locator( 'h2' ) + ).toBeVisible(); + } ); + + test( 'can choose different shipping types in the checkout', async ( { + page, + } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + await page.getByLabel( 'Email address' ).fill( customer.email ); + + // fill shipping address + await fillShippingCheckoutBlocks( page ); + + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'visible' } ); + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'hidden' } ); + + // check if you see all three shipping options + await expect( page.getByLabel( 'Free shipping' ) ).toBeVisible(); + await expect( page.getByLabel( 'Local pickup' ) ).toBeVisible(); + await expect( page.getByLabel( 'Flat rate' ) ).toBeVisible(); + + // check free shipping option + await page.getByLabel( 'Free shipping' ).check(); + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'visible' } ); + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'hidden' } ); + await expect( page.getByLabel( 'Free shipping' ) ).toBeChecked(); + await expect( + page.locator( '.wc-block-components-totals-shipping__via' ) + ).toHaveText( 'Free shipping' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( singleProductSalePrice ); + + // check local pickup option + await page.getByLabel( 'Local pickup' ).check(); + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'hidden' } ); + await expect( page.getByLabel( 'Local pickup' ) ).toBeChecked(); + await expect( + page.locator( '.wc-block-components-totals-shipping__via' ) + ).toHaveText( 'Local pickup' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( singleProductSalePrice ); + + // check flat rate option + await page.getByLabel( 'Flat rate' ).check(); + await page + .locator( '.wc-block-components-loading-mask' ) + .waitFor( { state: 'hidden' } ); + await expect( page.getByLabel( 'Flat rate' ) ).toBeChecked(); + await expect( + page.locator( '.wc-block-components-totals-shipping__via' ) + ).toHaveText( 'Flat rate' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( twoProductPrice ); + } ); + + test( 'allows guest customer to place an order', async ( { page } ) => { + await addProductsToCart( page, simpleProductName, '2' ); + + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + await page.getByLabel( 'Email address' ).fill( guestEmail ); + + // fill shipping address and check cash on delivery method + await fillShippingCheckoutBlocks( page ); + await page.getByLabel( 'Cash on delivery' ).check(); + await expect( page.getByLabel( 'Cash on delivery' ) ).toBeChecked(); + + // add note to the order + await page.getByLabel( 'Add a note to your order' ).check(); + await page + .getByPlaceholder( + 'Notes about your order, e.g. special notes for delivery.' + ) + .fill( 'This is to avoid flakiness' ); + + // place an order + await page.getByRole( 'button', { name: 'Place order' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // get order ID from the page + const orderReceivedText = await page + .locator( '.woocommerce-order-overview__order.order' ) + .textContent(); + guestOrderId1 = await orderReceivedText + .split( /(\s+)/ )[ 6 ] + .toString(); + + // Let's simulate a new browser context (by dropping all cookies), and reload the page. This approximates a + // scenario where the server can no longer identify the shopper. However, so long as we are within the 10 minute + // grace period following initial order placement, the 'order received' page should still be rendered. + await page.context().clearCookies(); + await page.reload(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // Let's simulate a scenario where the 10 minute grace period has expired. This time, we expect the shopper to + // be presented with a request to verify their email address. + await setFilterValue( + page, + 'woocommerce_order_email_verification_grace_period', + 0 + ); + await page.reload(); + await expect( + page.locator( 'form.woocommerce-verify-email p:nth-child(3)' ) + ).toContainText( /verify the email address associated with the order/ ); + + // Supplying an email address other than the actual order billing email address will take them back to the same + // page with an error message. + await page.fill( '#email', 'incorrect@email.address' ); + await page.locator( 'form.woocommerce-verify-email button' ).click(); + await expect( + page.locator( 'form.woocommerce-verify-email p:nth-child(4)' ) + ).toContainText( /verify the email address associated with the order/ ); + await expect( + page + .getByRole( 'alert' ) + .getByText( + 'We were unable to verify the email address you provided. Please try again.' + ) + ).toBeVisible(); + + // However if they supply the *correct* billing email address, they should see the order received page again. + await page.fill( '#email', guestEmail ); + await page.locator( 'form.woocommerce-verify-email button' ).click(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + await page.goto( 'wp-login.php' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log In' ).click(); + + // load the order placed as a guest + await page.goto( + `wp-admin/post.php?post=${ guestOrderId1 }&action=edit` + ); + + await expect( + page.getByRole( 'heading', { + name: `Order #${ guestOrderId1 } details`, + } ) + ).toBeVisible(); + await expect( page.locator( '.wc-order-item-name' ) ).toContainText( + simpleProductName + ); + await expect( page.locator( 'td.quantity >> nth=0' ) ).toContainText( + '2' + ); + await expect( page.locator( 'td.item_cost >> nth=0' ) ).toContainText( + singleProductSalePrice + ); + await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText( + twoProductPrice + ); + await clearFilters( page ); + } ); + + test( 'allows existing customer to place an order', async ( { page } ) => { + await addProductsToCart( page, simpleProductName, '2' ); + + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // click to log in and make sure you are on the same page after logging in + await page.locator( 'text=Log in.' ).click(); + await page.waitForLoadState( 'networkidle' ); + await page + .locator( 'input[name="username"]' ) + .fill( customer.username ); + await page + .locator( 'input[name="password"]' ) + .fill( customer.password ); + await page.locator( 'text=Log in' ).click(); + await page.waitForLoadState( 'networkidle' ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // if edit address is present click it, otherwise fill shipping details + if ( + await page + .getByLabel( 'Edit address', { exact: true } ) + .first() + .isVisible() + ) { + await page + .getByLabel( 'Edit address', { exact: true } ) + .first() + .click(); + } else { + console.log( + 'No saved shipping address found, filling it instead.' + ); + // fill shipping address + await fillShippingCheckoutBlocks( page ); + } + + // check COD payment method + await page.getByLabel( 'Cash on delivery' ).check(); + await expect( page.getByLabel( 'Cash on delivery' ) ).toBeChecked(); + + // add note to the order + await page.getByLabel( 'Add a note to your order' ).check(); + await page + .getByPlaceholder( + 'Notes about your order, e.g. special notes for delivery.' + ) + .fill( 'This is to avoid flakiness' ); + + // place an order + await page.getByRole( 'button', { name: 'Place order' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // get order ID from the page + const orderReceivedText = await page + .locator( '.woocommerce-order-overview__order.order' ) + .textContent(); + customerOrderId = await orderReceivedText + .split( /(\s+)/ )[ 6 ] + .toString(); + + // Effect a log out/simulate a new browsing session by dropping all cookies. + await page.context().clearCookies(); + await page.reload(); + + // Now we are logged out, return to the confirmation page: we should be asked to log back in. + await expect( + page + .getByRole( 'alert' ) + .getByText( 'Please log in to your account to view this order' ) + ).toBeVisible(); + + // Switch to admin user. + await page.goto( 'wp-login.php?loggedout=true' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log In' ).click(); + + // load the order placed as a customer + await page.goto( + `wp-admin/post.php?post=${ customerOrderId }&action=edit` + ); + await expect( + page.locator( 'h2.woocommerce-order-data__heading' ) + ).toContainText( `Order #${ customerOrderId } details` ); + await expect( page.locator( '.wc-order-item-name' ) ).toContainText( + simpleProductName + ); + await expect( page.locator( 'td.quantity >> nth=0' ) ).toContainText( + '2' + ); + await expect( page.locator( 'td.item_cost >> nth=0' ) ).toContainText( + singleProductSalePrice + ); + await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText( + twoProductPrice + ); + } ); + + test( 'can create an account during checkout', async ( { page } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + + // check create account during checkout + await expect( page.getByLabel( 'Create an account?' ) ).toBeVisible(); + await page.getByLabel( 'Create an account?' ).check(); + await expect( page.getByLabel( 'Create an account?' ) ).toBeChecked(); + + await page.getByLabel( 'Email address' ).fill( newAccountEmail ); + + // fill shipping address and check cash on delivery method + await fillShippingCheckoutBlocks( page, 'Marge' ); + await page.getByLabel( 'Cash on delivery' ).check(); + await expect( page.getByLabel( 'Cash on delivery' ) ).toBeChecked(); + + // add note to the order + await page.getByLabel( 'Add a note to your order' ).check(); + await page + .getByPlaceholder( + 'Notes about your order, e.g. special notes for delivery.' + ) + .fill( 'This is to avoid flakiness' ); + + // place an order + await page.getByRole( 'button', { name: 'Place order' } ).click(); + await expect( + page.getByRole( 'heading', { name: 'Order received' } ) + ).toBeVisible(); + + // get order ID from the page + const orderReceivedText = await page + .locator( '.woocommerce-order-overview__order.order' ) + .textContent(); + newAccountOrderId = orderReceivedText.split( /(\s+)/ )[ 6 ].toString(); + + // confirms that an account was created + await page.goto( '/my-account/' ); + await expect( + page.getByRole( 'heading', { name: 'My account' } ) + ).toBeVisible(); + await page + .getByRole( 'navigation' ) + .getByRole( 'link', { name: 'Log out' } ) + .click(); + + // sign in as admin to confirm account creation + await page.goto( 'wp-admin/users.php' ); + await page.waitForLoadState( 'networkidle' ); + await page.locator( 'input[name="log"]' ).fill( admin.username ); + await page.locator( 'input[name="pwd"]' ).fill( admin.password ); + await page.locator( 'text=Log in' ).click(); + await expect( page.locator( 'tbody#the-list' ) ).toContainText( + newAccountEmail + ); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js index 4110201ae9d..160117b24b7 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js @@ -2,6 +2,7 @@ const { test, expect } = require( '@playwright/test' ); const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; const { admin, customer } = require( '../../test-data/data' ); const { setFilterValue, clearFilters } = require( '../../utils/filters' ); +const { addProductsToCart } = require( '../../utils/pdp' ); const guestEmail = 'checkout-guest@example.com'; @@ -143,10 +144,7 @@ test.describe( 'Checkout page', () => { page, } ) => { // this time we're going to add two products to the cart - for ( let i = 1; i < 3; i++ ) { - await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); - } + await addProductsToCart( page, simpleProductName, '2' ); await page.goto( '/checkout/' ); await expect( page.locator( 'strong.product-quantity' ) ).toContainText( @@ -169,10 +167,7 @@ test.describe( 'Checkout page', () => { test( 'allows customer to fill billing details', async ( { page } ) => { // this time we're going to add three products to the cart - for ( let i = 1; i < 4; i++ ) { - await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); - } + await addProductsToCart( page, simpleProductName, '3' ); await page.goto( '/checkout/' ); await expect( page.locator( 'strong.product-quantity' ) ).toContainText( @@ -272,10 +267,7 @@ test.describe( 'Checkout page', () => { } ); test( 'allows customer to fill shipping details', async ( { page } ) => { - for ( let i = 1; i < 3; i++ ) { - await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); - } + await addProductsToCart( page, simpleProductName, '2' ); await page.goto( '/checkout/' ); await expect( page.locator( 'strong.product-quantity' ) ).toContainText( @@ -306,10 +298,7 @@ test.describe( 'Checkout page', () => { } ); test( 'allows guest customer to place an order', async ( { page } ) => { - for ( let i = 1; i < 3; i++ ) { - await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); - } + await addProductsToCart( page, simpleProductName, '2' ); await page.goto( '/checkout/' ); await expect( page.locator( 'strong.product-quantity' ) ).toContainText( @@ -430,10 +419,7 @@ test.describe( 'Checkout page', () => { .fill( customer.password ); await page.locator( 'text=Log In' ).click(); await page.waitForLoadState( 'networkidle' ); - for ( let i = 1; i < 3; i++ ) { - await page.goto( `/shop/?add-to-cart=${ productId }` ); - await page.waitForLoadState( 'networkidle' ); - } + await addProductsToCart( page, simpleProductName, '2' ); await page.goto( '/checkout/' ); await expect( page.locator( 'strong.product-quantity' ) ).toContainText( diff --git a/plugins/woocommerce/tests/e2e-pw/utils/checkout.js b/plugins/woocommerce/tests/e2e-pw/utils/checkout.js new file mode 100644 index 00000000000..0d1c43a699d --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/checkout.js @@ -0,0 +1,79 @@ +/** + * Util helper made for filling shipping details in the blockbased checkout + * + * @param page + * @param firstName + * @param lastName + * @param address + * @param city + * @param 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 blockbased checkout + * + * @param page + * @param firstName + * @param lastName + * @param address + * @param city + * @param 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/plugins/woocommerce/tests/e2e-pw/utils/pdp.js b/plugins/woocommerce/tests/e2e-pw/utils/pdp.js new file mode 100644 index 00000000000..f7271085413 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/pdp.js @@ -0,0 +1,26 @@ +const { expect } = require( '@playwright/test' ); + +/** + * Util helper made for adding multiple same products to cart + * + * @param page + * @param productName + * @param quantityCount + */ +export async function addProductsToCart( page, productName, quantityCount ) { + await page.goto( + `product/${ productName.replace( / /gi, '-' ).toLowerCase() }` + ); + await expect( page.locator( '.product_title' ) ).toContainText( + productName + ); + await page.getByLabel( 'Product quantity' ).fill( quantityCount ); + await page.getByRole( 'button', { name: 'Add to cart' } ).click(); + await expect( + page + .getByRole( 'alert' ) + .getByText( + `${ quantityCount } × “${ productName }” have been added to your cart.` + ) + ).toBeVisible(); +}