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 < 2" should display as "1 < 2"
+ // (entity decoded), not as literal "1 < 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 < 2" should display as "1 < 2"
- // (entity decoded), not as literal "1 < 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,
+ },
+ ] );
+}