Commit e765c73c607 for woocommerce
commit e765c73c6079daa4e2fadc57825feac380b3a812
Author: Ajitem Sahasrabuddhe <ajitem.s@outlook.com>
Date: Thu Nov 5 19:58:24 2026 +0530
Demote Mini-Cart item data E2E tests to the unit test suite (#68181)
diff --git a/plugins/woocommerce/changelog/testops-234-remove-redundant-mini-cart-item-data-tests b/plugins/woocommerce/changelog/testops-234-remove-redundant-mini-cart-item-data-tests
new file mode 100644
index 00000000000..c0850b5042e
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-remove-redundant-mini-cart-item-data-tests
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Remove 8 redundant Mini-Cart item data E2E tests already covered by unit tests.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/frontend.ts
index e94c3a6d4a4..1a4b4deb628 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/frontend.ts
@@ -28,10 +28,10 @@ import type { CartItem, Currency } from '@woocommerce/types';
import { getFocusableElements } from '../../utils/get-focusable-elements';
import { translateJQueryEventToNative } from '../../base/stores/woocommerce/legacy-events';
import {
- getEntryFieldRaw,
isItemDataEntryVisible,
isLastVisibleEntry,
buildCartItemDataAttr,
+ renderEntryFieldHTML,
} from './utils/item-data';
import type { ItemData, CartItemDataAttr } from './utils/item-data';
@@ -439,14 +439,6 @@ function resolveDataItemAttr(): ItemData | undefined {
return itemData || cartItemState.cartItem[ dataProperty ]?.[ 0 ];
}
-/**
- * Returns the raw API value for an item_data field. Used by both innerHTML
- * callbacks and the cartItemDataAttr getter.
- */
-function getItemDataRaw( field: 'name' | 'value' ): string {
- return getEntryFieldRaw( resolveDataItemAttr(), field );
-}
-
const { state: cartItemState } = store(
'woocommerce/mini-cart-products-table-block',
{
@@ -960,18 +952,22 @@ const { state: cartItemState } = store(
itemDataNameInnerHTML() {
const { ref } = getElement();
- const raw = getItemDataRaw( 'name' );
- if ( ref && raw ) {
- ref.innerHTML = trimWords( raw + ':' );
- }
+ renderEntryFieldHTML(
+ ref,
+ resolveDataItemAttr(),
+ 'name',
+ trimWords
+ );
},
itemDataValueInnerHTML() {
const { ref } = getElement();
- const raw = getItemDataRaw( 'value' );
- if ( ref && raw ) {
- ref.innerHTML = trimWords( raw );
- }
+ renderEntryFieldHTML(
+ ref,
+ resolveDataItemAttr(),
+ 'value',
+ trimWords
+ );
},
filterCartItemClass() {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
index b677041b1aa..4afef04e150 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/item-data.ts
@@ -1,11 +1,11 @@
/**
- * Pure, side-effect-free helpers backing the Mini-Cart product table's
- * per-entry `item_data`/`variation` rendering.
+ * Framework-free helpers backing the Mini-Cart product table's per-entry
+ * `item_data`/`variation` rendering.
*
* This module must stay free of top-level side effects and must not import
* `@wordpress/interactivity` so it can be unit-tested with zero mocking.
- * `frontend.ts`'s store getters resolve the Interactivity context and
- * delegate the actual decision-making to these functions.
+ * `frontend.ts`'s store getters and callbacks resolve the Interactivity
+ * context and delegate the actual decision-making to these functions.
*/
/**
@@ -175,3 +175,37 @@ export function buildCartItemDataAttr(
.toLowerCase() }`,
};
}
+
+/**
+ * Writes an entry's raw field into `el` as HTML, backing the Mini-Cart's
+ * `itemDataNameInnerHTML`/`itemDataValueInnerHTML` callbacks.
+ *
+ * The raw string goes through `innerHTML` on purpose. The Store API returns
+ * `item_data` already escaped and a `display` field may carry real markup,
+ * so entities decode (`1 < 2` reads as `1 < 2`) while entity-encoded tags
+ * stay inert text rather than becoming elements.
+ *
+ * Nothing is written when there is no element or no usable field value, so a
+ * malformed entry leaves the existing markup alone.
+ *
+ * @param {Element | null | undefined} el Element to write into.
+ * @param {ItemData | undefined} entry Entry to read the field from.
+ * @param {'name'|'value'} field Which field to render. `name` is
+ * suffixed with `:`.
+ * @param {Function} format Optional transform applied to the
+ * HTML before it is written.
+ */
+export function renderEntryFieldHTML(
+ el: Element | null | undefined,
+ entry: ItemData | undefined,
+ field: 'name' | 'value',
+ format: ( html: string ) => string = ( html ) => html
+): void {
+ const raw = getEntryFieldRaw( entry, field );
+
+ if ( ! el || ! raw ) {
+ return;
+ }
+
+ el.innerHTML = format( field === 'name' ? `${ raw }:` : raw );
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/test/item-data.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/test/item-data.ts
index c24f25bd040..87b2b5119a7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/test/item-data.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/utils/test/item-data.ts
@@ -8,6 +8,7 @@ import {
isItemDataEntryVisible,
isLastVisibleEntry,
buildCartItemDataAttr,
+ renderEntryFieldHTML,
} from '../item-data';
/**
@@ -397,3 +398,68 @@ describe( 'buildCartItemDataAttr tests', () => {
} );
} );
} );
+
+describe( 'renderEntryFieldHTML tests', () => {
+ let el: HTMLElement;
+
+ beforeEach( () => {
+ el = document.createElement( 'span' );
+ } );
+
+ test( 'renders the display field as real markup', () => {
+ renderEntryFieldHTML(
+ el,
+ entry( 'Engraving', 'Best Wishes', {
+ display: '<em>Best Wishes</em>',
+ } ),
+ 'value'
+ );
+
+ expect( el.querySelectorAll( 'em' ) ).toHaveLength( 1 );
+ expect( el.querySelector( 'em' )?.textContent ).toBe( 'Best Wishes' );
+ } );
+
+ test( 'decodes entity-encoded characters in the value', () => {
+ renderEntryFieldHTML( el, entry( 'Size', '1 < 2' ), 'value' );
+
+ expect( el.textContent ).toBe( '1 < 2' );
+ } );
+
+ test( 'leaves entity-encoded tags as text instead of DOM elements', () => {
+ renderEntryFieldHTML(
+ el,
+ entry( 'Note', '<b>important</b>' ),
+ 'value'
+ );
+
+ expect( el.querySelectorAll( 'b' ) ).toHaveLength( 0 );
+ expect( el.textContent ).toBe( '<b>important</b>' );
+ } );
+
+ test( 'suffixes the name field with a colon', () => {
+ renderEntryFieldHTML( el, entry( 'Gift Message' ), 'name' );
+
+ expect( el.textContent ).toBe( 'Gift Message:' );
+ } );
+
+ test( 'writes nothing when there is no usable field value', () => {
+ el.innerHTML = 'untouched';
+
+ renderEntryFieldHTML( el, malformed(), 'value' );
+ renderEntryFieldHTML( el, malformed(), 'name' );
+ renderEntryFieldHTML( el, undefined, 'value' );
+
+ expect( el.innerHTML ).toBe( 'untouched' );
+ } );
+
+ test( 'applies the format callback before writing', () => {
+ renderEntryFieldHTML(
+ el,
+ entry( 'Size', '1 < 2' ),
+ 'value',
+ ( html ) => `${ html }…`
+ );
+
+ expect( el.textContent ).toBe( '1 < 2…' );
+ } );
+} );
diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-cover-item-data-inner-html-path b/plugins/woocommerce/client/blocks/changelog/testops-234-cover-item-data-inner-html-path
new file mode 100644
index 00000000000..352f075bb2c
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/testops-234-cover-item-data-inner-html-path
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Cover the Mini-Cart item data innerHTML rendering path in unit tests.
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
deleted file mode 100644
index 4a2061688b6..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-hidden.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-/**
- * Plugin Name: WooCommerce Blocks Test Item Data Display Hidden
- * Description: Adds a trailing explicitly-hidden item_data entry (or two consecutive ones) to cart items for Mini-Cart e2e tests.
- * Plugin URI: https://github.com/woocommerce/woocommerce
- * Author: WooCommerce
- *
- * @package woocommerce-blocks-test-item-data-display-hidden
- */
-
-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.
- *
- * @internal
- */
- public function __construct() {
- add_action( 'woocommerce_init', array( $this, 'handle_woocommerce_init' ) );
- }
-
- /**
- * Register the item data filter.
- *
- * @internal
- */
- public function handle_woocommerce_init(): void {
- add_filter(
- 'woocommerce_get_item_data',
- array( $this, 'handle_woocommerce_get_item_data' ),
- 10,
- 2
- );
- }
-
- /**
- * Add explicitly hidden item data entries.
- *
- * @internal
- *
- * @param array $item_data Existing item data.
- * @param array $cart_item Cart item data.
- * @return array Filtered item data.
- */
- public function handle_woocommerce_get_item_data( array $item_data, array $cart_item ): array {
- // A well-formed leading entry, so there is always a defined
- // "last visible entry" to check the separator against.
- $item_data[] = array(
- 'key' => 'Gift Message',
- 'value' => 'Happy Birthday!',
- );
-
- // Well-formed (usable key/value) but explicitly hidden.
- $item_data[] = array(
- 'key' => 'Secret',
- 'value' => 'v',
- 'hidden' => true,
- );
-
- // A cart line added with quantity 2 instead ends with two
- // *consecutive* explicitly-hidden entries, so both the
- // single- and double-trailing-hidden scenarios are covered
- // by this one fixture.
- if ( 2 === (int) ( $cart_item['quantity'] ?? 0 ) ) {
- $item_data[] = array(
- 'key' => 'Secret 2',
- 'value' => 'v2',
- 'hidden' => true,
- );
- }
-
- return $item_data;
- }
-}
-
-new Item_Data_Display_Hidden_Test_Fixture();
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
deleted file mode 100644
index 8939373631d..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-malformed.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-/**
- * Plugin Name: WooCommerce Blocks Test Item Data Display Malformed
- * Description: Adds a trailing malformed item_data entry (or two consecutive ones) to cart items for Mini-Cart e2e tests.
- * Plugin URI: https://github.com/woocommerce/woocommerce
- * Author: WooCommerce
- *
- * @package woocommerce-blocks-test-item-data-display-malformed
- */
-
-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.
- *
- * @internal
- */
- public function __construct() {
- add_action( 'woocommerce_init', array( $this, 'handle_woocommerce_init' ) );
- }
-
- /**
- * Register the item data filter.
- *
- * @internal
- */
- public function handle_woocommerce_init(): void {
- add_filter(
- 'woocommerce_get_item_data',
- array( $this, 'handle_woocommerce_get_item_data' ),
- 10,
- 2
- );
- }
-
- /**
- * Add malformed item data entries.
- *
- * @internal
- *
- * @param array $item_data Existing item data.
- * @param array $cart_item Cart item data.
- * @return array Filtered item data.
- */
- public function handle_woocommerce_get_item_data( array $item_data, array $cart_item ): array {
- // A well-formed leading entry, so there is always a defined
- // "last visible entry" to check the separator against.
- $item_data[] = array(
- 'key' => 'Gift Message',
- 'value' => 'Happy Birthday!',
- );
-
- // Malformed: no usable key/attribute/name and no usable
- // display/value.
- $item_data[] = array(
- 'key' => '',
- 'value' => '',
- );
-
- // A cart line added with quantity 2 instead ends with two
- // *consecutive* malformed entries, so both the single- and
- // double-trailing-malformed scenarios are covered by this
- // one fixture.
- if ( 2 === (int) ( $cart_item['quantity'] ?? 0 ) ) {
- $item_data[] = array(
- 'key' => '',
- 'value' => '',
- );
- }
-
- return $item_data;
- }
-}
-
-new Item_Data_Display_Malformed_Test_Fixture();
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
deleted file mode 100644
index 4745a4ffcfd..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/blocks/item-data-display-mixed.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-/**
- * Plugin Name: WooCommerce Blocks Test Item Data Display Mixed
- * Description: Adds a trailing mix of a malformed entry and an explicitly-hidden entry, in both orders, to cart items for Mini-Cart e2e tests.
- * Plugin URI: https://github.com/woocommerce/woocommerce
- * Author: WooCommerce
- *
- * @package woocommerce-blocks-test-item-data-display-mixed
- */
-
-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.
- *
- * @internal
- */
- public function __construct() {
- add_action( 'woocommerce_init', array( $this, 'handle_woocommerce_init' ) );
- }
-
- /**
- * Register the item data filter.
- *
- * @internal
- */
- public function handle_woocommerce_init(): void {
- add_filter(
- 'woocommerce_get_item_data',
- array( $this, 'handle_woocommerce_get_item_data' ),
- 10,
- 2
- );
- }
-
- /**
- * Add mixed malformed and hidden item data entries.
- *
- * @internal
- *
- * @param array $item_data Existing item data.
- * @param array $cart_item Cart item data.
- * @return array Filtered item data.
- */
- public function handle_woocommerce_get_item_data( array $item_data, array $cart_item ): array {
- // A well-formed leading entry, so there is always a defined
- // "last visible entry" to check the separator against.
- $item_data[] = array(
- 'key' => 'Gift Message',
- 'value' => 'Happy Birthday!',
- );
-
- // A cart line added with quantity 2 ends with a hidden
- // entry followed by a malformed one (the reverse order);
- // otherwise (quantity 1) it ends with a malformed entry
- // followed by a hidden one. Both orders are covered by
- // this one fixture.
- if ( 2 === (int) ( $cart_item['quantity'] ?? 0 ) ) {
- $item_data[] = array(
- 'key' => 'Secret',
- 'value' => 'v',
- 'hidden' => true,
- );
- $item_data[] = array(
- 'key' => '',
- 'value' => '',
- );
- } else {
- $item_data[] = array(
- 'key' => '',
- 'value' => '',
- );
- $item_data[] = array(
- 'key' => 'Secret',
- 'value' => 'v',
- 'hidden' => true,
- );
- }
-
- return $item_data;
- }
-}
-
-new Item_Data_Display_Mixed_Test_Fixture();
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
deleted file mode 100644
index 6f452bef48b..00000000000
--- a/plugins/woocommerce/tests/e2e/tests/blocks/mini-cart/mini-cart-item-data.block_theme.spec.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-/**
- * 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 523bc5b1fe7..b48b08352bf 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
@@ -1,7 +1,13 @@
/**
* External dependencies
*/
-import { test, expect, BlockData, wpCLI } from '@woocommerce/e2e-utils';
+import {
+ test,
+ expect,
+ BlockData,
+ enableTestPlugin,
+ wpCLI,
+} from '@woocommerce/e2e-utils';
/**
* Internal dependencies
@@ -188,19 +194,24 @@ test.describe( `${ blockData.name } Block`, () => {
await checkProductLink( page );
} );
- test( 'should render the product table with no client-side error for a product with no item data', async ( {
+ test( 'should render the product table and its item data with no client-side error', async ( {
page,
+ context,
frontendUtils,
miniCartUtils,
} ) => {
+ // The fixture plugin adds `woocommerce_get_item_data` entries to every
+ // cart item, so this test covers the whole wiring: the PHP filter, the
+ // Store API `item_data` field, and the Interactivity API render. How
+ // each entry is normalized is covered by the unit tests for
+ // `mini-cart/utils/item-data`.
+ await enableTestPlugin( context, 'item-data-display' );
+
const pageErrors: string[] = [];
const consoleErrors: string[] = [];
// Register listeners before navigating so nothing that happens during
- // the mini-cart's render is missed. This product has no variation and
- // no `woocommerce_get_item_data` additions (no fixture plugin is
- // activated in this describe block), which is the plain rendering
- // path the item-data/separator getters must not throw on.
+ // the mini-cart's render is missed.
page.on( 'pageerror', ( error ) => {
pageErrors.push( error.message );
} );
@@ -231,6 +242,15 @@ test.describe( `${ blockData.name } Block`, () => {
page.getByLabel( 'Quantity of Polo in your cart.' )
).toHaveValue( '1' );
+ // The filtered entries reach the rendered table.
+ const dialog = page.getByRole( 'dialog' );
+ 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!' ] );
+
expect( pageErrors ).toEqual( [] );
expect( consoleErrors ).toEqual( [] );
} );
@@ -511,15 +531,13 @@ test.describe( `${ blockData.name } Block (variation attributes)`, () => {
},
} );
- test( 'should decode entities in variation attributes rendered via data-wp-text', async ( {
+ test( 'should render variation attributes in the products table', async ( {
page,
frontendUtils,
miniCartUtils,
} ) => {
- // Create a variable product with an attribute value containing an
- // ampersand, which the API returns as "&". The data-wp-text
- // path (textContent) relies on the textarea entity-decode step to
- // display "Red & Blue" instead of literal "Red & Blue".
+ // The API returns the ampersand as "&", so this also checks that
+ // the variation branch of the table decodes it.
const cliOutput = await wpCLI(
`wc product create --user=1 --name="Test Variable" --type="variable" --attributes='${ JSON.stringify(
[
@@ -535,58 +553,24 @@ test.describe( `${ blockData.name } Block (variation attributes)`, () => {
const productId = cliOutput.stdout.match(
/Created product (\d+)/
)?.[ 1 ];
- await wpCLI(
+ const variationOutput = await wpCLI(
`wc product_variation create --user=1 "${ productId }" --regular_price="10" --attributes='${ JSON.stringify(
[ { name: 'Shade', option: 'Red & Blue' } ]
) }'`
);
+ const variationId = variationOutput.stdout.match(
+ /Created product_variation (\d+)/
+ )?.[ 1 ];
- // Navigate to the product page and add the variation to cart.
- await page.goto( `/product/test-variable/` );
- await page
- .getByLabel( 'Shade', { exact: true } )
- .selectOption( 'Red & Blue' );
-
- // The classic variable-product Add to cart button only registers a
- // selection once the variations script has processed it. Until then the
- // hidden `variation_id` input stays at "0" and clicking is a silent
- // no-op (the disabled state is class-based, which Playwright's
- // actionability checks ignore). Wait for the variation to be registered
- // (a positive, non-zero variation id) before clicking.
- // That is necessary but not sufficient: the script sets `variation_id`
- // synchronously and only drops the `disabled` class ~300ms later, from a
- // setTimeout, so wait for the button to be enabled as well. Clicking in
- // that window hits the guard that raises a window.alert Playwright
- // silently dismisses, and the form never submits.
- await expect( page.locator( 'input.variation_id' ) ).toHaveValue(
- /^[1-9][0-9]*$/
+ // Add the variation through the classic add-to-cart URL, since the
+ // product page form is not what this test covers.
+ await page.goto(
+ `/?add-to-cart=${ productId }&variation_id=${ variationId }`
);
- const addToCartButton = page.getByRole( 'button', {
- name: 'Add to cart',
- exact: true,
- } );
- await expect( addToCartButton ).not.toHaveClass( /\bdisabled\b/ );
- await addToCartButton.click();
-
- // Wait for a definitive "added to cart" confirmation before navigating
- // to the shop. The single-product Add to cart is a form submission; if
- // we navigate away before it settles the item may not be in the cart
- // when the mini-cart opens, which is what makes the assertions below
- // race the add-to-cart request. Scope to the success notice (role=alert)
- // so we don't match unrelated page text.
- await expect(
- page.getByRole( 'alert' ).getByText( /added to your cart/i )
- ).toBeVisible();
-
- // Open the mini-cart.
await frontendUtils.goToShop();
await miniCartUtils.openMiniCart();
const dialog = page.getByRole( 'dialog' );
- await expect( dialog ).toBeVisible();
-
- // Variation attributes are rendered via data-wp-text (textContent).
- // The entity "&" should be decoded to "&".
await expect(
dialog.locator( '.wc-block-components-product-details__name' )
).toContainText( [ 'Shade' ] );
diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts b/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts
index 5118ee40dd7..a2f10e3dd4b 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/test-plugins.ts
@@ -16,9 +16,6 @@ import { BASE_URL } from './constants';
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',