diff --git a/plugins/woocommerce/changelog/e2e-add-tests-shopper-cart-block b/plugins/woocommerce/changelog/e2e-add-tests-shopper-cart-block new file mode 100644 index 00000000000..80991134e29 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-add-tests-shopper-cart-block @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Adds tests for shopper cart block flows diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js new file mode 100644 index 00000000000..82579ff4f9b --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js @@ -0,0 +1,328 @@ +const { test, expect } = require( '@playwright/test' ); +const { admin } = require( '../../test-data/data' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +const firstProductName = 'First Product'; +const firstProductPrice = '10.00'; +const secondProductName = 'Second Product'; +const secondProductPrice = '20.00'; +const firstProductWithFlatRate = +firstProductPrice + 5; +const doubleFirstProductWithFlatRate = +firstProductPrice * 2 + 5; +const twoProductsTotal = +firstProductPrice + +secondProductPrice; +const twoProductsWithFlatRate = twoProductsTotal + 5; + +const pageTitle = 'Cart Block'; +const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); + +const shippingZoneNameES = 'Netherlands Free Shipping'; +const shippingCountryNL = 'NL'; +const shippingZoneNamePT = 'Portugal Flat Local'; +const shippingCountryPT = 'PT'; + +test.describe( 'Cart Block Calculate Shipping', () => { + let product1Id, product2Id, shippingZoneNLId, shippingZonePTId; + + 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 products + await api + .post( 'products', { + name: firstProductName, + type: 'simple', + regular_price: firstProductPrice, + } ) + .then( ( response ) => { + product1Id = response.data.id; + } ); + await api + .post( 'products', { + name: secondProductName, + type: 'simple', + regular_price: secondProductPrice, + } ) + .then( ( response ) => { + product2Id = response.data.id; + } ); + + // create shipping zones + await api + .post( 'shipping/zones', { + name: shippingZoneNameES, + } ) + .then( ( response ) => { + shippingZoneNLId = response.data.id; + } ); + await api + .post( 'shipping/zones', { + name: shippingZoneNamePT, + } ) + .then( ( response ) => { + shippingZonePTId = response.data.id; + } ); + + // set shipping zone locations + await api.put( `shipping/zones/${ shippingZoneNLId }/locations`, [ + { + code: shippingCountryNL, + }, + ] ); + await api.put( `shipping/zones/${ shippingZonePTId }/locations`, [ + { + code: shippingCountryPT, + }, + ] ); + + // set shipping zone methods + await api.post( `shipping/zones/${ shippingZoneNLId }/methods`, { + method_id: 'free_shipping', + } ); + await api.post( `shipping/zones/${ shippingZonePTId }/methods`, { + method_id: 'flat_rate', + settings: { + cost: '5.00', + }, + } ); + await api.post( `shipping/zones/${ shippingZonePTId }/methods`, { + method_id: 'local_pickup', + } ); + + // confirm that we allow shipping to any country + await api.put( 'settings/general/woocommerce_allowed_countries', { + value: 'all', + } ); + } ); + + 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: [ product1Id, product2Id ], + } ); + await api.delete( `shipping/zones/${ shippingZoneNLId }`, { + force: true, + } ); + await api.delete( `shipping/zones/${ shippingZonePTId }`, { + force: true, + } ); + } ); + + test.beforeEach( async ( { page, context } ) => { + // Shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + + // all tests use the first product + await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + await page.waitForLoadState( 'networkidle' ); + } ); + + test( 'create Cart Block page', async ( { page } ) => { + // create a new page with cart 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( '/cart' ); + 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 customer to calculate Free Shipping in cart block if in Netherlands', async ( { + page, + } ) => { + await page.goto( pageSlug ); + + // Set shipping country to Netherlands + await page + .locator( + '.wc-block-components-totals-shipping__change-address__link' + ) + .click(); + await page.getByRole( 'combobox' ).first().fill( 'Netherlands' ); + await page.getByLabel( 'Postal code' ).fill( '1011AA' ); + await page.getByLabel( 'City' ).fill( 'Amsterdam' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); + + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Free shipping' ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-radio-control__description > .wc-block-components-formatted-money-amount' + ) + ).toContainText( '$0.00' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( firstProductPrice ); + } ); + + test( 'allows customer to calculate Flat rate and Local pickup in cart block if in Portugal', async ( { + page, + } ) => { + await page.goto( pageSlug ); + + // Set shipping country to Portugal + await page + .locator( + '.wc-block-components-totals-shipping__change-address__link' + ) + .click(); + await page.getByRole( 'combobox' ).first().fill( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); + + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Flat rate' ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-shipping > .wc-block-components-totals-item' + ) + ).toContainText( '$5.00' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( firstProductWithFlatRate.toString() ); + + // Set shipping to local pickup instead of flat rate + await page.getByRole( 'group' ).getByText( 'Local pickup' ).click(); + + // Verify updated shipping costs + await expect( + page.locator( + '.wc-block-components-totals-shipping > .wc-block-components-totals-item' + ) + ).toContainText( '$0.00' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( firstProductPrice ); + } ); + + test( 'should show correct total cart block price after updating quantity', async ( { + page, + } ) => { + await page.goto( pageSlug ); + + // Set shipping country to Portugal + await page + .locator( + '.wc-block-components-totals-shipping__change-address__link' + ) + .click(); + await page.getByRole( 'combobox' ).first().fill( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); + + // Increase product quantity and verify the updated price + await page + .getByRole( 'button' ) + .filter( { hasText: '+', exact: true } ) + .click(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( doubleFirstProductWithFlatRate.toString() ); + } ); + + test( 'should show correct total cart block price with 2 different products and flat rate/local pickup', async ( { + page, + } ) => { + await page.goto( `/shop/?add-to-cart=${ product2Id }` ); + await page.waitForLoadState( 'networkidle' ); + + await page.goto( pageSlug ); + + // Set shipping country to Portugal + await page + .locator( + '.wc-block-components-totals-shipping__change-address__link' + ) + .click(); + await page.getByRole( 'combobox' ).first().fill( 'Portugal' ); + await page.getByLabel( 'Postal code' ).fill( '1000-001' ); + await page.getByLabel( 'City' ).fill( 'Lisbon' ); + await page.getByRole( 'button', { name: 'Update' } ).click(); + + // Verify shipping costs + await expect( + page.getByRole( 'group' ).getByText( 'Flat rate' ) + ).toBeVisible(); + await expect( + page.locator( + '.wc-block-components-totals-shipping > .wc-block-components-totals-item' + ) + ).toContainText( '$5.00' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( twoProductsWithFlatRate.toString() ); + + // Set shipping to local pickup instead of flat rate + await page.getByRole( 'group' ).getByText( 'Local pickup' ).click(); + + // Verify updated shipping costs + await expect( + page.locator( + '.wc-block-components-totals-shipping > .wc-block-components-totals-item' + ) + ).toContainText( '$0.00' ); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( twoProductsTotal.toString() ); + } ); +} ); 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-block-calculate-tax.spec.js new file mode 100644 index 00000000000..e0e8bcc3d9e --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-tax.spec.js @@ -0,0 +1,768 @@ +const { test, expect } = require( '@playwright/test' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; +const { admin } = require( '../../test-data/data' ); + +const productName = 'First Product Cart Block Taxing'; +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(); + +let productId, + productId2, + nastyTaxId, + seventeenTaxId, + sixTaxId, + countryTaxId, + stateTaxId, + cityTaxId, + zipTaxId, + shippingTaxId, + shippingZoneId, + shippingMethodId; + +test.describe( 'Shopper Cart Block Tax Display', () => { + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'yes', + } ); + await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', { + value: 'no', + } ); + await api.put( 'settings/tax/woocommerce_tax_total_display', { + value: 'itemized', + } ); + await api + .post( 'products', { + name: productName, + type: 'simple', + regular_price: productPrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + await api + .post( 'taxes', { + country: 'US', + state: '*', + cities: '*', + postcodes: '*', + rate: '25', + name: 'Nasty Tax', + shipping: false, + } ) + .then( ( response ) => { + nastyTaxId = response.data.id; + } ); + } ); + + test.beforeEach( async ( { page, context } ) => { + // shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + + // all tests use the first product + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + } ); + + 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.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'incl', + } ); + await api.put( 'settings/tax/woocommerce_price_display_suffix', { + value: '', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'no', + } ); + await api.delete( `products/${ productId }`, { + force: true, + } ); + await api.delete( `taxes/${ nastyTaxId }`, { + force: true, + } ); + } ); + + test( 'can create Cart Block page', async ( { page } ) => { + // create a new page with cart 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( '/cart' ); + 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( 'that inclusive tax is displayed properly in Cart Block page', async ( { + page, + } ) => { + 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( '$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 ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + 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' ); + } ); +} ); + +test.describe( 'Shopper Cart Block Tax Rounding', () => { + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'yes', + } ); + await api + .post( 'products', { + name: productName, + type: 'simple', + regular_price: messyProductPrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + await api + .post( 'products', { + name: secondProductName, + type: 'simple', + regular_price: messyProductPrice, + } ) + .then( ( response ) => { + productId2 = response.data.id; + } ); + await api + .post( 'taxes', { + country: 'US', + state: '*', + cities: '*', + postcodes: '*', + rate: '17', + name: 'Seventeen Tax', + shipping: false, + compound: true, + priority: 1, + } ) + .then( ( response ) => { + seventeenTaxId = response.data.id; + } ); + await api + .post( 'taxes', { + country: 'US', + state: '*', + cities: '*', + postcodes: '*', + rate: '6', + name: 'Six Tax', + shipping: false, + compound: true, + priority: 2, + } ) + .then( ( response ) => { + sixTaxId = response.data.id; + } ); + } ); + + test.beforeEach( async ( { page, context } ) => { + // shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + + // all tests use the same products + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + await page.goto( `/shop/?add-to-cart=${ productId2 }`, { + waitUntil: 'networkidle', + } ); + await page.goto( `/shop/?add-to-cart=${ productId2 }`, { + waitUntil: 'networkidle', + } ); + } ); + + 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.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'incl', + } ); + await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', { + value: 'no', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'no', + } ); + await api.put( 'settings/tax/woocommerce_tax_total_display', { + value: 'single', + } ); + await api.delete( `products/${ productId }`, { + force: true, + } ); + await api.delete( `products/${ productId2 }`, { + force: true, + } ); + await api.delete( `taxes/${ seventeenTaxId }`, { + force: true, + } ); + await api.delete( `taxes/${ sixTaxId }`, { + force: true, + } ); + } ); + + test( 'that tax rounding is present at subtotal level', async ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'excl', + } ); + await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', { + value: 'yes', + } ); + 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' ); + } ); + + test( 'that tax rounding is off at subtotal level', async ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'excl', + } ); + await api.put( 'settings/tax/woocommerce_tax_round_at_subtotal', { + value: 'no', + } ); + 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' ); + } ); +} ); + +test.describe( 'Shopper Cart Block Tax Levels', () => { + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'yes', + } ); + await api.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'excl', + } ); + + // add product + await api + .post( 'products', { + name: productName, + type: 'simple', + regular_price: productPrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + + // create shipping zone + await api + .post( 'shipping/zones', { + name: 'All', + } ) + .then( ( response ) => { + shippingZoneId = response.data.id; + } ); + + // set shipping zone location + await api.put( `shipping/zones/${ shippingZoneId }/locations`, [ + { + code: 'US', + }, + ] ); + + // set shipping zone method + await api + .post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'free_shipping', + } ) + .then( ( response ) => { + shippingMethodId = response.data.id; + } ); + + // confirm that we allow shipping to any country + await api.put( 'settings/general/woocommerce_allowed_countries', { + value: 'all', + } ); + await api + .post( 'taxes', { + country: 'US', + state: '*', + cities: '*', + postcodes: '*', + rate: '10', + name: 'Country Tax', + shipping: false, + priority: 1, + } ) + .then( ( response ) => { + countryTaxId = response.data.id; + } ); + await api + .post( 'taxes', { + country: '*', + state: 'CA', + cities: '*', + postcodes: '*', + rate: '5', + name: 'State Tax', + shipping: false, + priority: 2, + } ) + .then( ( response ) => { + stateTaxId = response.data.id; + } ); + await api + .post( 'taxes', { + country: '*', + state: '*', + cities: 'Sacramento', + postcodes: '*', + rate: '2.5', + name: 'City Tax', + shipping: false, + priority: 3, + } ) + .then( ( response ) => { + cityTaxId = response.data.id; + } ); + await api + .post( 'taxes', { + country: '*', + state: '*', + cities: '*', + postcodes: '55555', + rate: '1.25', + name: 'Zip Tax', + shipping: false, + priority: 4, + } ) + .then( ( response ) => { + zipTaxId = response.data.id; + } ); + } ); + + test.beforeEach( async ( { page, context } ) => { + // shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + + // all tests use the first product + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + } ); + + 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.put( 'settings/tax/woocommerce_tax_total_display', { + value: 'single', + } ); + await api.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'incl', + } ); + await api.delete( `products/${ productId }`, { + force: true, + } ); + await api.delete( `shipping/zones/${ shippingZoneId }`, { + force: true, + } ); + await api.delete( `taxes/${ countryTaxId }`, { + force: true, + } ); + await api.delete( `taxes/${ stateTaxId }`, { + force: true, + } ); + await api.delete( `taxes/${ cityTaxId }`, { + force: true, + } ); + await api.delete( `taxes/${ zipTaxId }`, { + force: true, + } ); + } ); + + test( 'that applying taxes in cart block of 4 different levels calculates properly', async ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/tax/woocommerce_tax_total_display', { + value: 'itemized', + } ); + // workaround to fill shipping details since there is an issue with showing + // shipping calculator on the cart block-based experience for logged out user + await page.goto( '/cart/' ); // we will use the old cart for this purpose + await page.locator( '.shipping-calculator-button' ).click(); + await page.getByLabel( 'Town / City' ).fill( 'Sacramento' ); + await page.getByLabel( 'ZIP Code' ).fill( '55555' ); + await page + .getByRole( 'button', { name: 'Update', exact: true } ) + .click(); + await expect( page.locator( '.woocommerce-info' ) ).toContainText( + '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' ); + } ); + + test( 'that applying taxes in Cart Block of 2 different levels (2 excluded) calculates properly', async ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + 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' ); + } ); +} ); + +test.describe( 'Shipping Cart Block Tax', () => { + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'yes', + } ); + await api + .post( 'products', { + name: productName, + type: 'simple', + regular_price: productPrice, + } ) + .then( ( response ) => { + productId = response.data.id; + } ); + await api + .post( 'taxes', { + country: 'US', + state: '*', + cities: '*', + postcodes: '*', + rate: '15', + name: 'Shipping Tax', + shipping: true, + } ) + .then( ( response ) => { + shippingTaxId = response.data.id; + } ); + await api + .post( 'shipping/zones', { + name: 'All', + } ) + .then( ( response ) => { + shippingZoneId = response.data.id; + } ); + await api + .post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'flat_rate', + } ) + .then( ( response ) => { + shippingMethodId = response.data.id; + } ); + await api.put( + `shipping/zones/${ shippingZoneId }/methods/${ shippingMethodId }`, + { + settings: { + cost: '20.00', + }, + } + ); + await api.put( 'payment_gateways/cod', { + enabled: true, + } ); + await api.put( 'settings/tax/woocommerce_tax_display_cart', { + value: 'incl', + } ); + } ); + + test.beforeEach( async ( { page, context } ) => { + // shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + + // all tests use the first product + await page.goto( `/shop/?add-to-cart=${ productId }`, { + waitUntil: 'networkidle', + } ); + } ); + + 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.put( 'settings/general/woocommerce_calc_taxes', { + value: 'no', + } ); + await api.delete( `products/${ productId }`, { + force: true, + } ); + await api.delete( `taxes/${ shippingTaxId }`, { + force: true, + } ); + await api.put( 'payment_gateways/cod', { + enabled: false, + } ); + await api.delete( `shipping/zones/${ shippingZoneId }`, { + force: true, + } ); + } ); + + test( 'that tax is applied in Cart Block to shipping as well as order', async ( { + page, + baseURL, + } ) => { + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + 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' ); + } ); +} ); 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 new file mode 100644 index 00000000000..c6602d98c7f --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js @@ -0,0 +1,256 @@ +const { test, expect } = require( '@playwright/test' ); +const { admin } = require( '../../test-data/data' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +const simpleProductName = 'A Simple Product'; +const singleProductFullPrice = '110.00'; +const singleProductSalePrice = '55.00'; +const coupons = [ + { + code: '5fixedcart', + discount_type: 'fixed_cart', + amount: '5.00', + }, + { + code: '50percoff', + discount_type: 'percent', + amount: '50', + }, + { + code: '10fixedproduct', + discount_type: 'fixed_product', + amount: '10.00', + }, +]; + +const pageTitle = 'Cart Block'; +const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); + +let product1Id; + +test.describe( 'Cart 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 ) => { + product1Id = 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 ); + } + } ); + } ); + + 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: [ product1Id ], + } ); + await api.post( 'coupons/batch', { delete: [ ...couponBatchId ] } ); + } ); + + test.beforeEach( async ( { context } ) => { + // Shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + } ); + + test( 'can create Cart Block page', async ( { page } ) => { + // create a new page with cart 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( '/cart' ); + 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 cart block to apply coupon of any type', async ( { + page, + } ) => { + const totals = [ '$50.00', '$27.50', '$45.00' ]; + // add product to cart block + await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + 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 cart 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 + await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + 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 cart block applying same coupon twice', async ( { + page, + } ) => { + // add product to cart block + await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + 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(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js new file mode 100644 index 00000000000..6cc5e769707 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block.spec.js @@ -0,0 +1,236 @@ +const { test, expect } = require( '@playwright/test' ); +const { admin } = require( '../../test-data/data' ); +const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +const simpleProductName = 'Single Simple Product'; +const simpleProductDesc = 'Lorem ipsum dolor sit amet.'; +const singleProductFullPrice = '110.00'; +const singleProductSalePrice = '55.00'; +const firstCrossSellProductPrice = '25.00'; +const secondCrossSellProductPrice = '15.00'; +const doubleProductsPrice = +singleProductSalePrice * 2; +const singleProductWithCrossSellProducts = + +singleProductFullPrice + + +firstCrossSellProductPrice + + +secondCrossSellProductPrice; + +const pageTitle = 'Cart Block'; +const pageSlug = pageTitle.replace( / /gi, '-' ).toLowerCase(); + +let product1Id, product2Id, product3Id; + +test.describe( 'Cart 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', + } ); + // make sure the currency is USD + await api.put( 'settings/general/woocommerce_currency', { + value: 'USD', + } ); + // add 3 products + await api + .post( 'products', { + name: `${ simpleProductName } Cross-Sell 1`, + type: 'simple', + regular_price: firstCrossSellProductPrice, + } ) + .then( ( response ) => { + product2Id = response.data.id; + } ); + await api + .post( 'products', { + name: `${ simpleProductName } Cross-Sell 2`, + type: 'simple', + regular_price: secondCrossSellProductPrice, + } ) + .then( ( response ) => { + product3Id = response.data.id; + } ); + await api + .post( 'products', { + name: simpleProductName, + description: simpleProductDesc, + type: 'simple', + regular_price: singleProductFullPrice, + sale_price: singleProductSalePrice, + cross_sell_ids: [ product2Id, product3Id ], + manage_stock: true, + stock_quantity: 2, + } ) + .then( ( response ) => { + product1Id = 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: [ product1Id, product2Id, product3Id ], + } ); + } ); + + test.beforeEach( async ( { context } ) => { + // Shopping cart is very sensitive to cookies, so be explicit + await context.clearCookies(); + } ); + + test( 'can see empty cart block', async ( { page } ) => { + // create a new page with cart 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( '/cart' ); + 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( 'can add product to cart block, increase quantity, manage cross-sell products and proceed to checkout', async ( { + page, + } ) => { + // add product to cart block + await page.goto( `/shop/?add-to-cart=${ product1Id }` ); + await page.waitForLoadState( 'networkidle' ); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: simpleProductName, exact: true } ) + ).toBeVisible(); + await expect( page.getByText( simpleProductDesc ) ).toBeVisible(); + await expect( + page.getByText( `Save $${ singleProductSalePrice }` ) + ).toBeVisible(); + + // increase product quantity to its maximum + await expect( page.getByText( '2 left in stock' ) ).toBeVisible(); + await page + .getByRole( 'button' ) + .filter( { hasText: '+', exact: true } ) + .click(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( `$${ doubleProductsPrice.toString() }` ); + await expect( + page.getByRole( 'button' ).filter( { hasText: '+', exact: true } ) + ).toBeDisabled(); + + // add cross-sell products to cart + await expect( + page.getByRole( 'heading', { name: 'You may be interested in…' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { + name: `${ simpleProductName } Cross-Sell 1`, + exact: true, + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { + name: `${ simpleProductName } Cross-Sell 2`, + exact: true, + } ) + ).toBeVisible(); + await page + .getByRole( 'link', { + name: `${ simpleProductName } Cross-Sell 1`, + exact: true, + } ) + .click(); + await page.getByRole( 'button', { name: 'Add to cart' } ).click(); + await page.goto( pageSlug ); + await expect( + page.getByRole( 'heading', { name: pageTitle } ) + ).toBeVisible(); + await page.locator( '.add_to_cart_button' ).click(); + await expect( + page.getByRole( 'heading', { name: 'You may be interested in…' } ) + ).toBeHidden(); + await expect( + page.locator( + '.wc-block-components-totals-footer-item > .wc-block-components-totals-item__value' + ) + ).toContainText( + `$${ singleProductWithCrossSellProducts.toString() }` + ); + + // remove cross-sell products from cart + await page.locator( ':nth-match(:text("Remove item"), 3)' ).click(); + await page.locator( ':nth-match(:text("Remove item"), 2)' ).click(); + await expect( + page.getByRole( 'heading', { name: 'You may be interested in…' } ) + ).toBeVisible(); + + // check if the link to proceed to the checkout exists + await expect( + page.getByRole( 'link', { + name: 'Proceed to Checkout', + } ) + ).toBeVisible(); + + // remove product from cart + await page.locator( ':text("Remove item")' ).click(); + await expect( + page.getByText( 'Your cart is currently empty!' ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: 'Browse store' } ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/calculate-shipping.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js similarity index 100% rename from plugins/woocommerce/tests/e2e-pw/tests/shopper/calculate-shipping.spec.js rename to plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-calculate-shipping.spec.js