Commit cafe00b2a15 for woocommerce

commit cafe00b2a15094b338692c1989972cdeacf19a78
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Tue Oct 13 14:43:03 2026 +0300

    [tests] Fix 14 order-dependent PHPUnit failures and 6 latent state leaks (#69117)

    Several PHPUnit tests passed or failed depending on which tests ran
    before them, because they changed process state that neither the
    per-test database rollback nor WP_UnitTestCase's hook restore resets.
    Each fix restores that state in the class that changes it.
    WC_Unit_Test_Case is unchanged; #69738 proposes moving the shared
    restores into it.

    Order-dependent failures fixed:

    - Block types: MiniCartFooterBlock puts the original footer block back,
      and the Add to Cart with Options mock blocks register through one
      helper, AddToCartWithOptionsMocks, which skips any already
      registered.
    - Shared caches: ProductFilterTaxonomyTest clears the taxonomy
      hierarchy map in setUp() and tearDown(); ProductsStore resets its
      statics before each test.
    - ShippingControllerTest moves to WC_Unit_Test_Case, resets
      WC_Shipping's method list and the dynamic WC()->shipping property,
      and reloads the shipping methods in setUp() so local pickup is
      registered once the checkout is a block.
    - Customer address: five classes put back the shipping address they
      change, and OrderControllerTests sets the US/CA location its
      invalid-address tests need for a flat rate.
    - REQUEST_URI: ProductPageIntegrationTest restores what go_to() sets,
      which made WC_Cart_Test think the shopper was on its product page.
    - OrderItemSchemaTest registers pa_size itself and undoes that
      registration and the $wc_product_attributes global afterwards.
    - LegacySelect2UsageTrackerTest accepts the checkout page type once
      WOOCOMMERCE_CHECKOUT is defined, and WC_Tests_Coupon sets it through
      Constants instead of define().

    Latent leaks closed: Totals and RateLimitsTests move to
    WC_Unit_Test_Case for its cart_context reset; BlockTypesController and
    Blocks\Checkout unregister their test block types; the V4 shipping zone
    method tests unload their custom method; the Select2 tests dequeue
    through WP_Dependencies; and the PHP_OS and WCPAY_VERSION_NUMBER
    overrides are cleared.

    No production code changes. The whole suite passes in default order
    (17097 tests), and each fix's pair fails on trunk and passes here.

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-test-order-shared-state-leaks b/plugins/woocommerce/changelog/fix-test-order-shared-state-leaks
new file mode 100644
index 00000000000..93d9a4aab3f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-test-order-shared-state-leaks
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Make PHPUnit tests restore shared state they change so they pass in any order; no production change.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
index e6fd2127c68..a668f27cf86 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
@@ -12,12 +12,22 @@
  */
 class WC_Tests_Coupon extends WC_Unit_Test_Case {

+	/**
+	 * Customer shipping address before the test, restored at teardown.
+	 *
+	 * @var array<string, string>
+	 */
+	private $previous_shipping_address = array();
+
 	/**
 	 * Sets up the test class.
 	 */
 	public function setUp(): void {
 		parent::setUp();

+		// WC()->customer outlives a test, so keep its location to restore at teardown.
+		$this->previous_shipping_address = WC()->customer->get_shipping( 'edit' );
+
 		// Set a valid address for the customer so shipping rates will calculate.
 		WC()->customer->set_shipping_country( 'US' );
 		WC()->customer->set_shipping_state( 'NY' );
@@ -28,7 +38,10 @@ class WC_Tests_Coupon extends WC_Unit_Test_Case {
 	 * Cleans up after the test class.
 	 */
 	public function tearDown(): void {
-		WC()->cart->remove_coupons();
+		\Automattic\Jetpack\Constants::clear_single_constant( 'WOOCOMMERCE_CHECKOUT' );
+		foreach ( $this->previous_shipping_address as $key => $value ) {
+			WC()->customer->{"set_shipping_{$key}"}( $value );
+		}

 		parent::tearDown();
 	}
@@ -301,10 +314,9 @@ class WC_Tests_Coupon extends WC_Unit_Test_Case {
 			)
 		);

-		// We need this to have the calculate_totals() method calculate totals.
-		if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
-			define( 'WOOCOMMERCE_CHECKOUT', true );
-		}
+		// We need this to have the calculate_totals() method calculate totals. Set it through
+		// Constants rather than define(), which would stay defined for every later test.
+		\Automattic\Jetpack\Constants::set_constant( 'WOOCOMMERCE_CHECKOUT', true );

 		// Add 2 products and coupon to cart.
 		WC()->cart->add_to_cart( $product->get_id(), 2 );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-default-shipping-method-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-default-shipping-method-test.php
index df411075f05..56d33cf9060 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-default-shipping-method-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-default-shipping-method-test.php
@@ -13,11 +13,11 @@ declare( strict_types = 1 );
 class WC_Cart_Default_Shipping_Method_Test extends WC_Unit_Test_Case {

 	/**
-	 * Shipping zone used across tests.
+	 * Customer shipping address before the test, restored at teardown.
 	 *
-	 * @var WC_Shipping_Zone
+	 * @var array<string, string>
 	 */
-	private $zone;
+	private $previous_shipping_address = array();

 	/**
 	 * Set up test fixtures.
@@ -26,10 +26,10 @@ class WC_Cart_Default_Shipping_Method_Test extends WC_Unit_Test_Case {
 		parent::setUp();

 		// Create a shipping zone with a flat rate so CartCheckoutUtils::shipping_methods_exist() returns true.
-		$this->zone = new WC_Shipping_Zone();
-		$this->zone->set_zone_name( 'Test Zone' );
-		$this->zone->save();
-		$this->zone->add_shipping_method( 'flat_rate' );
+		$zone = new WC_Shipping_Zone();
+		$zone->set_zone_name( 'Test Zone' );
+		$zone->save();
+		$zone->add_shipping_method( 'flat_rate' );

 		// Flush the shipping method count transient so the new zone is picked up.
 		WC_Cache_Helper::get_transient_version( 'shipping', true );
@@ -37,15 +37,18 @@ class WC_Cart_Default_Shipping_Method_Test extends WC_Unit_Test_Case {

 		// Set block checkout context (not shortcode).
 		WC()->cart->cart_context = 'store-api';
+
+		// Tests clear or set the shipping address on WC()->customer, which outlives a test.
+		$this->previous_shipping_address = WC()->customer->get_shipping( 'edit' );
 	}

 	/**
 	 * Tear down test fixtures.
 	 */
 	public function tearDown(): void {
-		$this->zone->delete( true );
-		update_option( 'woocommerce_shipping_cost_requires_address', 'no' );
-		WC()->cart->cart_context = 'shortcode';
+		foreach ( $this->previous_shipping_address as $key => $value ) {
+			WC()->customer->{"set_shipping_{$key}"}( $value );
+		}
 		parent::tearDown();
 	}

diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index 187f0a80009..cf62b8a466d 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -22,11 +22,11 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 	protected $add_to_cart_quantity_filter_args = array();

 	/**
-	 * Customer shipping address to restore after a test that changes it, keyed by prop name.
+	 * Customer shipping address before the test, restored at teardown.
 	 *
-	 * @var array<string, string>|null
+	 * @var array<string, string>
 	 */
-	private $original_shipping_address = null;
+	private $previous_shipping_address = array();

 	/**
 	 * Called before every test.
@@ -35,6 +35,9 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		parent::setUp();
 		$fixtures = new FixtureData();
 		$fixtures->shipping_add_flat_rate();
+
+		// Several tests change the shipping address on WC()->customer, which outlives a test.
+		$this->previous_shipping_address = WC()->customer->get_shipping( 'edit' );
 	}

 	/**
@@ -44,14 +47,13 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		parent::tearDown();

 		WC()->customer->set_is_vat_exempt( false );
+		foreach ( $this->previous_shipping_address as $key => $value ) {
+			WC()->customer->{"set_shipping_{$key}"}( $value );
+		}
 		WC()->session->set( 'wc_notices', null );

-		if ( null !== $this->original_shipping_address ) {
-			WC()->customer->set_props( $this->original_shipping_address );
-			$this->original_shipping_address = null;
-			// The checkout fields were built while the test's filters were attached.
-			$this->clear_checkout_fields();
-		}
+		// The checkout fields may have been built while a test's filters were attached.
+		$this->clear_checkout_fields();

 		// The parent teardown only clears chosen_shipping_methods, through
 		// WC_Shipping::reset_shipping(). Planted shipping_for_package_* rates survive and
@@ -841,6 +843,9 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 	 * Test show shipping.
 	 */
 	public function test_show_shipping() {
+		// Start from a customer without an address; WC()->customer outlives a test.
+		WC()->customer->set_shipping_location( '', '', '', '' );
+
 		// Test with an empty cart.
 		$this->assertFalse( WC()->cart->show_shipping() );

@@ -1382,13 +1387,7 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
 		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
 		$product = WC_Helper_Product::create_simple_product();
 		WC()->cart->add_to_cart( $product->get_id(), 1 );
-		$customer                        = WC()->cart->get_customer();
-		$this->original_shipping_address = array(
-			'shipping_country'  => $customer->get_shipping_country(),
-			'shipping_state'    => $customer->get_shipping_state(),
-			'shipping_city'     => $customer->get_shipping_city(),
-			'shipping_postcode' => $customer->get_shipping_postcode(),
-		);
+		$customer = WC()->cart->get_customer();
 		$customer->set_shipping_country( 'US' );
 		$customer->set_shipping_city( 'New York' );
 		$customer->set_shipping_state( 'state' === $missing_field ? '' : 'NY' );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
index 2c3727508b1..601266010c2 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
@@ -39,6 +39,7 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case {
 			$download->set_file( $file_path_with_period_at_end );
 			$this->assertEquals( false, $download->is_allowed_filetype() );
 		} finally {
+			\Automattic\Jetpack\Constants::clear_single_constant( 'PHP_OS' );
 			$filesystem->delete( $file_path_with_period_at_end );
 		}
 	}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZoneMethod/class-wc-rest-shipping-zone-method-v4-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZoneMethod/class-wc-rest-shipping-zone-method-v4-controller-tests.php
index 56dbbf48428..6be6c5e344f 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZoneMethod/class-wc-rest-shipping-zone-method-v4-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZoneMethod/class-wc-rest-shipping-zone-method-v4-controller-tests.php
@@ -82,6 +82,10 @@ class WC_REST_Shipping_Zone_Method_V4_Controller_Tests extends WC_REST_Unit_Test
 		}
 		$this->created_zones = array();

+		// A test loads a custom method into the shared WC_Shipping list. The hook restore drops
+		// its filter but not the loaded list, so reset the list to load again on next use.
+		WC()->shipping()->unregister_shipping_methods();
+
 		parent::tearDown();
 	}

diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
index 7b22bf2ec6c..adee11bb09a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
@@ -7,14 +7,7 @@ namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
 use Automattic\WooCommerce\Tests\Blocks\Utils\WC_Product_Custom;
 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsQuantitySelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsGroupedProductSelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsGroupedProductItemMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsGroupedProductItemSelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeNameMock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsMocks;
 use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils;
 use Automattic\WooCommerce\Internal\Features\FeaturesController;

@@ -23,33 +16,13 @@ use Automattic\WooCommerce\Internal\Features\FeaturesController;
  */
 class AddToCartWithOptions extends \WP_UnitTestCase {

-	/**
-	 * Tracks whether blocks have been registered.
-	 *
-	 * @var bool
-	 */
-	protected static $are_blocks_registered = false;
-
 	/**
 	 * Initiate the mock object.
 	 */
 	protected function setUp(): void {
 		parent::setUp();

-		if ( ! self::$are_blocks_registered ) {
-			// We need to register the blocks after set up. They are no registered
-			// on `init` because `init` is called with a classic theme.
-			new AddToCartWithOptionsMock();
-			new AddToCartWithOptionsQuantitySelectorMock();
-			new AddToCartWithOptionsGroupedProductSelectorMock();
-			new AddToCartWithOptionsGroupedProductItemMock();
-			new AddToCartWithOptionsGroupedProductItemSelectorMock();
-			new AddToCartWithOptionsVariationSelectorMock();
-			new AddToCartWithOptionsVariationSelectorAttributeMock();
-			new AddToCartWithOptionsVariationSelectorAttributeNameMock();
-
-			self::$are_blocks_registered = true;
-		}
+		AddToCartWithOptionsMocks::register_all();
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
index e2c1b7ce53e..1a48218a05b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
@@ -64,6 +64,11 @@ class Checkout extends \WP_UnitTestCase {
 	protected function tearDown(): void {
 		parent::tearDown();
 		remove_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) );
+
+		// A test registers a CheckoutMock block type, and the block type registry outlives a test.
+		if ( \WP_Block_Type_Registry::get_instance()->is_registered( 'woocommerce/checkout-mock' ) ) {
+			\WP_Block_Type_Registry::get_instance()->unregister( 'woocommerce/checkout-mock' );
+		}
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCartFooterBlock.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCartFooterBlock.php
index 78bb1208e04..8fb7cf44b8f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCartFooterBlock.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCartFooterBlock.php
@@ -22,6 +22,13 @@ class MiniCartFooterBlock extends \WP_UnitTestCase {
 	 */
 	protected $block;

+	/**
+	 * The original block type registry entry for the block.
+	 *
+	 * @var \WP_Block_Type|null
+	 */
+	private $original_block_type;
+
 	/**
 	 * Setup test.
 	 */
@@ -29,7 +36,9 @@ class MiniCartFooterBlock extends \WP_UnitTestCase {
 		parent::setUp();

 		$registry = \WP_Block_Type_Registry::get_instance();
-		if ( $registry->is_registered( 'woocommerce/mini-cart-footer-block' ) ) {
+
+		$this->original_block_type = $registry->get_registered( 'woocommerce/mini-cart-footer-block' );
+		if ( $this->original_block_type ) {
 			$registry->unregister( 'woocommerce/mini-cart-footer-block' );
 		}

@@ -48,6 +57,9 @@ class MiniCartFooterBlock extends \WP_UnitTestCase {
 		if ( $registry->is_registered( 'woocommerce/mini-cart-footer-block' ) ) {
 			$registry->unregister( 'woocommerce/mini-cart-footer-block' );
 		}
+		if ( $this->original_block_type ) {
+			$registry->register( $this->original_block_type );
+		}
 		unset( $this->block );
 		parent::tearDown();
 	}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/Totals.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
index e1b98efe153..4ce2eab5674 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
@@ -19,11 +19,11 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
  *
  * @since $VID:$
  */
-class Totals extends \WP_UnitTestCase {
+class Totals extends \WC_Unit_Test_Case {
 	/**
 	 * Setup test product data. Called before every test.
 	 */
-	protected function setUp(): void {
+	public function setUp(): void {
 		parent::setUp();

 		add_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ), 10, 4 );
@@ -102,10 +102,12 @@ class Totals extends \WP_UnitTestCase {
 		global $wp_rest_server;
 		$wp_rest_server = null;

-		parent::tearDown();
-		remove_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ) );
-		WC()->cart->empty_cart();
-		WC()->session->destroy_session();
+		try {
+			// The session handler deletes its row through $wpdb, so this has to run before the rollback.
+			WC()->session->destroy_session();
+		} finally {
+			parent::tearDown();
+		}
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterTaxonomyTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterTaxonomyTest.php
index d989be19369..6615916479e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterTaxonomyTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterTaxonomyTest.php
@@ -3,6 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;

+use Automattic\WooCommerce\Internal\ProductFilters\TaxonomyHierarchyData;
 use WC_Unit_Test_Case;

 /**
@@ -30,6 +31,11 @@ class ProductFilterTaxonomyTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

+		// The container keeps the product_cat hierarchy map across tests, and nothing here clears it when
+		// terms are created or deleted. Clear it before and after, so this class neither reads a stale
+		// map nor leaves its own behind.
+		wc_get_container()->get( TaxonomyHierarchyData::class )->clear_cache( 'product_cat' );
+
 		add_filter( 'woocommerce_pre_product_filter_data', array( $this, 'filter_product_filter_data' ), 10, 4 );
 	}

@@ -43,6 +49,8 @@ class ProductFilterTaxonomyTest extends WC_Unit_Test_Case {
 			wp_delete_term( $term_id, 'product_cat' );
 		}

+		wc_get_container()->get( TaxonomyHierarchyData::class )->clear_cache( 'product_cat' );
+
 		parent::tearDown();
 	}

diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
index d540e79d158..e1637b4e3e5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
@@ -6,8 +6,7 @@ namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;

 use Automattic\WooCommerce\Enums\ProductStockStatus;
 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeNameMock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsMocks;
 use WC_Unit_Test_Case;

 /**
@@ -15,25 +14,16 @@ use WC_Unit_Test_Case;
  */
 class VariationSelectorAttribute extends WC_Unit_Test_Case {

-	/**
-	 * Tracks whether blocks have been registered.
-	 *
-	 * @var bool
-	 */
-	protected static $are_blocks_registered = false;
-
 	/**
 	 * Register blocks required for do_blocks tests.
 	 */
 	public function setUp(): void {
 		parent::setUp();

-		if ( ! self::$are_blocks_registered && ! \WP_Block_Type_Registry::get_instance()->is_registered( 'woocommerce/add-to-cart-with-options-variation-selector-attribute' ) ) {
-			new AddToCartWithOptionsVariationSelectorAttributeMock();
-			new AddToCartWithOptionsVariationSelectorAttributeNameMock();
-		}
-
-		self::$are_blocks_registered = true;
+		AddToCartWithOptionsMocks::register(
+			'add-to-cart-with-options-variation-selector-attribute',
+			'add-to-cart-with-options-variation-selector-attribute-name'
+		);
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
index ec728729d3b..b54d3077213 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
@@ -36,6 +36,23 @@ class BlockTypesController extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Remove the block types test_block_should_have_data_attributes() registers, since the
+	 * block type registry outlives a test.
+	 */
+	public function tearDown(): void {
+		try {
+			$registry = \WP_Block_Type_Registry::get_instance();
+			foreach ( array( 'unrelated-namespace/unrelated-block-name', 'namespace/allowed-block-name', 'allowed-namespace/block-name', 'child-of-woo/block-name' ) as $block_name ) {
+				if ( $registry->is_registered( $block_name ) ) {
+					$registry->unregister( $block_name );
+				}
+			}
+		} finally {
+			parent::tearDown();
+		}
+	}
+
 	/**
 	 * @testdox Should identify blocks that should have data attributes.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsMocks.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsMocks.php
new file mode 100644
index 00000000000..84db3c741ff
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Mocks/AddToCartWithOptionsMocks.php
@@ -0,0 +1,46 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Mocks;
+
+/**
+ * Registers the Add to Cart with Options mock blocks, which `init` skips under a classic theme.
+ */
+class AddToCartWithOptionsMocks {
+
+	/**
+	 * Mock class for each block name, without the `woocommerce/` namespace.
+	 */
+	private const MOCKS = array(
+		'add-to-cart-with-options'                      => AddToCartWithOptionsMock::class,
+		'add-to-cart-with-options-quantity-selector'    => AddToCartWithOptionsQuantitySelectorMock::class,
+		'add-to-cart-with-options-grouped-product-selector' => AddToCartWithOptionsGroupedProductSelectorMock::class,
+		'add-to-cart-with-options-grouped-product-item' => AddToCartWithOptionsGroupedProductItemMock::class,
+		'add-to-cart-with-options-grouped-product-item-selector' => AddToCartWithOptionsGroupedProductItemSelectorMock::class,
+		'add-to-cart-with-options-variation-selector'   => AddToCartWithOptionsVariationSelectorMock::class,
+		'add-to-cart-with-options-variation-selector-attribute' => AddToCartWithOptionsVariationSelectorAttributeMock::class,
+		'add-to-cart-with-options-variation-selector-attribute-name' => AddToCartWithOptionsVariationSelectorAttributeNameMock::class,
+	);
+
+	/**
+	 * Register the given mock blocks, skipping any already registered, since the registry outlives a test.
+	 *
+	 * @param string ...$block_names Block names without the `woocommerce/` namespace.
+	 */
+	public static function register( string ...$block_names ): void {
+		$registry = \WP_Block_Type_Registry::get_instance();
+		foreach ( $block_names as $block_name ) {
+			if ( ! $registry->is_registered( 'woocommerce/' . $block_name ) ) {
+				$mock_class = self::MOCKS[ $block_name ];
+				new $mock_class();
+			}
+		}
+	}
+
+	/**
+	 * Register every mock block that is not registered yet.
+	 */
+	public static function register_all(): void {
+		self::register( ...array_keys( self::MOCKS ) );
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php b/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
index 7d2d2e9064f..6bb3d0ecf36 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/SharedStores/ProductsStore.php
@@ -35,6 +35,14 @@ class ProductsStore extends \WC_Unit_Test_Case {
 	 */
 	protected $original_hydration_registry_entry = null;

+	/**
+	 * Start from empty ProductsStore statics, since block renders in other test classes fill them.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->reset_products_store_static_state();
+	}
+
 	/**
 	 * Reset static state on the ProductsStore and the global
 	 * WP_Interactivity_API instance between tests so state does not bleed.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
index 82f34cfad4b..294fade93be 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
@@ -18,21 +18,6 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
 	 */
 	private ShippingController $shipping_controller;

-	/**
-	 * The old checkout page ID.
-	 *
-	 * @var int $original_checkout_page_id
-	 */
-	private $original_checkout_page_id;
-
-	/**
-	 * The new checkout page ID.
-	 *
-	 * @var int $block_checkout_page_id
-	 */
-	private $block_checkout_page_id;
-
-
 	/**
 	 * Mock logger instance.
 	 *
@@ -47,6 +32,13 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
 	 */
 	private $backup_wc;

+	/**
+	 * Customer shipping address before the test, restored at teardown.
+	 *
+	 * @var array<string, string>
+	 */
+	private $previous_shipping_address = array();
+
 	/**
 	 * Initialize the registry instance.
 	 *
@@ -66,8 +58,7 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
 		$this->backup_wc = WC();

 		// Local pickup only works with the checkout block.
-		$this->original_checkout_page_id = get_option( 'woocommerce_checkout_page_id' );
-		$this->block_checkout_page_id    = $this->factory->post->create(
+		$block_checkout_page_id = $this->factory->post->create(
 			array(
 				'post_type'    => 'page',
 				'post_title'   => 'Checkout',
@@ -75,12 +66,17 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
 				'post_status'  => 'publish',
 			)
 		);
-		update_option( 'woocommerce_checkout_page_id', $this->block_checkout_page_id );
+		update_option( 'woocommerce_checkout_page_id', $block_checkout_page_id );
+		// An earlier test class may have loaded the shipping methods while the checkout was a
+		// shortcode page, which skips local pickup. Unload them so the next read registers it.
+		WC()->shipping()->unregister_shipping_methods();

 		$this->shipping_controller = new ShippingController(
 			Package::container()->get( Api::class ),
 			Package::container()->get( AssetDataRegistry::class )
 		);
+		// WC()->customer outlives the test, so keep its location to restore at teardown.
+		$this->previous_shipping_address = WC()->customer->get_shipping( 'edit' );
 		WC()->customer->set_shipping_postcode( '' );
 		WC()->customer->set_shipping_city( '' );
 		WC()->customer->set_shipping_state( '' );
@@ -95,11 +91,18 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
 	public function tearDown(): void {
 		global $woocommerce;

-		update_option( 'woocommerce_checkout_page_id', $this->original_checkout_page_id );
-		wp_delete_post( $this->block_checkout_page_id );
-		remove_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) );
-		$woocommerce = $this->backup_wc;
-		parent::tearDown();
+		try {
+			$woocommerce = $this->backup_wc;
+			foreach ( $this->previous_shipping_address as $key => $value ) {
+				WC()->customer->{"set_shipping_{$key}"}( $value );
+			}
+			// A test may null WC()->shipping, which leaves a dynamic property on the WC singleton,
+			// and register a lone pickup method on the shared WC_Shipping. Undo both.
+			unset( WC()->shipping );
+			WC()->shipping()->unregister_shipping_methods();
+		} finally {
+			parent::tearDown();
+		}
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/RateLimitsTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/RateLimitsTests.php
index d901334bfbf..84ddc78887a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/RateLimitsTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/RateLimitsTests.php
@@ -11,16 +11,15 @@ use ReflectionClass;
 use ReflectionException;
 use Spy_REST_Server;
 use WP_REST_Server;
-use WP_Test_REST_TestCase;

 /**
  * ControllerTests
  */
-class RateLimitsTests extends WP_Test_REST_TestCase {
+class RateLimitsTests extends \WC_Unit_Test_Case {
 	/**
 	 * Setup Rest API server.
 	 */
-	protected function setUp(): void {
+	public function setUp(): void {
 		parent::setUp();

 		/** @var WP_REST_Server $wp_rest_server */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
index 7e39e47d4e5..7db478eca8e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
@@ -37,6 +37,20 @@ class OrderItemSchemaTest extends \WC_Unit_Test_Case {
 	 */
 	private $schema_controller;

+	/**
+	 * Whether the pa_size taxonomy was registered when the test started.
+	 *
+	 * @var bool
+	 */
+	private $size_taxonomy_existed_at_setup;
+
+	/**
+	 * The $wc_product_attributes global when the test started.
+	 *
+	 * @var array<string, object>
+	 */
+	private $product_attributes_at_setup;
+
 	/**
 	 * Set up before test.
 	 *
@@ -45,6 +59,10 @@ class OrderItemSchemaTest extends \WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

+		// create_attribute() registers a taxonomy and rebuilds this global, and the rollback undoes neither.
+		$this->size_taxonomy_existed_at_setup = taxonomy_exists( 'pa_size' );
+		$this->product_attributes_at_setup    = $GLOBALS['wc_product_attributes'] ?? array();
+
 		// Set up formatters and extend schema.
 		$formatters = new Formatters();
 		$formatters->register( 'money', MoneyFormatter::class );
@@ -63,7 +81,14 @@ class OrderItemSchemaTest extends \WC_Unit_Test_Case {
 		$this->schema_controller = null;
 		$this->mock_extend       = null;

-		parent::tearDown();
+		try {
+			if ( ! $this->size_taxonomy_existed_at_setup && taxonomy_exists( 'pa_size' ) ) {
+				unregister_taxonomy( 'pa_size' );
+			}
+			$GLOBALS['wc_product_attributes'] = $this->product_attributes_at_setup; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restores the value from setUp().
+		} finally {
+			parent::tearDown();
+		}
 	}

 	/**
@@ -245,6 +270,9 @@ class OrderItemSchemaTest extends \WC_Unit_Test_Case {
 	 * Test that variations with "Any" attributes return user-selected values.
 	 */
 	public function test_get_item_response_any_variation_returns_user_selected_values(): void {
+		// Register the pa_size taxonomy so the attribute label resolves the same way in any test order.
+		\WC_Helper_Product::create_attribute( 'size', array() );
+
 		$variable_product = new \WC_Product_Variable();
 		$variable_product->set_name( 'Test Variable Product' );

@@ -305,6 +333,9 @@ class OrderItemSchemaTest extends \WC_Unit_Test_Case {
 	 * Test variations with mixed global and custom "Any" attributes.
 	 */
 	public function test_get_item_response_any_variation_with_mixed_attributes(): void {
+		// Register the pa_size taxonomy so the attribute label resolves the same way in any test order.
+		\WC_Helper_Product::create_attribute( 'size', array() );
+
 		$variable_product = new \WC_Product_Variable();
 		$variable_product->set_name( 'Test Variable Product with Mixed Attributes' );

diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
index 2d50ec9cbec..2a6dbb18d9e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
@@ -21,6 +21,13 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
 	 */
 	private $sut;

+	/**
+	 * Customer shipping address before the test, restored at teardown.
+	 *
+	 * @var array<string, string>
+	 */
+	private $previous_shipping_address = array();
+
 	/**
 	 * Set up before test.
 	 *
@@ -35,6 +42,12 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
 		// The per-test database rollback restores the option.
 		update_option( 'woocommerce_checkout_phone_field', 'optional' );

+		// The invalid-address tests need a shipping rate, and the flat rate only offers one for a
+		// country the store ships to. The cart ships to WC()->customer, which outlives a test, so
+		// give it a US/CA location here rather than depend on the address it arrives with.
+		$this->previous_shipping_address = WC()->customer->get_shipping( 'edit' );
+		WC()->customer->set_shipping_location( 'US', 'CA' );
+
 		$this->sut = new class() extends OrderController {
 			/**
 			 * Check all required address fields are set and return errors if not. Parent is protected.
@@ -49,6 +62,19 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
 		};
 	}

+	/**
+	 * Put back the customer shipping location that setUp() replaced.
+	 */
+	public function tearDown(): void {
+		try {
+			foreach ( $this->previous_shipping_address as $key => $value ) {
+				WC()->customer->{"set_shipping_{$key}"}( $value );
+			}
+		} finally {
+			parent::tearDown();
+		}
+	}
+
 	/**
 	 * test_validate_existing_order_before_payment_valid_data.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsServiceTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsServiceTest.php
index aa173124af1..16bfb82a5bf 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsServiceTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsServiceTest.php
@@ -206,6 +206,17 @@ class WooPaymentsServiceTest extends WC_Unit_Test_Case {
 		$this->sut->init( $this->mock_providers, $this->mockable_proxy );
 	}

+	/**
+	 * Clear the version constant setUp() sets, which would otherwise outlive the class.
+	 */
+	public function tearDown(): void {
+		try {
+			Constants::clear_single_constant( 'WCPAY_VERSION_NUMBER' );
+		} finally {
+			parent::tearDown();
+		}
+	}
+
 	/**
 	 * Test get onboarding details when the extension is NOT active.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php
index 882776c9f18..eac41c7e731 100644
--- a/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php
@@ -406,7 +406,8 @@ class LegacySelect2UsageTrackerTest extends WC_Unit_Test_Case {
 	private function reset_scripts(): void {
 		$wp_scripts = wp_scripts();

-		$wp_scripts->queue     = array();
+		// dequeue() also clears the cached dependency list that wp_script_is( ..., 'enqueued' ) reads.
+		$wp_scripts->dequeue( $wp_scripts->queue );
 		$wp_scripts->to_do     = array();
 		$wp_scripts->done      = array();
 		$wp_scripts->groups    = array();
@@ -448,14 +449,19 @@ class LegacySelect2UsageTrackerTest extends WC_Unit_Test_Case {
 	/**
 	 * Get the frontend page type expected for the current PHPUnit process.
 	 *
-	 * Some tests render cart flows that define WOOCOMMERCE_CART. Since PHP
-	 * constants cannot be undefined, this test must accept the inherited cart
-	 * page type when it runs later in the same process.
+	 * Some tests render cart or checkout flows that define WOOCOMMERCE_CART or
+	 * WOOCOMMERCE_CHECKOUT. Since PHP constants cannot be undefined, this test must
+	 * accept the inherited page type when it runs later in the same process. The
+	 * checks follow the tracker's own order: cart before checkout.
 	 *
 	 * @return string
 	 */
 	private function get_expected_frontend_page_type(): string {
-		return Constants::is_defined( 'WOOCOMMERCE_CART' ) ? 'cart' : 'other';
+		if ( Constants::is_defined( 'WOOCOMMERCE_CART' ) ) {
+			return 'cart';
+		}
+
+		return Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) ? 'checkout' : 'other';
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/ProductPageIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/ProductPageIntegrationTest.php
index e4fa22ce660..6983aa79eba 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/ProductPageIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/ProductPageIntegrationTest.php
@@ -6,11 +6,7 @@ namespace Automattic\WooCommerce\Tests\Internal\StockNotifications\Frontend;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
 use Automattic\WooCommerce\Internal\StockNotifications\Frontend\ProductPageIntegration;
 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsQuantitySelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeMock;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsVariationSelectorAttributeNameMock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AddToCartWithOptionsMocks;
 use Automattic\WooCommerce\Tests\Internal\StockNotifications\StockNotificationsFeatureTrait;
 use WC_Unit_Test_Case;

@@ -28,21 +24,29 @@ class ProductPageIntegrationTest extends WC_Unit_Test_Case {
 	 */
 	private $sut;

+	/**
+	 * REQUEST_URI before the test, restored at teardown.
+	 *
+	 * @var string|null
+	 */
+	private $request_uri_at_setup;
+
 	/**
 	 * Set up test fixtures.
 	 */
 	public function setUp(): void {
 		parent::setUp();

-		// The blocks are not registered on `init` because `init` runs with a classic theme.
-		// Other test classes may have registered them already.
-		if ( ! \WP_Block_Type_Registry::get_instance()->is_registered( 'woocommerce/add-to-cart-with-options' ) ) {
-			new AddToCartWithOptionsMock();
-			new AddToCartWithOptionsQuantitySelectorMock();
-			new AddToCartWithOptionsVariationSelectorMock();
-			new AddToCartWithOptionsVariationSelectorAttributeMock();
-			new AddToCartWithOptionsVariationSelectorAttributeNameMock();
-		}
+		// go_to() sets REQUEST_URI and no base test case puts it back.
+		$this->request_uri_at_setup = $_SERVER['REQUEST_URI'] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
+
+		AddToCartWithOptionsMocks::register(
+			'add-to-cart-with-options',
+			'add-to-cart-with-options-quantity-selector',
+			'add-to-cart-with-options-variation-selector',
+			'add-to-cart-with-options-variation-selector-attribute',
+			'add-to-cart-with-options-variation-selector-attribute-name'
+		);

 		update_option( 'woocommerce_customer_stock_notifications_allow_signups', 'yes' );

@@ -59,6 +63,11 @@ class ProductPageIntegrationTest extends WC_Unit_Test_Case {
 	 */
 	public function tearDown(): void {
 		try {
+			if ( null === $this->request_uri_at_setup ) {
+				unset( $_SERVER['REQUEST_URI'] );
+			} else {
+				$_SERVER['REQUEST_URI'] = $this->request_uri_at_setup;
+			}
 			$this->restore_stock_notifications_feature_option();
 		} finally {
 			parent::tearDown();