Commit d58d61ce5b9 for woocommerce

commit d58d61ce5b97217f915cfafcfb6e74dd3b6b317b
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Tue Oct 20 13:20:38 2026 +0200

    Gate Blocks e2e test plugins by cookie and move 4 specs to parallel (#69419)

    * Wait for the add-to-cart response in Blocks e2e addToCart

    * Gate Blocks e2e test plugins by cookie and move specs to parallel

    * Add changelog entry for gating Blocks e2e test plugins

    * Run the parent-item-key Blocks e2e spec in parallel

diff --git a/plugins/woocommerce/changelog/dev-testops-231-blocks-e2e-m2b-gate-test-plugins b/plugins/woocommerce/changelog/dev-testops-231-blocks-e2e-m2b-gate-test-plugins
new file mode 100644
index 00000000000..f331978e5d2
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-testops-231-blocks-e2e-m2b-gate-test-plugins
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Gate Blocks e2e test plugins by cookie and move more specs to the parallel project; no production change.
diff --git a/plugins/woocommerce/tests/e2e/README.md b/plugins/woocommerce/tests/e2e/README.md
index 511a80ea0dd..c2c4f849388 100644
--- a/plugins/woocommerce/tests/e2e/README.md
+++ b/plugins/woocommerce/tests/e2e/README.md
@@ -234,7 +234,9 @@ How a helper is wired up depends on when it needs to be active:
         - **Test helper REST API** — endpoints (`e2e-feature-flags`, `e2e-options`, `e2e-environment`, `e2e-theme`) for toggling feature flags, setting/deleting options, reading environment info and switching themes during a test.
         - **Timing overrides** — fixed filters removing production delays and throttles that only slow tests down or make them flaky: WordPress' comment flood protection, and the 1-minute wait before the first Back in Stock Notifications batch. Unconditional rather than cookie-driven, because they must also apply to REST requests made outside the browser.
     - `wc-email-template-sync-test-helper` — see below (email template sync fixtures for RSM-146).
-- **Per-test block plugins** live in `tests/e2e/test-plugins/blocks/`, mounted (not auto-activated) via the `woocommerce-blocks-test-plugins` mapping. Each is activated and deactivated by the spec that needs it (e.g. `wp plugin activate woocommerce-blocks-test-plugins/<file>.php`), because they change store behavior globally and must not be on for every test.
+- **Per-test block plugins** live in `tests/e2e/test-plugins/blocks/`, mounted (not auto-activated) via the `woocommerce-blocks-test-plugins` mapping. They change store behavior, so they must not act for every test.
+    - The plugins in `COOKIE_GATED_TEST_PLUGINS` (`utils/blocks/test-plugins.ts`) are activated once by `blocks setup`. Each one acts only on requests that send a cookie named after its slug. A spec turns one on for its own browser context with `enableTestPlugin( context, '<plugin>' )`, so the spec can run in `blocks-parallel`.
+    - The other plugins are activated and deactivated by the spec that needs them (e.g. `wp plugin activate woocommerce-blocks-test-plugins/<file>.php`). Specs that do this are serial, because the plugin acts for all workers and `activatePlugin` races other specs on the `active_plugins` option.

 `woocommerce-cleanup` also lives under `test-plugins/`, but it is **not** in the wp-env `plugins` array — it's an on-demand site-reset tool installed only by the external (non-wp-env) setup path, `bin/test-env-setup-external.sh`.

diff --git a/plugins/woocommerce/tests/e2e/fixtures/blocks-setup.ts b/plugins/woocommerce/tests/e2e/fixtures/blocks-setup.ts
index c21bb5145a2..e69cabe2097 100644
--- a/plugins/woocommerce/tests/e2e/fixtures/blocks-setup.ts
+++ b/plugins/woocommerce/tests/e2e/fixtures/blocks-setup.ts
@@ -11,6 +11,7 @@ import {
 	customerFile,
 	BLOCK_THEME_SLUG,
 	DB_EXPORT_FILE,
+	COOKIE_GATED_TEST_PLUGINS,
 	// eslint-disable-next-line import/no-unresolved -- resolved via the @woocommerce/e2e-utils tsconfig alias.
 } from '@woocommerce/e2e-utils';

@@ -106,6 +107,17 @@ setup( 'blocks setup', async () => {
 		await prepareAttributes();
 	}

+	console.log( '├ Activating test plugins…' );
+	// `cart-line-identity` acts only on add-to-cart requests with its flags.
+	const testPlugins = [ ...COOKIE_GATED_TEST_PLUGINS, 'cart-line-identity' ];
+	await wpCLI(
+		`plugin activate ${ testPlugins
+			.map(
+				( plugin ) => `woocommerce-blocks-test-plugins/${ plugin }.php`
+			)
+			.join( ' ' ) }`
+	);
+
 	console.log( '├ Exporting database snapshot…' );
 	await wpCLI( `db export ${ DB_EXPORT_FILE }` );

diff --git a/plugins/woocommerce/tests/e2e/playwright.config.ts b/plugins/woocommerce/tests/e2e/playwright.config.ts
index 5576776a108..c63707abccc 100644
--- a/plugins/woocommerce/tests/e2e/playwright.config.ts
+++ b/plugins/woocommerce/tests/e2e/playwright.config.ts
@@ -216,6 +216,7 @@ const blocksSerialSpecs = [
 	'**/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts',
 	'**/tests/blocks/classic-template/classic-template.block_theme.spec.ts',
 	'**/tests/blocks/mini-cart/mini-cart-block.shopper.block_theme.spec.ts',
+	'**/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts',
 	'**/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts',
 	'**/tests/blocks/page-content-wrapper/page-content-wrapper.block_theme.spec.ts',
 	'**/tests/blocks/price-filter/price-filter.block_theme.spec.ts',
@@ -228,26 +229,18 @@ const blocksSerialSpecs = [
 	'**/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts',
 	'**/tests/blocks/products/products.block_theme.spec.ts',
 	'**/tests/blocks/rating-filter/rating-filter.block_theme.spec.ts',
+	'**/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts',
 	'**/tests/blocks/stock-filter/stock-filter.block_theme.spec.ts',
 	'**/tests/blocks/templates/legacy-templates.block_theme.spec.ts',

 	// Activate test plugins that change every cart, checkout or product page, or
 	// race other specs on the `active_plugins` option.
-	'**/tests/blocks/cart/cart-block.shopper.block_theme.spec.ts',
 	'**/tests/blocks/cart/cart-checkout-block-extension-callbacks.shopper.block_theme.spec.ts',
-	'**/tests/blocks/cart/cart-store.block_theme.spec.ts',
-	'**/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts',
-	'**/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts',
-	'**/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts',
 	'**/tests/blocks/checkout/checkout-block-custom-place-order-button.block_theme.spec.ts',
 	'**/tests/blocks/checkout/checkout-block-extensibility.shopper.block_theme.spec.ts',
-	'**/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts',
-	'**/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts',
 	'**/tests/blocks/product-button/product-button.block_theme.spec.ts',
-	'**/tests/blocks/product-collection/compatibility-layer.block_theme.spec.ts',
 	'**/tests/blocks/product-collection/product-picker.block_theme.spec.ts',
 	'**/tests/blocks/product-collection/register-product-collection.block_theme.spec.ts',
-	'**/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts',

 	// Change global settings that cart, checkout and storefront specs read:
 	// shipping, taxes, local pickup, account creation, site language, the shop
@@ -262,12 +255,15 @@ const blocksSerialSpecs = [
 	'**/tests/blocks/single-product-template/single-product-template.product-rating.spec.ts',
 	'**/tests/blocks/templates/shop-page.block_theme.spec.ts',

-	// Add to the cart as the admin user, whose cart all workers share, and
-	// place orders, which change the Best Sellers ranking.
+	// Place orders, which change the Best Sellers ranking. Some also add to the
+	// cart as the admin user, whose cart all workers share.
 	'**/tests/blocks/cart/cart-checkout-block-coupons.shopper.block_theme.spec.ts',
+	'**/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts',
+	'**/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts',
+	'**/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts',
+	'**/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts',

 	// Create or edit products and categories that catalog specs count.
-	'**/tests/blocks/cart-store/parent-item-key.block_theme.spec.ts',
 	'**/tests/blocks/product-collection/product-collection-errors.block_theme.spec.ts',
 ];

diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
index ae2bfcf214d..a42d237c004 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/additional-checkout-fields.php
@@ -7,6 +7,12 @@ declare(strict_types=1);
  * Author: WooCommerce
  * @package woocommerce-blocks-test-additional-checkout-fields
  */
+
+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-additional-checkout-fields'] ) ) {
+	return;
+}
+
 class Additional_Checkout_Fields_Test_Helper {
 	/**
 	 * Constructor.
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-hidden.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-hidden.php
index d1e00292a5c..4a2061688b6 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-hidden.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-hidden.php
@@ -10,6 +10,11 @@

 declare(strict_types=1);

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-item-data-display-hidden'] ) ) {
+	return;
+}
+
 final class Item_Data_Display_Hidden_Test_Fixture {
 	/**
 	 * Register fixture hooks.
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-malformed.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-malformed.php
index cb80ebfa9eb..8939373631d 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-malformed.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-malformed.php
@@ -10,6 +10,11 @@

 declare(strict_types=1);

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-item-data-display-malformed'] ) ) {
+	return;
+}
+
 final class Item_Data_Display_Malformed_Test_Fixture {
 	/**
 	 * Register fixture hooks.
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-mixed.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-mixed.php
index 6cf540eb8e5..4745a4ffcfd 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-mixed.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-mixed.php
@@ -10,6 +10,11 @@

 declare(strict_types=1);

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-item-data-display-mixed'] ) ) {
+	return;
+}
+
 final class Item_Data_Display_Mixed_Test_Fixture {
 	/**
 	 * Register fixture hooks.
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display.php
index 7675257cd50..2d17e7ed65a 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display.php
@@ -10,6 +10,11 @@

 declare(strict_types=1);

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-item-data-display'] ) ) {
+	return;
+}
+
 add_action(
 	'woocommerce_init',
 	function () {
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/locale-hide-country.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/locale-hide-country.php
index 123ca88e135..af0c4c659f7 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/locale-hide-country.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/locale-hide-country.php
@@ -10,6 +10,11 @@

 declare(strict_types=1);

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-locale-hide-country'] ) ) {
+	return;
+}
+
 add_filter(
 	'woocommerce_get_country_locale',
 	function ( $locales ) {
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/product-collection-compatibility-layer.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/product-collection-compatibility-layer.php
index b44b07a154a..b1cec709302 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/product-collection-compatibility-layer.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/product-collection-compatibility-layer.php
@@ -8,6 +8,11 @@
  * @package woocommerce-blocks-test-product-collection-compatibility-layer
  */

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-product-collection-compatibility-layer'] ) ) {
+	return;
+}
+
 add_action(
 	'woocommerce_before_main_content',
 	function () {
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
deleted file mode 100644
index 0af4a340bcb..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/short-nonce-life.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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/test-plugins/blocks/single-product-template-compatibility-layer.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/single-product-template-compatibility-layer.php
index 8299dfd3774..3eff695eb11 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/single-product-template-compatibility-layer.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/single-product-template-compatibility-layer.php
@@ -8,6 +8,11 @@
  * @package woocommerce-blocks-test-single-product-template-compatibility-layer
  */

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-single-product-template-compatibility-layer'] ) ) {
+	return;
+}
+
 $hooks = array(
 	'woocommerce_before_main_content',
 	'woocommerce_sidebar',
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/blocks/update-price.php b/plugins/woocommerce/tests/e2e/test-plugins/blocks/update-price.php
index 618d191ae2f..4dfeb3945b4 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/update-price.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/blocks/update-price.php
@@ -8,6 +8,11 @@
  * @package woocommerce-blocks-test-update-price
  */

+// Only requests from specs that enable this plugin send the cookie.
+if ( ! isset( $_COOKIE['woocommerce-blocks-test-update-price'] ) ) {
+	return;
+}
+
 function calc_price( $cart_object ) {
 	foreach ( $cart_object->get_cart() as $hash => $value ) {
 		$value['data']->set_price( 50 );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart-store/parent-item-key.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart-store/parent-item-key.block_theme.spec.ts
index 9ec81420843..be4ee49d40e 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart-store/parent-item-key.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart-store/parent-item-key.block_theme.spec.ts
@@ -8,12 +8,7 @@ import { test as base, expect, guestFile, wpCLI } from '@woocommerce/e2e-utils';
  * Internal dependencies
  */
 import AddToCartWithOptionsPage from '../add-to-cart-with-options/add-to-cart-with-options.page';
-import {
-	CART_LINE_IDENTITY_PLUGIN,
-	PRODUCT_X,
-	productButton,
-	seedDeclaredChildLine,
-} from './utils';
+import { PRODUCT_X, productButton, seedDeclaredChildLine } from './utils';

 type StoreApiCartItem = {
 	id: number;
@@ -58,35 +53,29 @@ const getProductIdBySlug = ( slug: string ): Promise< number > =>
 		`post list --post_type=product --field=ID --name="${ slug }" --format=ids`
 	);

-/** Adds Cap through the legacy URL and returns its session cart line. */
-const addCapToCart = async ( page: Page ) => {
-	const capId = await getProductIdBySlug( 'cap' );
-	await page.goto( `/?add-to-cart=${ capId }` );
-	const capLine = ( await readCartItems( page ) ).find(
-		( item ) => item.id === capId
+/** Adds a product through the legacy URL and returns its session cart line key. */
+const addProductToCart = async ( page: Page, productId: number ) => {
+	await page.goto( `/?add-to-cart=${ productId }` );
+	const line = ( await readCartItems( page ) ).find(
+		( item ) => item.id === productId
 	);
-	expect( capLine ).toBeDefined();
-	return {
-		capId,
-		parentItemKey: ( capLine as StoreApiCartItem ).key,
-	};
+	expect( line ).toBeDefined();
+	return ( line as StoreApiCartItem ).key;
 };

-/** Seeds Beanie as Cap's child and verifies the declared cart relationship. */
-const seedBeanieAsCapChild = async ( page: Page ) => {
-	const { capId, parentItemKey } = await addCapToCart( page );
+/** Seeds Beanie as the given parent's child and verifies the declared cart relationship. */
+const seedBeanieAsChildOf = async ( page: Page, parentId: number ) => {
+	const parentItemKey = await addProductToCart( page, parentId );
 	await seedDeclaredChildLine( page, PRODUCT_X.id, parentItemKey );

 	const items = await readCartItems( page );
 	expect( items ).toHaveLength( 2 );
-	expect( items.find( ( item ) => item.id === capId )?.key ).toBe(
+	expect( items.find( ( item ) => item.id === parentId )?.key ).toBe(
 		parentItemKey
 	);
 	expect(
 		items.find( ( item ) => item.id === PRODUCT_X.id )?.parent_item_key
 	).toBe( parentItemKey );
-
-	return { capId };
 };

 /** Reads the shop's server-rendered ProductButton without executing its scripts. */
@@ -150,13 +139,9 @@ const test = base.extend< {
 } );

 test.describe( 'ProductButton in-cart count', () => {
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin( CART_LINE_IDENTITY_PLUGIN );
-	} );
-
 	test( 'does not count a child cart line in the add to cart button', async ( {
 		page,
-		frontendUtils,
+		requestUtils,
 		addToCartWithOptionsPage,
 	} ) => {
 		await addToCartWithOptionsPage.createPostWithProductBlock(
@@ -164,64 +149,81 @@ test.describe( 'ProductButton in-cart count', () => {
 			'hoodie-blue-yes'
 		);
 		const postUrl = page.url();
+		const postId = Number(
+			( await page.locator( 'body' ).getAttribute( 'class' ) )?.match(
+				/\bpostid-(\d+)\b/
+			)?.[ 1 ]
+		);
+		expect( postId ).toBeGreaterThan( 0 );

-		await frontendUtils.emptyCart();
+		// All workers share the admin cart, so the cart steps run as a guest.
+		await page.context().clearCookies();

-		const variationId = await getSinglePostId(
-			'post list --post_type=product_variation --field=ID --name="Hoodie - Blue, Yes" --format=ids'
-		);
+		try {
+			const variationId = await getSinglePostId(
+				'post list --post_type=product_variation --field=ID --name="Hoodie - Blue, Yes" --format=ids'
+			);

-		const capId = await getProductIdBySlug( 'cap' );
-		await page.goto( `/?add-to-cart=${ capId }` );
-		const parentCartItems = await readCartItems( page );
-		expect( parentCartItems ).toHaveLength( 1 );
-		const parentLine = parentCartItems.find(
-			( item ) => item.id === capId
-		);
-		expect( parentLine ).toBeDefined();
-		const parentItemKey = ( parentLine as StoreApiCartItem ).key;
-
-		await seedDeclaredChildLine( page, variationId, parentItemKey );
-		const cartItems = await readCartItems( page );
-		expect( cartItems ).toHaveLength( 2 );
-		expect(
-			cartItems.find( ( item ) => item.id === variationId )
-				?.parent_item_key
-		).toBe( parentItemKey );
-		expect(
-			cartItems.find( ( item ) => item.key === parentItemKey )
-				?.parent_item_key
-		).toBeNull();
-
-		// Reads the server-rendered HTML without running scripts.
-		const readServerRenderedButtonText = async () => {
-			const response = await page.request.get( postUrl );
-			const button = ( await response.text() ).match(
-				/<button\b[^>]*class="[^"]*\bsingle_add_to_cart_button\b[^"]*"[^>]*>([\s\S]*?)<\/button>/
+			const capId = await getProductIdBySlug( 'cap' );
+			await page.goto( `/?add-to-cart=${ capId }` );
+			const parentCartItems = await readCartItems( page );
+			expect( parentCartItems ).toHaveLength( 1 );
+			const parentLine = parentCartItems.find(
+				( item ) => item.id === capId
 			);
-			expect( button ).not.toBeNull();
+			expect( parentLine ).toBeDefined();
+			const parentItemKey = ( parentLine as StoreApiCartItem ).key;

-			return button?.[ 1 ]?.replace( /<[^>]*>/g, '' ).trim();
-		};
+			await seedDeclaredChildLine( page, variationId, parentItemKey );
+			const cartItems = await readCartItems( page );
+			expect( cartItems ).toHaveLength( 2 );
+			expect(
+				cartItems.find( ( item ) => item.id === variationId )
+					?.parent_item_key
+			).toBe( parentItemKey );
+			expect(
+				cartItems.find( ( item ) => item.key === parentItemKey )
+					?.parent_item_key
+			).toBeNull();

-		const addToCartButton = page
-			.locator( '.wp-block-add-to-cart-with-options' )
-			.locator( '.single_add_to_cart_button' );
+			// Reads the server-rendered HTML without running scripts.
+			const readServerRenderedButtonText = async () => {
+				const response = await page.request.get( postUrl );
+				const button = ( await response.text() ).match(
+					/<button\b[^>]*class="[^"]*\bsingle_add_to_cart_button\b[^"]*"[^>]*>([\s\S]*?)<\/button>/
+				);
+				expect( button ).not.toBeNull();

-		expect( await readServerRenderedButtonText() ).toBe( 'Add to cart' );
-		await page.goto( postUrl );
-		// The server renders the button hidden and the client store reveals it, so
-		// waiting for it to be visible makes the next text check read the hydrated count.
-		await expect( addToCartButton ).toBeVisible();
-		await expect( addToCartButton ).toHaveText( 'Add to cart' );
+				return button?.[ 1 ]?.replace( /<[^>]*>/g, '' ).trim();
+			};

-		await addToCartButton.click();
-		await expect( addToCartButton ).toHaveText( '1 in cart' );
+			const addToCartButton = page
+				.locator( '.wp-block-add-to-cart-with-options' )
+				.locator( '.single_add_to_cart_button' );

-		expect( await readServerRenderedButtonText() ).toBe( '1 in cart' );
-		await page.reload();
-		await expect( addToCartButton ).toBeVisible();
-		await expect( addToCartButton ).toHaveText( '1 in cart' );
+			expect( await readServerRenderedButtonText() ).toBe(
+				'Add to cart'
+			);
+			await page.goto( postUrl );
+			// The server renders the button hidden and the client store reveals it, so
+			// waiting for it to be visible makes the next text check read the hydrated count.
+			await expect( addToCartButton ).toBeVisible();
+			await expect( addToCartButton ).toHaveText( 'Add to cart' );
+
+			await addToCartButton.click();
+			await expect( addToCartButton ).toHaveText( '1 in cart' );
+
+			expect( await readServerRenderedButtonText() ).toBe( '1 in cart' );
+			await page.reload();
+			await expect( addToCartButton ).toBeVisible();
+			await expect( addToCartButton ).toHaveText( '1 in cart' );
+		} finally {
+			await requestUtils.rest( {
+				method: 'DELETE',
+				path: `/wp/v2/posts/${ postId }`,
+				params: { force: true },
+			} );
+		}
 	} );

 	test.describe( 'as a guest', () => {
@@ -253,7 +255,8 @@ test.describe( 'ProductButton in-cart count', () => {
 			page,
 			miniCartUtils,
 		} ) => {
-			const { capId } = await seedBeanieAsCapChild( page );
+			const capId = await getProductIdBySlug( 'cap' );
+			await seedBeanieAsChildOf( page, capId );

 			await page.goto( '/shop/' );
 			const button = productButton( page, PRODUCT_X.id );
@@ -295,15 +298,31 @@ test.describe( 'ProductButton in-cart count', () => {

 		test( 'counts a child when its declared parent product is drafted', async ( {
 			page,
+			requestUtils,
 		} ) => {
-			const { capId } = await seedBeanieAsCapChild( page );
+			// Other specs read the sample catalog, so the parent is a product
+			// of its own. Hidden and dated in the past, it stays out of the
+			// catalog and New Arrivals.
+			const { id: parentId } = await requestUtils.rest< {
+				id: number;
+			} >( {
+				method: 'POST',
+				path: 'wc/v3/products',
+				data: {
+					name: 'Parent item key parent',
+					regular_price: '10',
+					catalog_visibility: 'hidden',
+					date_created: '2020-01-01T00:00:00',
+				},
+			} );

 			try {
-				await wpCLI( `post update ${ capId } --post_status=draft` );
+				await seedBeanieAsChildOf( page, parentId );
+				await wpCLI( `post update ${ parentId } --post_status=draft` );

 				const items = await readCartItems( page );
 				expect(
-					items.find( ( item ) => item.id === capId )
+					items.find( ( item ) => item.id === parentId )
 				).toBeUndefined();
 				const childLine = items.find(
 					( item ) => item.id === PRODUCT_X.id
@@ -326,7 +345,11 @@ test.describe( 'ProductButton in-cart count', () => {
 					'2 in cart'
 				);
 			} finally {
-				await wpCLI( `post update ${ capId } --post_status=publish` );
+				await requestUtils.rest( {
+					method: 'DELETE',
+					path: `wc/v3/products/${ parentId }`,
+					params: { force: true },
+				} );
 			}
 		} );
 	} );
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 5cc858cf7df..8f3b005f364 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
@@ -1,7 +1,12 @@
 /**
  * External dependencies
  */
-import { test as base, expect } from '@woocommerce/e2e-utils';
+import {
+	test as base,
+	expect,
+	enableTestPlugin,
+	guestFile,
+} from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -24,6 +29,8 @@ const test = base.extend< { pageObject: CartPage } >( {
 } );

 test.describe( 'Shopper → Cart block', () => {
+	test.use( { storageState: guestFile } );
+
 	test( 'The discount label is only visible next to the discounted product', async ( {
 		pageObject,
 		frontendUtils,
@@ -63,12 +70,10 @@ test.describe( 'Shopper → Cart block', () => {

 	test( 'Products with updated prices should not display a discount label', async ( {
 		pageObject,
-		requestUtils,
+		context,
 		frontendUtils,
 	} ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-update-price'
-		);
+		await enableTestPlugin( context, 'update-price' );

 		await frontendUtils.goToShop();
 		await frontendUtils.addToCart( DISCOUNTED_PRODUCT_NAME );
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 83b4f025a9e..9f1bc9945f2 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
@@ -7,14 +7,13 @@ import { test, expect, guestFile } from '@woocommerce/e2e-utils';
  * Internal dependencies
  */
 import { REGULAR_PRICED_PRODUCT_NAME } from '../checkout/constants';
+import { setFilterValue } from '../../../utils/filters';

 test.describe( 'Cart Store', () => {
 	test.use( { storageState: guestFile } );

-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-short-nonce-life'
-		);
+	test.beforeEach( async ( { page } ) => {
+		await setFilterValue( page, 'nonce_life', 2 );
 	} );

 	test( 'should refresh nonce from Store API and use it for cart mutations', async ( {
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
index 626908309ce..7995cbad231 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
@@ -1,7 +1,12 @@
 /**
  * External dependencies
  */
-import { expect, test as base, guestFile } from '@woocommerce/e2e-utils';
+import {
+	expect,
+	test as base,
+	enableTestPlugin,
+	guestFile,
+} from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -23,10 +28,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
 	test.describe( 'Guest shopper', () => {
 		test.use( { storageState: guestFile } );

-		test.beforeEach( async ( { requestUtils } ) => {
-			await requestUtils.activatePlugin(
-				'woocommerce-blocks-test-additional-checkout-fields'
-			);
+		test.beforeEach( async ( { context } ) => {
+			await enableTestPlugin( context, 'additional-checkout-fields' );
 		} );

 		test( 'Shopper can see an error message when a required field is not filled in the checkout form', async ( {
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
index be1fa356551..ab2c0063968 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { expect, test as base } from '@woocommerce/e2e-utils';
+import { expect, test as base, enableTestPlugin } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -19,10 +19,8 @@ const test = base.extend< { checkoutPageObject: CheckoutPage } >( {
 } );

 test.describe( 'Merchant → Additional Checkout Fields', () => {
-	test.beforeEach( async ( { requestUtils, frontendUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-additional-checkout-fields'
-		);
+	test.beforeEach( async ( { context, frontendUtils } ) => {
+		await enableTestPlugin( context, 'additional-checkout-fields' );

 		await frontendUtils.goToShop();
 		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
index d42f15a840f..55a68e73056 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
@@ -1,7 +1,12 @@
 /**
  * External dependencies
  */
-import { expect, test as base, customerFile } from '@woocommerce/e2e-utils';
+import {
+	expect,
+	test as base,
+	customerFile,
+	enableTestPlugin,
+} from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -23,10 +28,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
 	test.describe( 'Logged in shopper', () => {
 		test.use( { storageState: customerFile } );

-		test.beforeEach( async ( { requestUtils } ) => {
-			await requestUtils.activatePlugin(
-				'woocommerce-blocks-test-additional-checkout-fields'
-			);
+		test.beforeEach( async ( { context } ) => {
+			await enableTestPlugin( context, 'additional-checkout-fields' );
 		} );

 		test( 'Shopper can fill in the checkout form with additional fields and can have different value for same field in shipping and billing address', async ( {
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts
index 398e43150f5..0a0e23e653a 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-locale-hide-country.block_theme.spec.ts
@@ -1,7 +1,12 @@
 /**
  * External dependencies
  */
-import { expect, test as base, guestFile } from '@woocommerce/e2e-utils';
+import {
+	expect,
+	test as base,
+	enableTestPlugin,
+	guestFile,
+} from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -22,10 +27,8 @@ const test = base.extend< { checkoutPageObject: CheckoutPage } >( {
 test.describe( 'Checkout Block → Locale hides address fields but not country', () => {
 	test.use( { storageState: guestFile } );

-	test.beforeEach( async ( { requestUtils, frontendUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-locale-hide-country'
-		);
+	test.beforeEach( async ( { context, requestUtils, frontendUtils } ) => {
+		await enableTestPlugin( context, 'locale-hide-country' );
 		await requestUtils.rest( {
 			method: 'PUT',
 			path: 'wc/v3/settings/account/woocommerce_enable_guest_checkout',
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart-item-data.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart-item-data.block_theme.spec.ts
new file mode 100644
index 00000000000..6f452bef48b
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart-item-data.block_theme.spec.ts
@@ -0,0 +1,475 @@
+/**
+ * External dependencies
+ */
+import { test, expect, enableTestPlugin } from '@woocommerce/e2e-utils';
+import type { Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { REGULAR_PRICED_PRODUCT_NAME } from '../checkout/constants';
+
+test.describe( 'Mini-Cart Block (item data)', () => {
+	test.use( {
+		storageState: {
+			origins: [],
+			cookies: [],
+		},
+	} );
+
+	test.beforeEach( async ( { context } ) => {
+		await enableTestPlugin( context, 'item-data-display' );
+	} );
+
+	test( 'should render plain text item data', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		// Verify the plain text name and value are rendered.
+		await expect(
+			dialog.locator( '.wc-block-components-product-details__name' )
+		).toContainText( [ 'Gift Message' ] );
+		await expect(
+			dialog.locator( '.wc-block-components-product-details__value' )
+		).toContainText( [ 'Happy Birthday!' ] );
+	} );
+
+	test( 'should use display field value when present', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		// The display field contains "<em>Best Wishes</em>".
+		// Verify the <em> tag is rendered as an actual HTML element.
+		const engravingValue = dialog
+			.locator( '.wc-block-components-product-details__value' )
+			.filter( { hasText: 'Best Wishes' } );
+		await expect( engravingValue ).toBeVisible();
+		await expect( engravingValue.locator( 'em' ) ).toBeVisible();
+		await expect( engravingValue.locator( 'em' ) ).toHaveText(
+			'Best Wishes'
+		);
+	} );
+
+	test( 'should decode HTML entities in item data values', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		// The value "1 &lt; 2" should display as "1 < 2"
+		// (entity decoded), not as literal "1 &lt; 2".
+		const sizeValue = dialog
+			.locator( '.wc-block-components-product-details__value' )
+			.filter( { hasText: '1' } );
+		await expect( sizeValue ).toBeVisible();
+		await expect( sizeValue ).toContainText( '1 < 2' );
+	} );
+
+	test( 'should not render entity-encoded HTML tags as DOM elements', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const noteValue = dialog
+			.locator( '.wc-block-components-product-details__value' )
+			.filter( { hasText: 'important' } );
+		await expect( noteValue ).toBeVisible();
+		await expect( noteValue.locator( 'b' ) ).toHaveCount( 0 );
+	} );
+
+	test( 'should show a separator between item-data entries but not after the last one', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		// The fixture plugin injects 4 well-formed entries (Gift Message,
+		// Engraving, Size, Note) onto the cart item. Confirm all 4 rendered
+		// before counting separators, so a count mismatch below can only be
+		// attributed to separator placement, not to a missing entry.
+		const entryNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( entryNames ).toHaveCount( 4 );
+
+		// Each entry's trailing separator is a `span[aria-hidden="true"]`;
+		// the store getter that drives its `hidden` binding removes the
+		// `hidden` attribute exactly for the entries that should show a
+		// separator. Count only the ones that are actually visible (not
+		// `hidden`) — using `textContent` with a regex would also match the
+		// text of `hidden` separators and give a false pass/fail.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:not([hidden])'
+		);
+		await expect( visibleSeparators ).toHaveCount( 3 );
+	} );
+} );
+
+/**
+ * Registers page-error and console-error listeners for a defect scenario
+ * (trailing malformed/hidden item-data entries). Must be called before
+ * navigating so nothing that happens during the mini-cart's render is
+ * missed — mirrors the listener setup used by the "no client-side error"
+ * test in mini-cart.block_theme.spec.ts.
+ *
+ * @param {Page} page Playwright page to observe.
+ * @return {{pageErrors: string[], consoleErrors: string[]}} Arrays that
+ *                                                            accumulate as
+ *                                                            errors fire.
+ */
+const trackErrors = ( page: Page ) => {
+	const pageErrors: string[] = [];
+	const consoleErrors: string[] = [];
+
+	page.on( 'pageerror', ( error ) => {
+		pageErrors.push( error.message );
+	} );
+	page.on( 'console', ( message ) => {
+		// Exclude the browser's own "failed to load resource" network
+		// diagnostics: they are not JS errors raised by page code (no
+		// `console.error` call is involved) and can fire for reasons
+		// entirely unrelated to the item-data/separator logic under test.
+		if (
+			message.type() === 'error' &&
+			! message.text().includes( 'Failed to load resource' )
+		) {
+			consoleErrors.push( message.text() );
+		}
+	} );
+
+	return { pageErrors, consoleErrors };
+};
+
+test.describe( 'Mini-Cart Block (item data - malformed trailing entries)', () => {
+	test.use( {
+		storageState: {
+			origins: [],
+			cookies: [],
+		},
+	} );
+
+	test.beforeEach( async ( { context } ) => {
+		await enableTestPlugin( context, 'item-data-display-malformed' );
+	} );
+
+	test( 'should render no client-side error and no trailing separator when the item data ends with a single malformed entry', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Quantity 1 selects the fixture's [visible, malformed] scenario.
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		// Both entries render into the DOM (the malformed one hidden);
+		// confirm the DOM count before checking visibility, so a mismatch
+		// below can only be attributed to visibility/separator logic, not
+		// to a missing entry.
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 2 );
+
+		// The malformed entry renders no visible name/value: only the
+		// well-formed "Gift Message" entry is visible. Using `:visible`
+		// filters out elements hidden via the `hidden` attribute, unlike a
+		// plain `textContent` check which would also match hidden text.
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// The last (and only) visible entry shows no trailing separator,
+		// and no stray separator appears anywhere else in the list. Count
+		// only separators that are actually visible. `:visible` accounts
+		// for ancestor visibility, so a malformed/hidden entry's separator
+		// span — whose own `hidden` attribute is absent because it is not
+		// the last *visible* entry, but whose entry wrapper is hidden — is
+		// correctly excluded. A plain `:not([hidden])` attribute check
+		// would miss the hidden ancestor and a `textContent` regex would
+		// match hidden text.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+
+	test( 'should render no client-side error and no trailing separator when the item data ends with two consecutive malformed entries', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Adding the same product twice merges into a single cart line with
+		// quantity 2, which selects the fixture's
+		// [visible, malformed, malformed] scenario.
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 3 );
+
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// Count only separators that are actually visible. `:visible`
+		// accounts for ancestor visibility, so a malformed/hidden entry's
+		// separator span (hidden via its entry wrapper, not its own
+		// attribute) is correctly excluded — unlike `:not([hidden])`.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+} );
+
+test.describe( 'Mini-Cart Block (item data - hidden trailing entries)', () => {
+	test.use( {
+		storageState: {
+			origins: [],
+			cookies: [],
+		},
+	} );
+
+	test.beforeEach( async ( { context } ) => {
+		await enableTestPlugin( context, 'item-data-display-hidden' );
+	} );
+
+	test( 'should render no client-side error and no trailing separator when the item data ends with a single explicitly-hidden entry', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Quantity 1 selects the fixture's [visible, hidden] scenario.
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 2 );
+
+		// The explicitly-hidden entry renders no visible name/value ("Secret"
+		// never appears among the visible entries): only the well-formed
+		// "Gift Message" entry is visible.
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// Count only separators that are actually visible. `:visible`
+		// accounts for ancestor visibility, so a malformed/hidden entry's
+		// separator span (hidden via its entry wrapper, not its own
+		// attribute) is correctly excluded — unlike `:not([hidden])`.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+
+	test( 'should render no client-side error and no trailing separator when the item data ends with two consecutive explicitly-hidden entries', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Adding the same product twice merges into a single cart line with
+		// quantity 2, which selects the fixture's
+		// [visible, hidden, hidden] scenario.
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 3 );
+
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// Count only separators that are actually visible. `:visible`
+		// accounts for ancestor visibility, so a malformed/hidden entry's
+		// separator span (hidden via its entry wrapper, not its own
+		// attribute) is correctly excluded — unlike `:not([hidden])`.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+} );
+
+test.describe( 'Mini-Cart Block (item data - mixed trailing entries)', () => {
+	test.use( {
+		storageState: {
+			origins: [],
+			cookies: [],
+		},
+	} );
+
+	test.beforeEach( async ( { context } ) => {
+		await enableTestPlugin( context, 'item-data-display-mixed' );
+	} );
+
+	test( 'should show no trailing separator when the item data ends with a malformed entry followed by a hidden entry', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Quantity 1 selects the fixture's
+		// [visible, malformed, hidden] scenario.
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 3 );
+
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// Count only separators that are actually visible. `:visible`
+		// accounts for ancestor visibility, so a malformed/hidden entry's
+		// separator span (hidden via its entry wrapper, not its own
+		// attribute) is correctly excluded — unlike `:not([hidden])`.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+
+	test( 'should show no trailing separator when the item data ends with a hidden entry followed by a malformed entry', async ( {
+		page,
+		frontendUtils,
+		miniCartUtils,
+	} ) => {
+		const { pageErrors, consoleErrors } = trackErrors( page );
+
+		await frontendUtils.goToShop();
+		// Adding the same product twice merges into a single cart line with
+		// quantity 2, which selects the fixture's
+		// [visible, hidden, malformed] scenario (the reverse order).
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await miniCartUtils.openMiniCart();
+
+		const dialog = page.getByRole( 'dialog' );
+		await expect( dialog ).toBeVisible();
+
+		const allNames = dialog.locator(
+			'.wc-block-components-product-details__name'
+		);
+		await expect( allNames ).toHaveCount( 3 );
+
+		const visibleNames = dialog.locator(
+			'.wc-block-components-product-details__name:visible'
+		);
+		await expect( visibleNames ).toHaveCount( 1 );
+		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
+
+		// Count only separators that are actually visible. `:visible`
+		// accounts for ancestor visibility, so a malformed/hidden entry's
+		// separator span (hidden via its entry wrapper, not its own
+		// attribute) is correctly excluded — unlike `:not([hidden])`.
+		const visibleSeparators = dialog.locator(
+			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
+		);
+		await expect( visibleSeparators ).toHaveCount( 0 );
+
+		expect( pageErrors ).toEqual( [] );
+		expect( consoleErrors ).toEqual( [] );
+	} );
+} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts
index 6178f4fb0ff..523bc5b1fe7 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts
@@ -2,7 +2,6 @@
  * External dependencies
  */
 import { test, expect, BlockData, wpCLI } from '@woocommerce/e2e-utils';
-import type { Page } from '@playwright/test';

 /**
  * Internal dependencies
@@ -504,483 +503,6 @@ test.describe( `${ blockData.name } Block (admin)`, () => {
 	} );
 } );

-test.describe( `${ blockData.name } Block (item data)`, () => {
-	test.use( {
-		storageState: {
-			origins: [],
-			cookies: [],
-		},
-	} );
-
-	// Activate in beforeEach because the DB is reset after every test.
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-item-data-display'
-		);
-	} );
-
-	test( 'should render plain text item data', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		await frontendUtils.goToShop();
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		// Verify the plain text name and value are rendered.
-		await expect(
-			dialog.locator( '.wc-block-components-product-details__name' )
-		).toContainText( [ 'Gift Message' ] );
-		await expect(
-			dialog.locator( '.wc-block-components-product-details__value' )
-		).toContainText( [ 'Happy Birthday!' ] );
-	} );
-
-	test( 'should use display field value when present', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		await frontendUtils.goToShop();
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		// The display field contains "<em>Best Wishes</em>".
-		// Verify the <em> tag is rendered as an actual HTML element.
-		const engravingValue = dialog
-			.locator( '.wc-block-components-product-details__value' )
-			.filter( { hasText: 'Best Wishes' } );
-		await expect( engravingValue ).toBeVisible();
-		await expect( engravingValue.locator( 'em' ) ).toBeVisible();
-		await expect( engravingValue.locator( 'em' ) ).toHaveText(
-			'Best Wishes'
-		);
-	} );
-
-	test( 'should decode HTML entities in item data values', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		await frontendUtils.goToShop();
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		// The value "1 &lt; 2" should display as "1 < 2"
-		// (entity decoded), not as literal "1 &lt; 2".
-		const sizeValue = dialog
-			.locator( '.wc-block-components-product-details__value' )
-			.filter( { hasText: '1' } );
-		await expect( sizeValue ).toBeVisible();
-		await expect( sizeValue ).toContainText( '1 < 2' );
-	} );
-
-	test( 'should not render entity-encoded HTML tags as DOM elements', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		await frontendUtils.goToShop();
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const noteValue = dialog
-			.locator( '.wc-block-components-product-details__value' )
-			.filter( { hasText: 'important' } );
-		await expect( noteValue ).toBeVisible();
-		await expect( noteValue.locator( 'b' ) ).toHaveCount( 0 );
-	} );
-
-	test( 'should show a separator between item-data entries but not after the last one', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		await frontendUtils.goToShop();
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		// The fixture plugin injects 4 well-formed entries (Gift Message,
-		// Engraving, Size, Note) onto the cart item. Confirm all 4 rendered
-		// before counting separators, so a count mismatch below can only be
-		// attributed to separator placement, not to a missing entry.
-		const entryNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( entryNames ).toHaveCount( 4 );
-
-		// Each entry's trailing separator is a `span[aria-hidden="true"]`;
-		// the store getter that drives its `hidden` binding removes the
-		// `hidden` attribute exactly for the entries that should show a
-		// separator. Count only the ones that are actually visible (not
-		// `hidden`) — using `textContent` with a regex would also match the
-		// text of `hidden` separators and give a false pass/fail.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:not([hidden])'
-		);
-		await expect( visibleSeparators ).toHaveCount( 3 );
-	} );
-} );
-
-/**
- * Registers page-error and console-error listeners for a defect scenario
- * (trailing malformed/hidden item-data entries). Must be called before
- * navigating so nothing that happens during the mini-cart's render is
- * missed — mirrors the listener setup used by the "no client-side error"
- * test above.
- *
- * @param {Page} page Playwright page to observe.
- * @return {{pageErrors: string[], consoleErrors: string[]}} Arrays that
- *                                                            accumulate as
- *                                                            errors fire.
- */
-const trackErrors = ( page: Page ) => {
-	const pageErrors: string[] = [];
-	const consoleErrors: string[] = [];
-
-	page.on( 'pageerror', ( error ) => {
-		pageErrors.push( error.message );
-	} );
-	page.on( 'console', ( message ) => {
-		// Exclude the browser's own "failed to load resource" network
-		// diagnostics: they are not JS errors raised by page code (no
-		// `console.error` call is involved) and can fire for reasons
-		// entirely unrelated to the item-data/separator logic under test.
-		if (
-			message.type() === 'error' &&
-			! message.text().includes( 'Failed to load resource' )
-		) {
-			consoleErrors.push( message.text() );
-		}
-	} );
-
-	return { pageErrors, consoleErrors };
-};
-
-test.describe( `${ blockData.name } Block (item data - malformed trailing entries)`, () => {
-	test.use( {
-		storageState: {
-			origins: [],
-			cookies: [],
-		},
-	} );
-
-	// Activate in beforeEach because the DB is reset after every test.
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-item-data-display-malformed'
-		);
-	} );
-
-	test( 'should render no client-side error and no trailing separator when the item data ends with a single malformed entry', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Quantity 1 selects the fixture's [visible, malformed] scenario.
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		// Both entries render into the DOM (the malformed one hidden);
-		// confirm the DOM count before checking visibility, so a mismatch
-		// below can only be attributed to visibility/separator logic, not
-		// to a missing entry.
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 2 );
-
-		// The malformed entry renders no visible name/value: only the
-		// well-formed "Gift Message" entry is visible. Using `:visible`
-		// filters out elements hidden via the `hidden` attribute, unlike a
-		// plain `textContent` check which would also match hidden text.
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// The last (and only) visible entry shows no trailing separator,
-		// and no stray separator appears anywhere else in the list. Count
-		// only separators that are actually visible. `:visible` accounts
-		// for ancestor visibility, so a malformed/hidden entry's separator
-		// span — whose own `hidden` attribute is absent because it is not
-		// the last *visible* entry, but whose entry wrapper is hidden — is
-		// correctly excluded. A plain `:not([hidden])` attribute check
-		// would miss the hidden ancestor and a `textContent` regex would
-		// match hidden text.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-
-	test( 'should render no client-side error and no trailing separator when the item data ends with two consecutive malformed entries', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Adding the same product twice merges into a single cart line with
-		// quantity 2, which selects the fixture's
-		// [visible, malformed, malformed] scenario.
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 3 );
-
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// Count only separators that are actually visible. `:visible`
-		// accounts for ancestor visibility, so a malformed/hidden entry's
-		// separator span (hidden via its entry wrapper, not its own
-		// attribute) is correctly excluded — unlike `:not([hidden])`.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-} );
-
-test.describe( `${ blockData.name } Block (item data - hidden trailing entries)`, () => {
-	test.use( {
-		storageState: {
-			origins: [],
-			cookies: [],
-		},
-	} );
-
-	// Activate in beforeEach because the DB is reset after every test.
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-item-data-display-hidden'
-		);
-	} );
-
-	test( 'should render no client-side error and no trailing separator when the item data ends with a single explicitly-hidden entry', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Quantity 1 selects the fixture's [visible, hidden] scenario.
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 2 );
-
-		// The explicitly-hidden entry renders no visible name/value ("Secret"
-		// never appears among the visible entries): only the well-formed
-		// "Gift Message" entry is visible.
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// Count only separators that are actually visible. `:visible`
-		// accounts for ancestor visibility, so a malformed/hidden entry's
-		// separator span (hidden via its entry wrapper, not its own
-		// attribute) is correctly excluded — unlike `:not([hidden])`.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-
-	test( 'should render no client-side error and no trailing separator when the item data ends with two consecutive explicitly-hidden entries', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Adding the same product twice merges into a single cart line with
-		// quantity 2, which selects the fixture's
-		// [visible, hidden, hidden] scenario.
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 3 );
-
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// Count only separators that are actually visible. `:visible`
-		// accounts for ancestor visibility, so a malformed/hidden entry's
-		// separator span (hidden via its entry wrapper, not its own
-		// attribute) is correctly excluded — unlike `:not([hidden])`.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-} );
-
-test.describe( `${ blockData.name } Block (item data - mixed trailing entries)`, () => {
-	test.use( {
-		storageState: {
-			origins: [],
-			cookies: [],
-		},
-	} );
-
-	// Activate in beforeEach because the DB is reset after every test.
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-item-data-display-mixed'
-		);
-	} );
-
-	test( 'should show no trailing separator when the item data ends with a malformed entry followed by a hidden entry', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Quantity 1 selects the fixture's
-		// [visible, malformed, hidden] scenario.
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 3 );
-
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// Count only separators that are actually visible. `:visible`
-		// accounts for ancestor visibility, so a malformed/hidden entry's
-		// separator span (hidden via its entry wrapper, not its own
-		// attribute) is correctly excluded — unlike `:not([hidden])`.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-
-	test( 'should show no trailing separator when the item data ends with a hidden entry followed by a malformed entry', async ( {
-		page,
-		frontendUtils,
-		miniCartUtils,
-	} ) => {
-		const { pageErrors, consoleErrors } = trackErrors( page );
-
-		await frontendUtils.goToShop();
-		// Adding the same product twice merges into a single cart line with
-		// quantity 2, which selects the fixture's
-		// [visible, hidden, malformed] scenario (the reverse order).
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-		await miniCartUtils.openMiniCart();
-
-		const dialog = page.getByRole( 'dialog' );
-		await expect( dialog ).toBeVisible();
-
-		const allNames = dialog.locator(
-			'.wc-block-components-product-details__name'
-		);
-		await expect( allNames ).toHaveCount( 3 );
-
-		const visibleNames = dialog.locator(
-			'.wc-block-components-product-details__name:visible'
-		);
-		await expect( visibleNames ).toHaveCount( 1 );
-		await expect( visibleNames ).toContainText( [ 'Gift Message' ] );
-
-		// Count only separators that are actually visible. `:visible`
-		// accounts for ancestor visibility, so a malformed/hidden entry's
-		// separator span (hidden via its entry wrapper, not its own
-		// attribute) is correctly excluded — unlike `:not([hidden])`.
-		const visibleSeparators = dialog.locator(
-			'.wc-block-components-product-details span[aria-hidden="true"]:visible'
-		);
-		await expect( visibleSeparators ).toHaveCount( 0 );
-
-		expect( pageErrors ).toEqual( [] );
-		expect( consoleErrors ).toEqual( [] );
-	} );
-} );
-
 test.describe( `${ blockData.name } Block (variation attributes)`, () => {
 	test.use( {
 		storageState: {
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/compatibility-layer.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/compatibility-layer.block_theme.spec.ts
index e5eaaa2ce08..be36f78e602 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/compatibility-layer.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/compatibility-layer.block_theme.spec.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { test as base, expect } from '@woocommerce/e2e-utils';
+import { test as base, expect, enableTestPlugin } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -20,9 +20,10 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
 } );

 test.describe( 'Product Collection: Compatibility Layer', () => {
-	test.beforeEach( async ( { pageObject, requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-product-collection-compatibility-layer'
+	test.beforeEach( async ( { pageObject, context } ) => {
+		await enableTestPlugin(
+			context,
+			'product-collection-compatibility-layer'
 		);
 		await pageObject.goToProductCatalogFrontend();
 	} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
index c015ff65970..99c3f7660f6 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
@@ -1,7 +1,12 @@
 /**
  * External dependencies
  */
-import { test, expect, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';
+import {
+	test,
+	expect,
+	BLOCK_THEME_SLUG,
+	enableTestPlugin,
+} from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -149,9 +154,10 @@ const variableProductAddToCartWithOptionsBlockHooks: Scenario[] = [
 ];

 test.describe( 'Compatibility Layer in Single Product template', () => {
-	test.beforeEach( async ( { requestUtils } ) => {
-		await requestUtils.activatePlugin(
-			'woocommerce-blocks-test-single-product-template-compatibility-layer'
+	test.beforeEach( async ( { context } ) => {
+		await enableTestPlugin(
+			context,
+			'single-product-template-compatibility-layer'
 		);
 	} );

diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/frontend/frontend-utils.page.ts b/plugins/woocommerce/tests/e2e/utils/blocks/frontend/frontend-utils.page.ts
index 168a0514056..59a8cc86cbb 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/frontend/frontend-utils.page.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/frontend/frontend-utils.page.ts
@@ -132,6 +132,16 @@ export class FrontendUtils {
 			// descendant text, so a product cannot be selected by text the
 			// card repeats, and keep it exact, or "Beanie" also selects
 			// "Beanie with Logo".
+			//
+			// The request starts after the click returns, so the tracker below
+			// alone can resolve before it. A guest gets its session cookie
+			// only from this response.
+			const cartUpdated = this.page.waitForResponse( ( response ) =>
+				[
+					...STORE_API_CART_WRITE_REQUEST_URLS,
+					'wc-ajax=add_to_cart',
+				].some( ( url ) => response.url().includes( url ) )
+			);
 			await this.page
 				.locator( 'li.product' )
 				.filter( {
@@ -142,6 +152,7 @@ export class FrontendUtils {
 				} )
 				.locator( '.add_to_cart_button' )
 				.click();
+			await cartUpdated;
 		} else {
 			await this.page.click( 'text=Add to cart' );
 		}
diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/index.ts b/plugins/woocommerce/tests/e2e/utils/blocks/index.ts
index fc54dbe7f96..19c07b7943a 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/index.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/index.ts
@@ -11,6 +11,7 @@ export * from './performance';
 export * from './product-attributes';
 export * from './request-utils';
 export * from './shipping';
+export * from './test-plugins';

 export * from './test';

diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts b/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts
new file mode 100644
index 00000000000..5118ee40dd7
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import type { BrowserContext } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { BASE_URL } from './constants';
+
+/**
+ * Test plugins that `blocks setup` activates for all specs. Each one acts only
+ * on requests that send a cookie with the plugin slug as its name, so a spec
+ * enables it for its own browser context with `enableTestPlugin()`.
+ */
+export const COOKIE_GATED_TEST_PLUGINS = [
+	'additional-checkout-fields',
+	'item-data-display',
+	'item-data-display-hidden',
+	'item-data-display-malformed',
+	'item-data-display-mixed',
+	'locale-hide-country',
+	'product-collection-compatibility-layer',
+	'single-product-template-compatibility-layer',
+	'update-price',
+] as const;
+
+export async function enableTestPlugin(
+	context: BrowserContext,
+	plugin: ( typeof COOKIE_GATED_TEST_PLUGINS )[ number ]
+) {
+	await context.addCookies( [
+		{
+			name: `woocommerce-blocks-test-${ plugin }`,
+			value: '1',
+			url: BASE_URL,
+		},
+	] );
+}