Commit 7f556612eb4 for woocommerce
commit 7f556612eb43753f96ba19bde0d70151f752f132
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Thu Sep 24 18:10:13 2026 +0300
Revert "[tests] Reduce Cart and Checkout shopper E2E from 21 titles to 8" (#69046)
* revert: Revert "[tests] Reduce Cart and Checkout shopper E2E from 21 titles to 8" (#68581)
This reverts commit 5c973679cbf2daba90b323dd102114fa669cb0a9.
Rubik asked for the E2E migration's test changes in its areas to be
reverted until the team can review them. #68581 is one of the 11 PRs on
the revert list Rubik agreed on 2026-09-24.
The revert brings back the browser tests the PR removed or cut down and
removes the lower-layer tests it added in their place. Only test code
and changelog entries change; nothing ships.
Refs TESTOPS-234
Refs #68581
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cart): Expect the current empty cart heading after removing a product
The #68581 revert restores "User can remove a product from cart", which
waits for the heading "Your cart is currently empty!". #68560 has since
changed the Cart block's empty state heading to "Your cart is empty"
and updated the retained titles, so the restored one times out.
Match the heading #68560 introduced.
Refs #68581
Refs #68560
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/testops-234-cart-checkout-shopper-journeys b/plugins/woocommerce/changelog/testops-234-cart-checkout-shopper-journeys
deleted file mode 100644
index cc1b0125a68..00000000000
--- a/plugins/woocommerce/changelog/testops-234-cart-checkout-shopper-journeys
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Consolidate the Cart block shopper E2E journeys and move the discount-label, single-use coupon, and shipping-topology checks to Jest and PHPUnit; replace the wall-clock nonce expiry with one deterministic invalid nonce.
-
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/test/cart-line-item-row.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/test/cart-line-item-row.tsx
index 36dccf546d2..ae787afc243 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/test/cart-line-item-row.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/test/cart-line-item-row.tsx
@@ -60,14 +60,7 @@ const mockGetSettingWithCoercion = getSettingWithCoercion as jest.Mock;
* preview module's transitive imports (which expect `wcBlocksConfig` to be
* populated at module-load time).
*/
-type RawPriceOverrides = Partial< {
- precision: number;
- price: string;
- regular_price: string;
- sale_price: string;
-} >;
-
-const buildLineItem = ( rawPriceOverrides: RawPriceOverrides = {} ): CartItem =>
+const buildLineItem = (): CartItem =>
( {
key: 'test-key',
id: 1,
@@ -109,7 +102,6 @@ const buildLineItem = ( rawPriceOverrides: RawPriceOverrides = {} ): CartItem =>
price: '10000000',
regular_price: '10000000',
sale_price: '10000000',
- ...rawPriceOverrides,
},
},
totals: {
@@ -171,82 +163,17 @@ const setSettings = ( {
);
};
-const renderRow = ( rawPriceOverrides?: RawPriceOverrides ) => {
+const renderRow = () => {
// Wrap in a table so the row's <td>s render in a valid context.
return render(
<table>
<tbody>
- <CartLineItemRow
- lineItem={ buildLineItem( rawPriceOverrides ) }
- />
+ <CartLineItemRow lineItem={ buildLineItem() } />
</tbody>
</table>
);
};
-describe( 'renders cart row price', () => {
- it.each( [
- {
- name: 'sale price with its regular price',
- rawPrices: {
- precision: 6,
- price: '8000000',
- regular_price: '10000000',
- },
- currentPrice: '$8.00',
- regularPrice: '$10.00',
- },
- {
- name: 'ordinary price without sale markup',
- rawPrices: {
- price: '10000000',
- regular_price: '10000000',
- },
- currentPrice: '$10.00',
- regularPrice: undefined,
- },
- {
- name: 'extension-filtered price above its regular price without sale markup',
- rawPrices: {
- price: '50000000',
- regular_price: '10000000',
- },
- currentPrice: '$50.00',
- regularPrice: undefined,
- },
- ] )( '$name', ( { rawPrices, currentPrice, regularPrice } ) => {
- const { container } = renderRow( rawPrices );
- const price = container.querySelector( '.wc-block-cart-item__prices' );
- const currentPriceElement = price?.querySelector(
- '.wc-block-components-product-price__value'
- );
- const regularPriceElement = price?.querySelector(
- 'del.wc-block-components-product-price__regular'
- );
- const discountedPriceElement =
- price?.querySelector( 'ins.is-discounted' );
-
- expect( price ).toBeInTheDocument();
- expect(
- price?.querySelectorAll(
- '.wc-block-components-product-price__value'
- )
- ).toHaveLength( 1 );
- expect( currentPriceElement?.textContent ).toBe( currentPrice );
-
- expect( Boolean( regularPriceElement ) ).toBe(
- Boolean( regularPrice )
- );
- expect( Boolean( discountedPriceElement ) ).toBe(
- Boolean( regularPrice )
- );
- expect( regularPriceElement?.textContent ).toBe( regularPrice );
- expect( discountedPriceElement?.textContent ).toBe(
- regularPrice ? currentPrice : undefined
- );
- } );
-} );
-
describe( 'CartLineItemRow — Save for later visibility', () => {
beforeEach( () => {
jest.clearAllMocks();
diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-shopper-journeys b/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-shopper-journeys
deleted file mode 100644
index cc1b0125a68..00000000000
--- a/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-shopper-journeys
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Consolidate the Cart block shopper E2E journeys and move the discount-label, single-use coupon, and shipping-topology checks to Jest and PHPUnit; replace the wall-clock nonce expiry with one deterministic invalid nonce.
-
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/short-nonce-life.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/short-nonce-life.php
new file mode 100644
index 00000000000..0af4a340bcb
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/short-nonce-life.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Plugin Name: WooCommerce Blocks Test Short Nonce Life
+ * Description: Sets a very short nonce lifetime for testing nonce expiry scenarios.
+ * Plugin URI: https://github.com/woocommerce/woocommerce
+ * Author: WooCommerce
+ *
+ * @package woocommerce-blocks-test-short-nonce-life
+ */
+
+declare( strict_types=1 );
+
+/**
+ * Set nonce lifetime to 2 seconds to simulate cache expiry scenarios.
+ */
+add_filter(
+ 'nonce_life',
+ function () {
+ return 2;
+ }
+);
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-block.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-block.shopper.block_theme.spec.ts
index 66142eb6e9b..5cc858cf7df 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-block.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-block.shopper.block_theme.spec.ts
@@ -8,6 +8,8 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
*/
import { CartPage } from './cart.page';
import {
+ DISCOUNTED_PRODUCT_NAME,
+ REGULAR_PRICED_PRODUCT_NAME,
SIMPLE_PHYSICAL_PRODUCT_NAME,
SIMPLE_VIRTUAL_PRODUCT_NAME,
} from '../checkout/constants';
@@ -22,76 +24,135 @@ const test = base.extend< { pageObject: CartPage } >( {
} );
test.describe( 'Shopper → Cart block', () => {
- test( 'User can add, update, remove, and proceed from the cart', async ( {
- frontendUtils,
- page,
+ test( 'The discount label is only visible next to the discounted product', async ( {
pageObject,
+ frontendUtils,
} ) => {
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( DISCOUNTED_PRODUCT_NAME );
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCart();
+ // Check for discount on the discounted product
+ const discountedProductRow = await pageObject.findProductRow(
+ DISCOUNTED_PRODUCT_NAME
+ );
+ await expect( discountedProductRow ).toBeVisible();
+ const capRegularPriceElement = discountedProductRow.locator(
+ '.wc-block-components-product-price__regular'
+ );
+ const capDiscountedPriceElement = discountedProductRow.locator(
+ '.wc-block-components-product-price__value.is-discounted'
+ );
+ await expect( capRegularPriceElement ).toBeVisible();
+ await expect( capDiscountedPriceElement ).toBeVisible();
- await expect(
- page.getByRole( 'heading', {
- name: 'Your cart is empty',
- } )
- ).toBeVisible();
+ // Check for absence of discount on the regular priced product
+ const regularPricedProductRow = await pageObject.findProductRow(
+ REGULAR_PRICED_PRODUCT_NAME
+ );
+ await expect( regularPricedProductRow ).toBeVisible();
+ const hoodieRegularPriceElement = regularPricedProductRow.locator(
+ '.wc-block-components-product-price__regular'
+ );
+ const hoodieDiscountedPriceElement = regularPricedProductRow.locator(
+ '.wc-block-components-product-price__value.is-discounted'
+ );
+ await expect( hoodieRegularPriceElement ).toBeHidden();
+ await expect( hoodieDiscountedPriceElement ).toBeHidden();
+ } );
+
+ test( 'Products with updated prices should not display a discount label', async ( {
+ pageObject,
+ requestUtils,
+ frontendUtils,
+ } ) => {
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-update-price'
+ );
await frontendUtils.goToShop();
- await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( SIMPLE_VIRTUAL_PRODUCT_NAME );
+ await frontendUtils.addToCart( DISCOUNTED_PRODUCT_NAME );
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCart();
- const physicalRow = await pageObject.findProductRow(
- SIMPLE_PHYSICAL_PRODUCT_NAME
+ // Ensure no discount label is present on the products in the cart
+ const capRow = await pageObject.findProductRow(
+ DISCOUNTED_PRODUCT_NAME
+ );
+ await expect( capRow ).toBeVisible();
+ const capRegularPriceElement = capRow.locator(
+ '.wc-block-components-product-price__regular'
);
- const virtualRow = await pageObject.findProductRow(
- SIMPLE_VIRTUAL_PRODUCT_NAME
+ const capDiscountedPriceElement = capRow.locator(
+ '.wc-block-components-product-price__value.is-discounted'
);
- await expect( physicalRow ).toHaveCount( 1 );
- await expect( physicalRow ).toBeVisible();
- await expect( virtualRow ).toHaveCount( 1 );
- await expect( virtualRow ).toBeVisible();
+ await expect( capRegularPriceElement ).toBeHidden();
+ await expect( capDiscountedPriceElement ).toBeHidden();
+
+ // Locate the price element within the Cap product row
+ const capPriceElement = capRow
+ .locator( '.wc-block-components-product-price__value' )
+ .first();
+ await expect( capPriceElement ).toBeVisible();
+
+ // Get the text content of the price element and check the price
+ const capPriceText = await capPriceElement.textContent();
+ expect( capPriceText ).toBe( '$50.00' );
- const quantityInput = page.getByLabel(
- `Quantity of ${ SIMPLE_VIRTUAL_PRODUCT_NAME } in your cart.`
+ const hoodieRow = await pageObject.findProductRow(
+ REGULAR_PRICED_PRODUCT_NAME
);
- const proceedToCheckout = page.getByRole( 'link', {
- name: 'Proceed to Checkout',
- } );
+ await expect( hoodieRow ).toBeVisible();
+ const hoodieRegularPriceElement = hoodieRow.locator(
+ '.wc-block-components-product-price__regular'
+ );
+ const hoodieDiscountedPriceElement = hoodieRow.locator(
+ '.wc-block-components-product-price__value.is-discounted'
+ );
+ await expect( hoodieRegularPriceElement ).toBeHidden();
+ await expect( hoodieDiscountedPriceElement ).toBeHidden();
- await quantityInput.fill( '4' );
+ // Locate the price element within the Hoodie with Logo product row
+ const hoodiePriceElement = hoodieRow
+ .locator( '.wc-block-components-product-price__value' )
+ .first();
+ await expect( hoodiePriceElement ).toBeVisible();
- await expect( proceedToCheckout ).toBeDisabled();
- await expect( proceedToCheckout ).toBeEnabled();
- await expect( quantityInput ).toHaveValue( '4' );
+ // Get the text content of the price element and check the price
+ const hoodiePriceText = await hoodiePriceElement.textContent();
+ expect( hoodiePriceText ).toBe( '$50.00' );
+ } );
- await page
- .getByLabel(
- `Increase quantity of ${ SIMPLE_VIRTUAL_PRODUCT_NAME }`
- )
- .click();
- await expect( proceedToCheckout ).toBeDisabled();
- await expect( proceedToCheckout ).toBeEnabled();
- await expect( quantityInput ).toHaveValue( '5' );
+ test( 'User can view empty cart message', async ( {
+ frontendUtils,
+ page,
+ } ) => {
+ await frontendUtils.goToCart();
- await page
- .getByLabel( `Reduce quantity of ${ SIMPLE_VIRTUAL_PRODUCT_NAME }` )
- .click();
- await expect( proceedToCheckout ).toBeDisabled();
- await expect( proceedToCheckout ).toBeEnabled();
- await expect( quantityInput ).toHaveValue( '4' );
+ // Verify cart is empty
+ await expect(
+ page.getByRole( 'heading', {
+ name: 'Your cart is empty',
+ } )
+ ).toBeVisible();
+ } );
- await physicalRow
+ test( 'User can remove a product from cart', async ( {
+ frontendUtils,
+ page,
+ } ) => {
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
+ await frontendUtils.goToCart();
+ await page
.getByLabel( `Remove ${ SIMPLE_PHYSICAL_PRODUCT_NAME } from cart` )
.click();
- await expect( physicalRow ).toBeHidden();
- await expect( virtualRow ).toBeVisible();
- await expect( quantityInput ).toHaveValue( '4' );
-
- await proceedToCheckout.click();
+ // Verify product is removed from the cart'
await expect(
- page.getByRole( 'button', { name: 'Place Order' } )
+ page.getByRole( 'heading', {
+ name: 'Your cart is empty',
+ } )
).toBeVisible();
} );
@@ -169,6 +230,20 @@ test.describe( 'Shopper → Cart block', () => {
).toHaveValue( '4' );
} );
+ test( 'User can proceed to checkout', async ( { frontendUtils, page } ) => {
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( SIMPLE_VIRTUAL_PRODUCT_NAME );
+ await frontendUtils.goToCart();
+
+ // Click on "Proceed to Checkout" button
+ await page.getByRole( 'link', { name: 'Proceed to Checkout' } ).click();
+
+ // Verify that the "Place Order" button is visible, then we're on the checkout page
+ await expect(
+ page.getByRole( 'button', { name: 'Place Order' } )
+ ).toBeVisible();
+ } );
+
test( 'User can see Cross-Sells products block', async ( {
frontendUtils,
page,
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-coupons.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-coupons.shopper.block_theme.spec.ts
index f8120490fe8..641ef4cb9c2 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-coupons.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-coupons.shopper.block_theme.spec.ts
@@ -34,10 +34,6 @@ test.describe( 'Shopper → Coupon', () => {
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCart();
- // Removing an applied coupon is exercised nowhere else in the E2E suite, on
- // either block. Applying on the Cart block is covered by
- // tests/coupons/cart-block-coupons.spec.ts, so these two clicks are here for
- // the removal that follows them.
await page.getByRole( 'button', { name: 'Add coupons' } ).click();
await page.getByLabel( 'Enter code' ).fill( 'single-use-coupon' );
await page.getByRole( 'button', { name: 'Apply' } ).click();
@@ -76,4 +72,40 @@ test.describe( 'Shopper → Coupon', () => {
page.getByRole( 'cell', { name: '–$2.00' } )
).toBeVisible();
} );
+
+ test( 'Logged in user cannot apply single-use coupon twice', async ( {
+ checkoutPageObject,
+ frontendUtils,
+ page,
+ } ) => {
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await page.getByRole( 'button', { name: 'Add coupons' } ).click();
+ await page.getByLabel( 'Enter code' ).fill( 'single-use-coupon' );
+ await page.getByRole( 'button', { name: 'Apply' } ).click();
+
+ await expect(
+ page.getByLabel( 'Remove coupon "single-use-coupon"' )
+ ).toBeVisible();
+
+ await checkoutPageObject.fillInCheckoutWithTestData();
+ await checkoutPageObject.placeOrder();
+
+ await frontendUtils.emptyCart();
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await page.getByRole( 'button', { name: 'Add coupons' } ).click();
+ await page.getByLabel( 'Enter code' ).fill( 'single-use-coupon' );
+ await page.getByRole( 'button', { name: 'Apply' } ).click();
+
+ await expect(
+ page.getByText(
+ 'Usage limit for coupon "SINGLE-USE-COUPON" has been reached.'
+ )
+ ).toBeVisible();
+ } );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-shipping.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-shipping.block_theme.spec.ts
index ad615e14424..8fb7c442948 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-shipping.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-shipping.block_theme.spec.ts
@@ -1,7 +1,8 @@
/**
* External dependencies
*/
-import { expect, test as base, wpCLI } from '@woocommerce/e2e-utils';
+import { expect, test as base, FrontendUtils } from '@woocommerce/e2e-utils';
+import { Dialog } from '@playwright/test';
/**
* Internal dependencies
@@ -9,186 +10,244 @@ import { expect, test as base, wpCLI } from '@woocommerce/e2e-utils';
import { CheckoutPage } from '../checkout/checkout.page';
import { REGULAR_PRICED_PRODUCT_NAME } from '../checkout/constants';
-type ShippingTopology = {
- defaultRate?: boolean;
- gbRate?: boolean;
- pickup?: boolean;
- requiresAddress: boolean;
-};
-
const test = base.extend< { checkoutPageObject: CheckoutPage } >( {
- checkoutPageObject: async ( { page, requestUtils }, use ) => {
- const pageObject = new CheckoutPage( { page, requestUtils } );
+ checkoutPageObject: async ( { page }, use ) => {
+ const pageObject = new CheckoutPage( {
+ page,
+ } );
await use( pageObject );
},
} );
-const configureShippingTopology = async ( topology: ShippingTopology ) => {
- const encodedTopology = Buffer.from(
- JSON.stringify( {
- defaultRate: false,
- gbRate: false,
- pickup: false,
- ...topology,
- } )
- ).toString( 'base64' );
-
- await wpCLI( `eval '
- $config = json_decode( base64_decode( "${ encodedTopology }" ), true );
-
- foreach ( WC_Shipping_Zones::get_zones() as $zone_data ) {
- $zone = new WC_Shipping_Zone( $zone_data["zone_id"] );
- $zone->delete( true );
- }
+test.describe( 'Merchant → Shipping', () => {
+ test( 'Merchant can hide shipping costs before address is entered', async ( {
+ page,
+ shippingUtils,
+ localPickupUtils,
+ } ) => {
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.enableShippingCostsRequireAddress();
- $default_zone = WC_Shipping_Zones::get_zone( 0 );
- foreach ( $default_zone->get_shipping_methods() as $method ) {
- $default_zone->delete_shipping_method( $method->instance_id );
- }
+ await expect(
+ page.getByLabel( 'Hide shipping costs until an address is entered' )
+ ).toBeChecked();
+ } );
+} );
- update_option( "woocommerce_flat_rate_settings", array( "enabled" => "no" ) );
- update_option( "woocommerce_free_shipping_settings", array( "enabled" => "no" ) );
- update_option( "woocommerce_local_pickup_settings", array( "enabled" => "no" ) );
- update_option( "woocommerce_default_customer_address", "" );
- update_option( "woocommerce_shipping_cost_requires_address", $config["requiresAddress"] ? "yes" : "no" );
-
- if ( $config["defaultRate"] ) {
- $instance_id = $default_zone->add_shipping_method( "flat_rate" );
- update_option(
- "woocommerce_flat_rate_{$instance_id}_settings",
- array( "enabled" => "yes", "title" => "Home delivery", "tax_status" => "none", "cost" => "10" )
- );
- }
+test.describe( 'Shopper → Shipping', () => {
+ test.beforeEach( async ( { shippingUtils } ) => {
+ await shippingUtils.enableShippingCostsRequireAddress();
+ } );
- if ( $config["gbRate"] ) {
- $gb_zone = new WC_Shipping_Zone();
- $gb_zone->set_zone_name( "United Kingdom" );
- $gb_zone->set_zone_locations( array( (object) array( "code" => "GB", "type" => "country" ) ) );
- $gb_zone->save();
- $instance_id = $gb_zone->add_shipping_method( "flat_rate" );
- update_option(
- "woocommerce_flat_rate_{$instance_id}_settings",
- array( "enabled" => "yes", "title" => "UK delivery", "tax_status" => "none", "cost" => "15" )
- );
- }
+ test.beforeEach( async ( { admin } ) => {
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=general' );
+ await admin.page
+ .getByLabel( 'Default customer location' )
+ .selectOption( 'No location by default' );
+ await admin.saveAdminPage();
- update_option(
- "woocommerce_pickup_location_settings",
- array( "enabled" => $config["pickup"] ? "yes" : "no", "title" => "Pickup", "tax_status" => "none", "cost" => "" )
- );
- update_option(
- "pickup_location_pickup_locations",
- $config["pickup"]
- ? array(
- array(
- "name" => "Automattic, Inc.",
- "address" => array(
- "address_1" => "60 29th Street, Suite 343",
- "city" => "San Francisco",
- "postcode" => "94110",
- "state" => "CA",
- "country" => "US"
- ),
- "details" => "American entity",
- "enabled" => true
- )
- )
- : array()
+ await admin.visitAdminPage(
+ 'admin.php?page=wc-settings&tab=shipping&zone_id=new'
);
+ await admin.page.getByLabel( 'Zone name' ).fill( 'UK' );
+ await admin.page
+ .getByRole( 'combobox', { name: 'Start typing to filter zones' } )
+ .fill( 'United Kingdom' );
+ await admin.page
+ .getByRole( 'checkbox', { name: 'United Kingdom (UK)' } )
+ .click(); // .check() won't work here as the input disappears immediately after checking.
+ await admin.saveAdminPage();
+ await admin.page
+ .getByRole( 'button', { name: 'Add shipping method' } )
+ .click();
+ await admin.page.getByText( 'Flat rate' ).click();
+ await admin.page.getByRole( 'button', { name: 'Continue' } ).click();
+ await admin.page
+ .getByRole( 'button', { name: 'Save zone and method' } )
+ .click();
+ await expect( admin.page.getByText( 'Flat rate' ) ).toBeVisible();
+ if (
+ ! ( await admin.page
+ .getByRole( 'button', { name: 'Save changes' } )
+ .isDisabled() )
+ ) {
+ await admin.saveAdminPage();
+ }
+ await expect(
+ admin.page.getByRole( 'button', { name: 'Save changes' } )
+ ).toBeDisabled();
+ } );
- WC_Cache_Helper::get_transient_version( "shipping", true );
- delete_transient( "wc_shipping_method_count" );
- WC()->shipping()->reset_shipping();
- '` );
-};
+ // Series of tests below to cover the following scenarios: see PR https://github.com/woocommerce/woocommerce/pull/56460 for more details
-test.describe( 'Merchant → Shipping', () => {
- test( 'Merchant can hide shipping costs before address is entered', async ( {
- page,
+ /**
+ * Rates enabled for default customer location
+ * Rates enabled for _any_ location
+ * Local pickup enabled
+ * Hide rates until an address is entered (only applicable when Local pickup is DISABLED)
+ *
+ * 1. Y Y Y #
+ * 2. Y Y N N
+ * 3. Y N Y # - Skipping because this behaves the same as test 1.
+ * 4. Y N N N
+ * 5. N Y Y #
+ * 6. N Y N N
+ * 7. N N Y # - known bug here with the text/UI to enter an address, despite there being no methods.
+ * 8. N N N N
+ * 9. Y Y N Y
+ * 10. Y N N Y
+ * 11. N Y N Y
+ * 12. N N N Y
+ */
+
+ test( '1. With shipping methods for the default location, shipping methods for _any_ location, and local pickup enabled, the shopper sees shipping rates and pickup options - rates are selected default', async ( {
+ localPickupUtils,
+ frontendUtils,
shippingUtils,
} ) => {
- await configureShippingTopology( { requiresAddress: false } );
- await shippingUtils.openShippingSettings();
-
- const hideShippingCosts = page.getByLabel(
- 'Hide shipping costs until an address is entered'
- );
- await expect( hideShippingCosts ).not.toBeChecked();
- await hideShippingCosts.check();
- await shippingUtils.saveShippingSettings();
- await page.reload();
+ await localPickupUtils.enableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
+ await localPickupUtils.addPickupLocation( {
+ location: {
+ name: 'Automattic, Inc.',
+ address: '60 29th Street, Suite 343',
+ city: 'San Francisco',
+ postcode: '94110',
+ state: 'US:CA',
+ details: 'American entity',
+ },
+ } );
- await expect( hideShippingCosts ).toBeChecked();
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeChecked();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeChecked();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeChecked();
} );
-} );
-test.describe( 'Shopper → Shipping', () => {
- test( 'Shopper can switch between shipping and local pickup while preserving the effective selected rate', async ( {
+ test( '2. With shipping methods for the default location, shipping methods for _any_ location, local pickup disabled, and shipping costs requires address disabled, the shopper sees shipping rates only', async ( {
+ localPickupUtils,
frontendUtils,
- page,
+ shippingUtils,
} ) => {
- await configureShippingTopology( {
- defaultRate: true,
- pickup: true,
- requiresAddress: false,
- } );
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
await frontendUtils.goToShop();
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCheckout();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeHidden();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeChecked();
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeChecked();
+ } );
- const ship = page.getByRole( 'radio', { name: 'Ship', exact: true } );
- const pickup = page.getByRole( 'radio', {
- name: 'Pickup',
- exact: true,
- } );
- const deliveryRate = page.getByRole( 'radio', {
- name: /Home delivery/,
- } );
- const pickupRate = page.getByRole( 'radio', {
- name: /Automattic, Inc\./,
- } );
+ // 3. With shipping methods for the default location, no shipping methods for _any_ other location, and local pickup enabled, the shopper sees shipping rates and pickup options - skipped as same result as 1.
- await expect( ship ).toBeChecked();
- await expect( pickup ).not.toBeChecked();
- await expect( deliveryRate ).toBeChecked();
+ test( '4. With shipping methods for the default location, no shipping methods for _any_ other location, local pickup disabled, and shipping costs require address disabled, the shopper sees shipping rates only after entering an address', async ( {
+ localPickupUtils,
+ admin,
+ frontendUtils,
+ checkoutPageObject,
+ shippingUtils,
+ } ) => {
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
- await pickup.click();
- await expect( pickup ).toBeChecked();
- await expect( ship ).not.toBeChecked();
- await expect( pickupRate ).toBeChecked();
- await expect( deliveryRate ).toBeHidden();
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+ // Accept the delete dialog, then remove the listener;
+ const acceptDialog = ( dialog: Dialog ) => dialog.accept();
+ admin.page.on( 'dialog', acceptDialog );
+ await admin.page.getByRole( 'link', { name: 'Delete' } ).click();
+ admin.page.off( 'dialog', acceptDialog );
- await ship.click();
- await expect( ship ).toBeChecked();
- await expect( pickup ).not.toBeChecked();
- await expect( deliveryRate ).toBeChecked();
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await expect(
+ frontendUtils.page.getByText(
+ 'Enter a shipping address to view shipping options'
+ )
+ ).toBeHidden();
+
+ await expect(
+ checkoutPageObject.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeChecked();
} );
- test( 'Shopper sees no matching rates until a complete address reveals the zone rate', async ( {
- checkoutPageObject,
+ // 5. With no shipping methods for the default location, but shipping methods for _any_ other location, local pickup enabled, the shopper sees pickup rates until entering an address for the zone with rates
+ // Not testing because this is a "bug" we are going to fix - see https://github.com/woocommerce/woocommerce/issues/56462
+
+ test( '6. With no shipping methods for the default location, but shipping methods for _any_ other location, local pickup disabled, and shipping costs require address disabled, the shopper sees shipping rates only after entering an address', async ( {
+ localPickupUtils,
+ admin,
frontendUtils,
- page,
+ checkoutPageObject,
+ shippingUtils,
} ) => {
- await configureShippingTopology( {
- gbRate: true,
- requiresAddress: true,
- } );
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=general' );
+ await admin.page
+ .getByLabel( 'Default customer location' )
+ .selectOption( 'No location by default' );
+ await admin.saveAdminPage();
+
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+
+ await admin.page
+ .getByRole( 'row', { name: 'Rest of the world' } )
+ .getByRole( 'link' )
+ .click();
+
+ // There are two shipping rates enabled. Clicking the first one turns it off.
+ // Then only one "name: yes" remains, making it the first, even though it's the second rate.
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.saveAdminPage();
await frontendUtils.goToShop();
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCheckout();
await expect(
- page.getByText(
- /Enter a shipping address to view shipping options/
+ frontendUtils.page.getByText(
+ 'Enter a shipping address to view shipping options'
)
).toBeVisible();
-
await checkoutPageObject.fillInCheckoutWithTestData();
- await checkoutPageObject.waitForCheckoutToFinishUpdating();
+
await expect(
- page
+ checkoutPageObject.page
.getByLabel( 'Checkout' )
.getByText(
'No shipping options are available for this address. Please verify the address is correct or try a different address.'
@@ -196,26 +255,275 @@ test.describe( 'Shopper → Shipping', () => {
).toBeVisible();
await checkoutPageObject.fillInCheckoutWithTestData( {
- country: 'United Kingdom (UK)',
- countryKey: 'GB',
- city: 'London',
- state: '',
- postcode: 'EC4M 9AF',
+ country: 'GB',
+ postcode: 'SW19 5AE',
} );
- const shippingAddress = page.getByRole( 'group', {
- name: 'Shipping address',
+ await expect(
+ checkoutPageObject.page.getByRole( 'radio', {
+ name: 'Flat rate Free',
+ } )
+ ).toBeChecked();
+ } );
+
+ test( '7. With no shipping methods for the default location, no shipping methods for _any_ other location, local pickup enabled the shopper sees local pickup rates only', async ( {
+ localPickupUtils,
+ admin,
+ frontendUtils,
+ shippingUtils,
+ } ) => {
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+ // Accept the delete dialog, then remove the listener;
+ const acceptDialog = ( dialog: Dialog ) => dialog.accept();
+ admin.page.on( 'dialog', acceptDialog );
+ await admin.page.getByRole( 'link', { name: 'Delete' } ).click();
+ admin.page.off( 'dialog', acceptDialog );
+
+ await localPickupUtils.enableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
+ await localPickupUtils.addPickupLocation( {
+ location: {
+ name: 'Automattic, Inc.',
+ address: '60 29th Street, Suite 343',
+ city: 'San Francisco',
+ postcode: '94110',
+ state: 'US:CA',
+ details: 'American entity',
+ },
} );
+
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+
+ await admin.page
+ .getByRole( 'row', { name: 'Rest of the world' } )
+ .getByRole( 'link' )
+ .click();
+
+ // There are two shipping rates enabled. Clicking the first one turns it off.
+ // Then only one "name: yes" remains, making it the first, even though it's the second rate.
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.saveAdminPage();
+
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
await expect(
- shippingAddress.getByLabel( 'Country/Region' )
- ).toHaveValue( 'GB' );
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Automattic, Inc. free 60 29th',
+ } )
+ ).toBeChecked();
+
await expect(
- shippingAddress.locator( '#shipping-postcode' )
- ).toHaveValue( 'EC4M 9AF' );
- await checkoutPageObject.waitForCheckoutToFinishUpdating();
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeHidden();
+ } );
+
+ // Skipping test due to a known bug with needs_shipping - see issue https://github.com/woocommerce/woocommerce/issues/56507
+ test.skip( '8. With no shipping methods for the default location, no shipping methods for _any_ other location, local pickup disabled the shopper sees no shipping and no pickup rates', async ( {
+ localPickupUtils,
+ admin,
+ frontendUtils,
+ shippingUtils,
+ } ) => {
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+ // Accept the delete dialog, then remove the listener;
+ const acceptDialog = ( dialog: Dialog ) => dialog.accept();
+ admin.page.on( 'dialog', acceptDialog );
+ await admin.page.getByRole( 'link', { name: 'Delete' } ).click();
+ admin.page.off( 'dialog', acceptDialog );
+
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.disableShippingCostsRequireAddress();
+
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+
+ await admin.page
+ .getByRole( 'row', { name: 'Rest of the world' } )
+ .getByRole( 'link' )
+ .click();
+
+ // There are two shipping rates enabled. Clicking the first one turns it off.
+ // Then only one "name: yes" remains, making it the first, even though it's the second rate.
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.saveAdminPage();
+
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
await expect(
- page.getByRole( 'radio', { name: /UK delivery/ } )
- ).toBeChecked();
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeHidden();
+
+ await expect(
+ frontendUtils.page.getByRole( 'heading', {
+ name: 'Shipping options',
+ } )
+ ).toBeHidden();
+ } );
+
+ test( '9. With shipping methods for the default location, shipping methods for _any_ other location, local pickup disabled and shipping requires address enabled, shopper sees shipping rates immediately.', async ( {
+ localPickupUtils,
+ frontendUtils,
+ shippingUtils,
+ checkoutPageObject,
+ } ) => {
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.enableShippingCostsRequireAddress();
+
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await expect(
+ frontendUtils.page.getByText(
+ 'Enter a shipping address to view shipping options.'
+ )
+ ).toBeVisible();
+
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeHidden();
+
+ await expect(
+ frontendUtils.page.getByRole( 'heading', {
+ name: 'Shipping options',
+ } )
+ ).toBeVisible();
+
+ await checkoutPageObject.fillInCheckoutWithTestData();
+
+ await expect(
+ checkoutPageObject.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeVisible();
+ } );
+
+ test( '10. With shipping methods for the default location, no shipping methods for _any_ other location, local pickup disabled, and shipping costs require address enabled, the shopper sees shipping rates only after entering an address', async ( {
+ localPickupUtils,
+ admin,
+ frontendUtils,
+ checkoutPageObject,
+ shippingUtils,
+ } ) => {
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.enableShippingCostsRequireAddress();
+
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+ // Accept the delete dialog, then remove the listener;
+ const acceptDialog = ( dialog: Dialog ) => dialog.accept();
+ admin.page.on( 'dialog', acceptDialog );
+ await admin.page.getByRole( 'link', { name: 'Delete' } ).click();
+ admin.page.off( 'dialog', acceptDialog );
+
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await expect(
+ frontendUtils.page.getByText(
+ 'Enter a shipping address to view shipping options'
+ )
+ ).toBeVisible();
+ await checkoutPageObject.fillInCheckoutWithTestData();
+
+ await expect(
+ checkoutPageObject.page.getByRole( 'radio', {
+ name: 'Flat rate shipping $',
+ } )
+ ).toBeVisible();
+ } );
+
+ // 11. With no shipping methods for the default location, but shipping methods for _any_ other location, local pickup disabled, and shipping requires address enabled, the shopper sees no shipping until an address is entered no pickup rates
+ // Skipping testing 11 because it is the same as 6.
+
+ // Skipping test due to a known bug with needs_shipping - see issue https://github.com/woocommerce/woocommerce/issues/56507
+ test.skip( '12. With no shipping methods for the default location, no shipping methods for _any_ other location, local pickup disabled, and shipping requires address enabled the shopper sees no shipping and no pickup rates', async ( {
+ localPickupUtils,
+ admin,
+ frontendUtils,
+ shippingUtils,
+ } ) => {
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+ // Accept the delete dialog, then remove the listener;
+ const acceptDialog = ( dialog: Dialog ) => dialog.accept();
+ admin.page.on( 'dialog', acceptDialog );
+ await admin.page.getByRole( 'link', { name: 'Delete' } ).click();
+ admin.page.off( 'dialog', acceptDialog );
+
+ await localPickupUtils.disableLocalPickup();
+ await shippingUtils.enableShippingCostsRequireAddress();
+
+ await admin.visitAdminPage( 'admin.php?page=wc-settings&tab=shipping' );
+
+ await admin.page
+ .getByRole( 'row', { name: 'Rest of the world' } )
+ .getByRole( 'link' )
+ .click();
+
+ // There are two shipping rates enabled. Clicking the first one turns it off.
+ // Then only one "name: yes" remains, making it the first, even though it's the second rate.
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.page.getByRole( 'link', { name: 'Yes' } ).first().click();
+ await admin.saveAdminPage();
+
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.goToCheckout();
+
+ await expect(
+ frontendUtils.page.getByRole( 'radio', {
+ name: 'Ship',
+ exact: true,
+ } )
+ ).toBeHidden();
+
+ await expect(
+ frontendUtils.page.getByRole( 'heading', {
+ name: 'Shipping options',
+ } )
+ ).toBeHidden();
+ } );
+
+ test( 'Guest user does not see shipping rates until full address is entered', async ( {
+ requestUtils,
+ browser,
+ } ) => {
+ const guestContext = await browser.newContext();
+ const userPage = await guestContext.newPage();
+
+ const userFrontendUtils = new FrontendUtils( userPage, requestUtils );
+ const userCheckoutPageObject = new CheckoutPage( { page: userPage } );
+
+ await userFrontendUtils.goToShop();
+ await userFrontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await userFrontendUtils.goToCheckout();
+
+ await expect(
+ userPage.getByText(
+ 'Enter a shipping address to view shipping options.'
+ )
+ ).toBeVisible();
+
+ await userCheckoutPageObject.fillInCheckoutWithTestData();
+
+ await expect(
+ userPage.getByText(
+ 'Enter a shipping address to view shipping options.'
+ )
+ ).toBeHidden();
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-store.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-store.block_theme.spec.ts
index eab4ff760bd..83b4f025a9e 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-store.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-store.block_theme.spec.ts
@@ -11,97 +11,63 @@ import { REGULAR_PRICED_PRODUCT_NAME } from '../checkout/constants';
test.describe( 'Cart Store', () => {
test.use( { storageState: guestFile } );
+ test.beforeEach( async ( { requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-short-nonce-life'
+ );
+ } );
+
test( 'should refresh nonce from Store API and use it for cart mutations', async ( {
page,
frontendUtils,
} ) => {
- const refreshCartResponse = page.waitForResponse(
- ( response ) =>
- response.url().includes( '/wc/store/v1/cart' ) &&
- response.request().method() === 'GET'
- );
- let initialBatchRequestNonce: string | null = null;
- let invalidateNextBatch = false;
- let invalidBatchRequestNonce: string | null = null;
- let invalidResponseNonce: string | null = null;
- let invalidResponseBody = '';
- let captureRecoveryBatch = false;
- let recoveryBatchRequestNonce: string | null = null;
+ let refreshNonce: string | null = null;
+ let requestNonce: string | null = null;
+ let responseNonce: string | null = null;
- // Forward exactly one invalid nonce to the real Store API so its response
- // supplies the replacement nonce the client must use for the next mutation.
- await page.route( '**/wc/store/v1/batch**', async ( route ) => {
- const batch = JSON.parse( route.request().postData() || '{}' );
- const [ batchRequest ] = batch.requests || [];
- const isCartAddItemBatch =
- batch.requests?.length === 1 &&
- batchRequest?.method === 'POST' &&
- batchRequest.path === '/wc/store/v1/cart/add-item';
- const requestNonce = isCartAddItemBatch
- ? batchRequest.headers?.Nonce || null
- : null;
-
- if ( invalidateNextBatch && isCartAddItemBatch ) {
- invalidateNextBatch = false;
- invalidBatchRequestNonce = requestNonce;
- const invalidBatch = {
- ...batch,
- requests: [
- {
- ...batchRequest,
- headers: {
- ...batchRequest.headers,
- Nonce: 'invalid-test-nonce',
- },
- },
- ],
- };
- const response = await route.fetch( {
- postData: JSON.stringify( invalidBatch ),
- } );
- invalidResponseNonce = response.headers().nonce || null;
- invalidResponseBody = await response.text();
+ // Intercept GET /cart (refreshCartItems) to capture the nonce.
+ await page.route( '**/wc/store/v1/cart**', async ( route ) => {
+ if ( route.request().method() === 'GET' ) {
+ const response = await route.fetch();
+ refreshNonce = response.headers().nonce || null;
await route.fulfill( { response } );
- return;
- }
-
- if (
- isCartAddItemBatch &&
- captureRecoveryBatch &&
- ! recoveryBatchRequestNonce
- ) {
- recoveryBatchRequestNonce = requestNonce;
- } else if ( isCartAddItemBatch && ! initialBatchRequestNonce ) {
- initialBatchRequestNonce = requestNonce;
+ } else {
+ await route.continue();
}
+ } );
+ // Intercept batch requests to track which nonce the client sends
+ // and which nonce the server returns.
+ await page.route( '**/wc/store/v1/batch**', async ( route ) => {
+ requestNonce = route.request().headers().nonce || null;
const response = await route.fetch();
+ responseNonce = response.headers().nonce || null;
await route.fulfill( { response } );
} );
await frontendUtils.goToShop();
- const refreshCartNonce =
- ( await refreshCartResponse ).headers().nonce || null;
- expect( refreshCartNonce ).toBeTruthy();
+
+ // The GET /cart fires during page load, so a waitForResponse set up
+ // here can miss it — poll the intercepted nonce instead.
+ await expect.poll( () => refreshNonce ).toBeTruthy();
// Adding a product should use the nonce from refreshCartItems.
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- expect( initialBatchRequestNonce ).toBe( refreshCartNonce );
+ expect( requestNonce ).toBe( refreshNonce );
+
+ // Wait for the nonce to expire.
+ // eslint-disable-next-line playwright/no-wait-for-timeout, no-restricted-syntax
+ await page.waitForTimeout( 2000 );
- // Forward an invalid nonce and let the real Store API return its error and replacement nonce.
- invalidateNextBatch = true;
+ // Adding another product should fail because it is using an expired nonce.
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- expect( invalidResponseBody ).toContain(
- 'woocommerce_rest_invalid_nonce'
- );
await expect( page.getByText( 'Nonce is invalid.' ) ).toBeVisible();
- expect( invalidBatchRequestNonce ).toBe( initialBatchRequestNonce );
- expect( invalidResponseNonce ).toBeTruthy();
+ const previousResponseNonce = responseNonce;
- // The next mutation should use the exact nonce returned by the invalid response.
- captureRecoveryBatch = true;
+ // Nonce should be updated now and the request should succeed.
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- expect( recoveryBatchRequestNonce ).toBe( invalidResponseNonce );
+ expect( requestNonce ).not.toBe( refreshNonce );
+ expect( requestNonce ).toBe( previousResponseNonce );
// Verify the product was actually added to the cart properly.
await frontendUtils.goToCart();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
index 3cea2928fc0..e6fd2127c68 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
@@ -284,13 +284,6 @@ class WC_Tests_Coupon extends WC_Unit_Test_Case {
/**
* Test an item limit for percent discounts.
- *
- * Defines WOOCOMMERCE_CHECKOUT, which a process cannot undefine, so it stays set
- * for every test that runs after this one. Do not reach for process isolation to
- * contain that: the forked child re-runs tests/legacy/bootstrap.php, which writes
- * woocommerce_custom_orders_table_enabled over its own connection, outside the
- * parent's rolled-back transaction, and flips the order store for the rest of the
- * run.
*/
public function test_percent_discount_item_limit() {
// Create product.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
index ea631492200..0b1f9ca1023 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
@@ -146,45 +146,6 @@ class Cart extends ControllerTestCase {
wc()->customer->set_shipping_country( 'US' );
}
- /**
- * Dispatches a Store API add-item request.
- *
- * @param array $body Request body.
- * @return \WP_REST_Response
- */
- private function dispatch_add_item_request( array $body ): \WP_REST_Response {
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/add-item' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params( $body );
-
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Gets a canonical map of the cart lines that participate in identity.
- *
- * @return array<string, array{key: string, product_id: int, variation_id: int, variation: array, quantity: int|float}>
- */
- private function get_cart_line_identity_map(): array {
- $cart_line_map = array();
-
- foreach ( WC()->cart->get_cart() as $key => $cart_item ) {
- $variation = $cart_item['variation'] ?? array();
- ksort( $variation );
-
- $cart_line_map[ $key ] = array(
- 'key' => $key,
- 'product_id' => (int) $cart_item['product_id'],
- 'variation_id' => (int) $cart_item['variation_id'],
- 'variation' => $variation,
- 'quantity' => wc_stock_amount( $cart_item['quantity'] ),
- );
- }
-
- ksort( $cart_line_map );
- return $cart_line_map;
- }
-
/**
* Test getting cart.
*/
@@ -367,420 +328,6 @@ class Cart extends ControllerTestCase {
);
}
- /**
- * Test the shipping topology returned by the Store API.
- *
- * @testdox Store API serializes the expected rates and selected method for $scenario.
- * @dataProvider shipping_topology_provider
- *
- * @param string $scenario Scenario name.
- * @param bool $has_default_rate Whether the Rest of the World zone has a rate.
- * @param bool $has_gb_rate Whether the GB zone has a rate.
- * @param bool $has_pickup Whether Local Pickup is enabled.
- * @param bool $requires_address Whether rates require a complete address.
- * @param string $address_key Address fixture key.
- * @param array $expected_rate_keys Expected rate fixture keys.
- * @param string $expected_selected_rate_key Expected selected rate fixture key.
- */
- public function test_shipping_topology(
- string $scenario,
- bool $has_default_rate,
- bool $has_gb_rate,
- bool $has_pickup,
- bool $requires_address,
- string $address_key,
- array $expected_rate_keys,
- string $expected_selected_rate_key
- ): void {
- unset( $scenario );
-
- $customer = $this->capture_shipping_address();
- $session = $this->capture_shipping_session();
- $cart_context = WC()->cart->cart_context;
- $country_data = get_object_vars( WC()->countries );
- $has_locale = array_key_exists( 'locale', $country_data );
- $locale = $has_locale ? $country_data['locale'] : array();
- $fixtures = new FixtureData();
- $topology = array();
-
- try {
- unset( WC()->countries->locale );
- $this->configure_shipping_topology( $fixtures, $has_default_rate, $has_gb_rate, $has_pickup, $topology );
-
- update_option( 'woocommerce_default_customer_address', 'blank' === $address_key ? '' : 'base' );
- update_option( 'woocommerce_shipping_cost_requires_address', $requires_address ? 'yes' : 'no' );
- WC()->cart->cart_context = 'store-api';
- $this->clear_shipping_session();
-
- $address = $this->get_shipping_address_fixture( $address_key );
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/update-customer' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params( array( 'shipping_address' => (object) $address ) );
- $response = rest_get_server()->dispatch( $request );
- $data = $response->get_data();
-
- $this->assertSame( 200, $response->get_status(), 'The customer update should succeed.' );
- $this->assertTrue( $data['needs_shipping'], 'The cart should continue to require shipping.' );
-
- $expected_package_count = $requires_address && 'blank' === $address_key && ! $has_pickup ? 0 : 1;
- $this->assertCount( $expected_package_count, $data['shipping_rates'], 'The response should serialize the exact expected package count.' );
-
- if ( 0 === $expected_package_count ) {
- $this->assertSame( array(), WC()->session->get( 'chosen_shipping_methods', array() ), 'No hidden package should choose a shipping method.' );
- return;
- }
-
- $package = (array) $data['shipping_rates'][0];
- $destination = (array) $package['destination'];
- $rates = array_map(
- function ( $rate ): array {
- return (array) $rate;
- },
- $package['shipping_rates']
- );
- $expected_destination = array_intersect_key(
- $address,
- array_flip( array( 'address_1', 'address_2', 'city', 'state', 'postcode', 'country' ) )
- );
-
- $this->assertSame( $expected_destination, $destination, 'The serialized package destination should match the submitted address exactly.' );
-
- $expected_rate_ids = array_map(
- function ( string $key ) use ( $topology ): string {
- return $topology['rate_ids'][ $key ];
- },
- $expected_rate_keys
- );
- $actual_rate_ids = array_column( $rates, 'rate_id' );
- sort( $expected_rate_ids );
- sort( $actual_rate_ids );
-
- $this->assertSame( $expected_rate_ids, $actual_rate_ids, 'The response should contain only the expected concrete rates.' );
-
- $expected_method_ids = array_map(
- function ( string $key ) use ( $topology ): string {
- return $topology['method_ids'][ $key ];
- },
- $expected_rate_keys
- );
- $actual_method_ids = array_column( $rates, 'method_id' );
- sort( $expected_method_ids );
- sort( $actual_method_ids );
-
- $this->assertSame( $expected_method_ids, $actual_method_ids, 'The response should contain only the expected shipping method types.' );
-
- $selected_by_rate = array_column( $rates, 'selected', 'rate_id' );
- foreach ( $expected_rate_ids as $rate_id ) {
- $this->assertSame(
- $expected_selected_rate_key && $topology['rate_ids'][ $expected_selected_rate_key ] === $rate_id,
- $selected_by_rate[ $rate_id ],
- "The selected flag should be exact for {$rate_id}."
- );
- }
-
- $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() );
- $expected_chosen = $expected_selected_rate_key ? $topology['rate_ids'][ $expected_selected_rate_key ] : '';
- $this->assertArrayHasKey( 0, $chosen_methods, 'A serialized package should persist a choice for package zero.' );
- $this->assertSame( $expected_chosen, $chosen_methods[0], 'The session should persist the same method selected in the response.' );
- } finally {
- // The five shipping options this test writes are rows inside the transaction.
- // Everything else below is on the WC() singletons, which this class does not
- // inherit a teardown for: it extends WP_Test_REST_TestCase, not
- // WC_Unit_Test_Case, so clear_wc_singleton_state() never runs.
- $this->remove_shipping_topology( $fixtures, $topology );
- $this->restore_shipping_address( $customer );
- WC()->shipping()->reset_shipping();
- $this->restore_shipping_session( $session );
- WC()->cart->cart_context = $cart_context;
- if ( $has_locale ) {
- WC()->countries->locale = $locale;
- } else {
- unset( WC()->countries->locale );
- }
- \WC_Cache_Helper::get_transient_version( 'shipping', true );
- delete_transient( 'wc_shipping_method_count' );
- }
- }
-
- /**
- * The topology owner restores both present and missing incoming session values.
- *
- * @testdox Shipping topology restores exact incoming shipping and customer session values.
- */
- public function test_shipping_topology_restores_existing_session_state(): void {
- $keys = array( 'chosen_shipping_methods', 'customer', 'shipping_method_counts' );
- $original = array();
-
- foreach ( $keys as $key ) {
- $original[ $key ] = array(
- 'exists' => isset( WC()->session->{$key} ),
- 'value' => WC()->session->get( $key ),
- );
- }
-
- try {
- $chosen_methods = array( 'external_shipping:7' );
- $customer = array( 'shipping_country' => 'SE' );
- WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
- WC()->session->set( 'customer', $customer );
- WC()->session->set( 'shipping_method_counts', null );
-
- $this->test_shipping_topology(
- 'session restoration',
- false,
- true,
- true,
- true,
- 'blank',
- array( 'pickup' ),
- ''
- );
-
- $this->assertTrue( isset( WC()->session->chosen_shipping_methods ), 'The incoming chosen-method key should remain present.' );
- $this->assertSame( $chosen_methods, WC()->session->get( 'chosen_shipping_methods' ), 'The incoming chosen methods should be restored exactly.' );
- $this->assertTrue( isset( WC()->session->customer ), 'The incoming customer key should remain present.' );
- $this->assertSame( $customer, WC()->session->get( 'customer' ), 'The incoming customer session value should be restored exactly.' );
- $this->assertFalse( isset( WC()->session->shipping_method_counts ), 'An incoming missing session key should remain missing.' );
- } finally {
- foreach ( $original as $key => $session_value ) {
- WC()->session->set( $key, $session_value['exists'] ? $session_value['value'] : null );
- }
- }
- }
-
- /**
- * Shipping topology scenarios.
- *
- * @return array<string, array{string, bool, bool, bool, bool, string, string[], string}>
- */
- public function shipping_topology_provider(): array {
- return array(
- 'default and any rates with pickup' => array( 'default and any rates with pickup', true, true, true, false, 'us', array( 'default', 'pickup' ), 'default' ),
- 'default and any rates without pickup' => array( 'default and any rates without pickup', true, true, false, false, 'us', array( 'default' ), 'default' ),
- 'default rate only' => array( 'default rate only', true, false, false, false, 'us', array( 'default' ), 'default' ),
- 'any rate only before address' => array( 'any rate only before address', false, true, false, false, 'blank', array(), '' ),
- 'any rate only for a nonmatching address' => array( 'any rate only for a nonmatching address', false, true, false, false, 'us', array(), '' ),
- 'any rate only for a matching address' => array( 'any rate only for a matching address', false, true, false, false, 'gb', array( 'gb' ), 'gb' ),
- 'pickup only without required address' => array( 'pickup only without required address', false, false, true, false, 'blank', array( 'pickup' ), 'pickup' ),
- 'pickup only with required address' => array( 'pickup only with required address', false, true, true, true, 'blank', array( 'pickup' ), '' ),
- 'no rates without required address' => array( 'no rates without required address', false, false, false, false, 'blank', array(), '' ),
- 'no rates with required address' => array( 'no rates with required address', false, false, false, true, 'blank', array(), '' ),
- 'required address before entry' => array( 'required address before entry', true, true, false, true, 'blank', array(), '' ),
- 'required address with default rate' => array( 'required address with default rate', true, true, false, true, 'us', array( 'default' ), 'default' ),
- 'required address with any rate' => array( 'required address with any rate', true, true, false, true, 'gb', array( 'gb' ), 'gb' ),
- );
- }
-
- /**
- * Configure real zones and shipping methods for a topology scenario.
- *
- * @param FixtureData $fixtures Fixture helper.
- * @param bool $has_default_rate Whether the default zone gets a rate.
- * @param bool $has_gb_rate Whether the GB zone gets a rate.
- * @param bool $has_pickup Whether pickup is enabled.
- * @param array $topology Created topology data, updated as records are added.
- */
- private function configure_shipping_topology( FixtureData $fixtures, bool $has_default_rate, bool $has_gb_rate, bool $has_pickup, array &$topology ): void {
- update_option( 'woocommerce_flat_rate_settings', array( 'enabled' => 'no' ) );
-
- $topology = array(
- 'rate_ids' => array( 'pickup' => 'pickup_location:0' ),
- 'method_ids' => array(
- 'default' => 'flat_rate',
- 'gb' => 'flat_rate',
- 'pickup' => 'pickup_location',
- ),
- );
-
- if ( $has_default_rate ) {
- $default_zone = \WC_Shipping_Zones::get_zone( 0 );
- if ( ! $default_zone instanceof \WC_Shipping_Zone ) {
- throw new \RuntimeException( 'The default shipping zone should be available.' );
- }
-
- $topology['default_instance_id'] = $default_zone->add_shipping_method( 'flat_rate' );
- $topology['rate_ids']['default'] = 'flat_rate:' . $topology['default_instance_id'];
- update_option(
- 'woocommerce_flat_rate_' . $topology['default_instance_id'] . '_settings',
- array(
- 'enabled' => 'yes',
- 'title' => 'Default delivery',
- 'tax_status' => 'none',
- 'cost' => '10',
- )
- );
- }
-
- if ( $has_gb_rate ) {
- $gb_zone = new \WC_Shipping_Zone();
- $gb_zone->set_zone_name( 'United Kingdom' );
- $gb_zone->set_zone_locations(
- array(
- (object) array(
- 'code' => 'GB',
- 'type' => 'country',
- ),
- )
- );
- $gb_zone->save();
- $topology['gb_zone'] = $gb_zone;
- $topology['gb_instance_id'] = $gb_zone->add_shipping_method( 'flat_rate' );
- $topology['rate_ids']['gb'] = 'flat_rate:' . $topology['gb_instance_id'];
- update_option(
- 'woocommerce_flat_rate_' . $topology['gb_instance_id'] . '_settings',
- array(
- 'enabled' => 'yes',
- 'title' => 'UK delivery',
- 'tax_status' => 'none',
- 'cost' => '15',
- )
- );
- }
-
- if ( $has_pickup ) {
- $fixtures->shipping_add_pickup_location();
- $topology['pickup_registered'] = true;
- }
-
- WC()->shipping()->reset_shipping();
- \WC_Cache_Helper::get_transient_version( 'shipping', true );
- delete_transient( 'wc_shipping_method_count' );
- }
-
- /**
- * Remove topology records created by a scenario.
- *
- * @param FixtureData $fixtures Fixture helper.
- * @param array $topology Created topology data.
- */
- private function remove_shipping_topology( FixtureData $fixtures, array $topology ): void {
- if ( isset( $topology['default_instance_id'] ) ) {
- $default_zone = \WC_Shipping_Zones::get_zone( 0 );
- if ( $default_zone instanceof \WC_Shipping_Zone ) {
- $default_zone->delete_shipping_method( $topology['default_instance_id'] );
- }
- }
-
- if ( isset( $topology['gb_zone'] ) && $topology['gb_zone'] instanceof \WC_Shipping_Zone ) {
- $topology['gb_zone']->delete( true );
- }
-
- if ( ! empty( $topology['pickup_registered'] ) ) {
- $fixtures->shipping_remove_pickup_location();
- }
- }
-
- /**
- * Get an exact address fixture for a topology scenario.
- *
- * @param string $key Fixture key.
- * @return array<string, string>
- */
- private function get_shipping_address_fixture( string $key ): array {
- $addresses = array(
- 'blank' => array(
- 'first_name' => '',
- 'last_name' => '',
- 'company' => '',
- 'address_1' => '',
- 'address_2' => '',
- 'city' => '',
- 'state' => '',
- 'postcode' => '',
- 'country' => '',
- ),
- 'us' => array(
- 'first_name' => 'Pat',
- 'last_name' => 'Shopper',
- 'company' => '',
- 'address_1' => '60 29th Street',
- 'address_2' => '',
- 'city' => 'San Francisco',
- 'state' => 'CA',
- 'postcode' => '94110',
- 'country' => 'US',
- ),
- 'gb' => array(
- 'first_name' => 'Pat',
- 'last_name' => 'Shopper',
- 'company' => '',
- 'address_1' => '1 New Change',
- 'address_2' => '',
- 'city' => 'London',
- 'state' => '',
- 'postcode' => 'EC4M 9AF',
- 'country' => 'GB',
- ),
- );
-
- return $addresses[ $key ];
- }
-
- /**
- * Capture the current shipping address.
- *
- * @return array<string, string>
- */
- private function capture_shipping_address(): array {
- $address = array();
- foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country' ) as $field ) {
- $getter = 'get_shipping_' . $field;
- $address[ $field ] = WC()->customer->$getter();
- }
-
- return $address;
- }
-
- /**
- * Restore a shipping address.
- *
- * @param array<string, string> $address Shipping address.
- */
- private function restore_shipping_address( array $address ): void {
- foreach ( $address as $field => $value ) {
- $setter = 'set_shipping_' . $field;
- WC()->customer->$setter( $value );
- }
- }
-
- /**
- * Capture session values involved in shipping selection and caching.
- *
- * @return array<string, array{exists: bool, value: mixed}>
- */
- private function capture_shipping_session(): array {
- $session = array();
- foreach ( array( 'chosen_shipping_methods', 'shipping_method_counts', 'previous_shipping_methods', 'shipping_for_package_0', 'customer' ) as $key ) {
- $session[ $key ] = array(
- 'exists' => isset( WC()->session->{$key} ),
- 'value' => WC()->session->get( $key ),
- );
- }
-
- return $session;
- }
-
- /**
- * Clear session values involved in shipping selection and caching.
- */
- private function clear_shipping_session(): void {
- foreach ( array( 'chosen_shipping_methods', 'shipping_method_counts', 'previous_shipping_methods', 'shipping_for_package_0' ) as $key ) {
- WC()->session->set( $key, null );
- }
- WC()->shipping()->reset_shipping();
- }
-
- /**
- * Restore shipping session values.
- *
- * @param array<string, array{exists: bool, value: mixed}> $session Session values.
- */
- private function restore_shipping_session( array $session ): void {
- foreach ( $session as $key => $session_value ) {
- WC()->session->set( $key, $session_value['exists'] ? $session_value['value'] : null );
- }
- }
-
/**
* Test shipping address validation.
*/
@@ -1470,288 +1017,6 @@ class Cart extends ControllerTestCase {
);
}
- /**
- * @testdox Re-adding a standalone product preserves its cart-line key and increments its quantity.
- */
- public function test_add_item_preserves_standalone_cart_line_identity(): void {
- wc_empty_cart();
-
- $first_response = $this->dispatch_add_item_request(
- array(
- 'id' => $this->products[0]->get_id(),
- 'quantity' => 1,
- )
- );
- $first_data = $first_response->get_data();
-
- $this->assertSame( 201, $first_response->get_status(), 'The first standalone add should succeed.' );
- $this->assertCount( 1, $first_data['items'], 'The first add should create exactly one cart line.' );
- $cart_item_key = $first_data['items'][0]['key'];
- $this->assertSame(
- array(
- $cart_item_key => array(
- 'key' => $cart_item_key,
- 'product_id' => $this->products[0]->get_id(),
- 'variation_id' => 0,
- 'variation' => array(),
- 'quantity' => 1,
- ),
- ),
- $this->get_cart_line_identity_map(),
- 'The first add should create the expected standalone cart line.'
- );
-
- $second_response = $this->dispatch_add_item_request(
- array(
- 'id' => $this->products[0]->get_id(),
- 'quantity' => 1,
- )
- );
- $second_data = $second_response->get_data();
-
- $this->assertSame( 201, $second_response->get_status(), 'The standalone re-add should succeed.' );
- $this->assertCount( 1, $second_data['items'], 'The re-add should not create another cart line.' );
- $this->assertSame( $cart_item_key, $second_data['items'][0]['key'], 'The re-add should preserve the server cart-line key.' );
- $this->assertSame(
- array(
- $cart_item_key => array(
- 'key' => $cart_item_key,
- 'product_id' => $this->products[0]->get_id(),
- 'variation_id' => 0,
- 'variation' => array(),
- 'quantity' => 2,
- ),
- ),
- $this->get_cart_line_identity_map(),
- 'The re-add should increment only the existing standalone line.'
- );
- }
-
- /**
- * @testdox Cart item data keeps a same-product meta line distinct from the standalone line.
- */
- public function test_add_item_preserves_cart_item_data_identity(): void {
- wc_empty_cart();
-
- $add_cart_item_data = function ( $add_to_cart_data, $request ) {
- if ( 'meta-line' === $request->get_param( 'cart_line_identity_marker' ) ) {
- $add_to_cart_data['cart_item_data']['_cart_line_identity'] = 'meta-line';
- }
- return $add_to_cart_data;
- };
- add_filter( 'woocommerce_store_api_add_to_cart_data', $add_cart_item_data, 10, 2 );
-
- try {
- $meta_response = $this->dispatch_add_item_request(
- array(
- 'id' => $this->products[0]->get_id(),
- 'quantity' => 1,
- 'cart_line_identity_marker' => 'meta-line',
- )
- );
- $meta_map = $this->get_cart_line_identity_map();
- $meta_key = array_key_first( $meta_map );
-
- $this->assertSame( 201, $meta_response->get_status(), 'The marker-scoped meta-line add should succeed.' );
- $this->assertCount( 1, $meta_map, 'The first request should create exactly the meta line.' );
-
- $plain_response = $this->dispatch_add_item_request(
- array(
- 'id' => $this->products[0]->get_id(),
- 'quantity' => 1,
- )
- );
- $mixed_map = $this->get_cart_line_identity_map();
- $plain_keys = array_values( array_diff( array_keys( $mixed_map ), array( $meta_key ) ) );
-
- $this->assertSame( 201, $plain_response->get_status(), 'The first plain add should succeed.' );
- $this->assertCount( 1, $plain_keys, 'The plain add should create one distinct standalone key.' );
- $plain_key = $plain_keys[0];
- $expected = array(
- $meta_key => array(
- 'key' => $meta_key,
- 'product_id' => $this->products[0]->get_id(),
- 'variation_id' => 0,
- 'variation' => array(),
- 'quantity' => 1,
- ),
- $plain_key => array(
- 'key' => $plain_key,
- 'product_id' => $this->products[0]->get_id(),
- 'variation_id' => 0,
- 'variation' => array(),
- 'quantity' => 1,
- ),
- );
- ksort( $expected );
- $this->assertSame( $expected, $mixed_map, 'The meta and standalone lines should remain distinct at quantity one.' );
-
- $second_plain_response = $this->dispatch_add_item_request(
- array(
- 'id' => $this->products[0]->get_id(),
- 'quantity' => 1,
- )
- );
- $expected[ $plain_key ]['quantity'] = 2;
-
- $this->assertSame( 201, $second_plain_response->get_status(), 'The standalone re-add should succeed.' );
- $this->assertSame( $expected, $this->get_cart_line_identity_map(), 'Only the standalone line should increment.' );
- } finally {
- remove_filter( 'woocommerce_store_api_add_to_cart_data', $add_cart_item_data, 10 );
- }
- }
-
- /**
- * @testdox Re-adding one variation preserves its key while another variation creates a distinct line.
- */
- public function test_add_item_preserves_variation_identity(): void {
- wc_empty_cart();
-
- $fixtures = new FixtureData();
- $variable_product = $fixtures->get_variable_product(
- array(
- 'name' => 'Cart line identity variable product',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- ),
- array(
- $fixtures->get_product_attribute( 'color', array( 'red', 'blue' ) ),
- )
- );
- $red_variation = $fixtures->get_variation_product( $variable_product->get_id(), array( 'pa_color' => 'red' ) );
- $blue_variation = $fixtures->get_variation_product( $variable_product->get_id(), array( 'pa_color' => 'blue' ) );
-
- $red_body = array(
- 'id' => $red_variation->get_id(),
- 'quantity' => 1,
- 'variation' => array(
- array(
- 'attribute' => 'pa_color',
- 'value' => 'red',
- ),
- ),
- );
- $first_response = $this->dispatch_add_item_request( $red_body );
- $first_map = $this->get_cart_line_identity_map();
- $red_key = array_key_first( $first_map );
-
- $this->assertSame( 201, $first_response->get_status(), 'The first variation add should succeed.' );
- $this->assertCount( 1, $first_map, 'The first variation add should create exactly one line.' );
-
- $second_response = $this->dispatch_add_item_request( $red_body );
- $second_map = $this->get_cart_line_identity_map();
-
- $this->assertSame( 201, $second_response->get_status(), 'Re-adding the same variation should succeed.' );
- $this->assertSame( array( $red_key ), array_keys( $second_map ), 'The same variation should preserve its cart-line key.' );
- $this->assertSame( 2, $second_map[ $red_key ]['quantity'], 'The same variation should increment its line.' );
-
- $blue_response = $this->dispatch_add_item_request(
- array(
- 'id' => $blue_variation->get_id(),
- 'quantity' => 1,
- 'variation' => array(
- array(
- 'attribute' => 'pa_color',
- 'value' => 'blue',
- ),
- ),
- )
- );
- $final_map = $this->get_cart_line_identity_map();
- $blue_keys = array_values( array_diff( array_keys( $final_map ), array( $red_key ) ) );
-
- $this->assertSame( 201, $blue_response->get_status(), 'Adding a different variation should succeed.' );
- $this->assertCount( 1, $blue_keys, 'The different variation should create one distinct key.' );
- $blue_key = $blue_keys[0];
- $this->assertSame( $red_variation->get_id(), $final_map[ $red_key ]['variation_id'], 'The original key should still identify the red variation.' );
- $this->assertSame( 2, $final_map[ $red_key ]['quantity'], 'The red variation quantity should remain two.' );
- $this->assertSame( $blue_variation->get_id(), $final_map[ $blue_key ]['variation_id'], 'The new key should identify the blue variation.' );
- $this->assertSame( 1, $final_map[ $blue_key ]['quantity'], 'The blue variation should start at quantity one.' );
- }
-
- /**
- * @testdox A rejected sold-individually re-add leaves the exact cart-line map unchanged.
- */
- public function test_rejected_add_item_leaves_sold_individually_cart_unchanged(): void {
- wc_empty_cart();
-
- $product = $this->products[0];
- $original_sold_individually = $product->get_sold_individually();
- $product->set_sold_individually( true );
- $product->save();
-
- try {
- $first_response = $this->dispatch_add_item_request(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 1,
- )
- );
- $before = $this->get_cart_line_identity_map();
- $second_response = $this->dispatch_add_item_request(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 1,
- )
- );
- $second_data = $second_response->get_data();
-
- $this->assertSame( 201, $first_response->get_status(), 'The first sold-individually add should succeed.' );
- $this->assertSame( 400, $second_response->get_status(), 'The sold-individually re-add should be rejected.' );
- $this->assertSame( 'readonly_quantity', $second_data['code'], 'The rejection should report the Store API quantity-limit code.' );
- $this->assertSame( $before, $this->get_cart_line_identity_map(), 'The rejected re-add should leave every cart line unchanged.' );
- } finally {
- $product->set_sold_individually( $original_sold_individually );
- $product->save();
- }
- }
-
- /**
- * @testdox A rejected managed-stock re-add leaves the exact cart-line map unchanged.
- */
- public function test_rejected_add_item_leaves_managed_stock_cart_unchanged(): void {
- wc_empty_cart();
-
- $product = $this->products[1];
- $original_manage_stock = $product->get_manage_stock();
- $original_stock_quantity = $product->get_stock_quantity();
- $original_backorders = $product->get_backorders();
- $original_stock_status = $product->get_stock_status();
- $product->set_manage_stock( true );
- $product->set_stock_quantity( 1 );
- $product->set_backorders( 'no' );
- $product->set_stock_status( ProductStockStatus::IN_STOCK );
- $product->save();
-
- try {
- $first_response = $this->dispatch_add_item_request(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 1,
- )
- );
- $before = $this->get_cart_line_identity_map();
- $second_response = $this->dispatch_add_item_request(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 1,
- )
- );
- $second_data = $second_response->get_data();
-
- $this->assertSame( 201, $first_response->get_status(), 'The first managed-stock add should succeed.' );
- $this->assertSame( 400, $second_response->get_status(), 'The managed-stock re-add should be rejected.' );
- $this->assertSame( 'woocommerce_rest_product_partially_out_of_stock', $second_data['code'], 'The rejection should report the Store API partial-stock code.' );
- $this->assertSame( $before, $this->get_cart_line_identity_map(), 'The rejected re-add should leave every cart line unchanged.' );
- } finally {
- $product->set_manage_stock( $original_manage_stock );
- $product->set_stock_quantity( $original_stock_quantity );
- $product->set_backorders( $original_backorders );
- $product->set_stock_status( $original_stock_status );
- $product->save();
- }
- }
-
/**
* Test adding item to cart with quantity 0 shows an error.
*/
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
index c1ab38bad61..fec8c48448a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartApplyCoupon.php
@@ -217,7 +217,6 @@ class CartApplyCoupon extends ControllerTestCase {
*/
public function test_coupon_usage_limit() {
wc()->cart->remove_coupons();
- wp_set_current_user( 1 );
$fixtures = new FixtureData();
@@ -245,22 +244,10 @@ class CartApplyCoupon extends ControllerTestCase {
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
- wp_set_current_user( 0 );
$this->assertEquals( 400, $response->get_status() );
$this->assertEquals( 'woocommerce_rest_cart_coupon_error', $data['code'] );
- // Match the opening sentence rather than the whole message. WooCommerce has
- // three usage-limit wordings and picks between them on whether the current
- // user has a failed or pending order -- state this test does not own and any
- // earlier test in the process can create. All three start the same way, and
- // the coupon code is the part worth pinning.
- $this->assertStringStartsWith(
- sprintf(
- 'Usage limit for coupon "%s" has been reached.',
- $limited_coupon->get_code()
- ),
- html_entity_decode( $data['message'] )
- );
+ $this->assertStringContainsString( 'Usage limit', html_entity_decode( $data['message'] ) );
}
/**