Commit a7b729880c for woocommerce
commit a7b729880ca75d98423873b10a7fa2d26adf919c
Author: Luis Herranz <luisherranz@gmail.com>
Date: Fri Feb 20 13:10:00 2026 +0100
Support non-optimistic item count property (#63378)
* Support non-optimistic item count property
* Add changefile(s) from automation for the following project(s): woocommerce
* Fix lint
* Fix e2e tests
* Update plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63378-minicart-support-non-optimistic-items-count b/plugins/woocommerce/changelog/63378-minicart-support-non-optimistic-items-count
new file mode 100644
index 0000000000..3ee4365568
--- /dev/null
+++ b/plugins/woocommerce/changelog/63378-minicart-support-non-optimistic-items-count
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Mini-Cart: use server-computed item count when third-party filters modify `woocommerce_cart_contents_count`.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
index 390eb8f496..891f888c2a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/cart.ts
@@ -35,6 +35,7 @@ export type WooCommerceConfig = {
};
placeholderImgSrc?: string;
currency?: Currency;
+ nonOptimisticProperties?: string[];
};
export type SelectedAttributes = Omit< CartVariationItem, 'raw_attribute' >;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
index a6ac7069e0..2775ee478b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/mini-cart/iapi-frontend.ts
@@ -31,9 +31,11 @@ import { translateJQueryEventToNative } from '../../base/stores/woocommerce/lega
const universalLock =
'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
-const { currency, placeholderImgSrc } = getConfig(
- 'woocommerce'
-) as WooCommerceConfig;
+const {
+ currency,
+ placeholderImgSrc,
+ nonOptimisticProperties = [],
+} = getConfig( 'woocommerce' ) as WooCommerceConfig;
const {
onCartClickBehaviour,
checkoutUrl,
@@ -171,6 +173,9 @@ store< MiniCart >(
{
state: {
get totalItemsInCart() {
+ if ( nonOptimisticProperties.includes( 'cart.items_count' ) ) {
+ return woocommerceState.cart.items_count as number;
+ }
return woocommerceState.cart.items.reduce< number >(
( total, { quantity } ) => total + quantity,
0
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/plugins/cart-contents-count-filter.php b/plugins/woocommerce/client/blocks/tests/e2e/plugins/cart-contents-count-filter.php
new file mode 100644
index 0000000000..36940e5287
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/tests/e2e/plugins/cart-contents-count-filter.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Plugin Name: WooCommerce Blocks Test Cart Contents Count Filter
+ * Description: Overrides the cart contents count to always return 999 for e2e testing.
+ * Plugin URI: https://github.com/woocommerce/woocommerce
+ * Author: WooCommerce
+ *
+ * @package woocommerce-blocks-test-cart-contents-count-filter
+ */
+
+declare( strict_types = 1 );
+
+add_filter(
+ 'woocommerce_cart_contents_count',
+ function () {
+ return 999;
+ }
+);
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/mini-cart/mini-cart.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/mini-cart/mini-cart.block_theme.spec.ts
index 2df2315d0d..3a5b0f435f 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/mini-cart/mini-cart.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/mini-cart/mini-cart.block_theme.spec.ts
@@ -300,4 +300,43 @@ test.describe( `${ blockData.name } Block`, () => {
await page.getByRole( 'link', { name: 'Go to checkout' } ).click();
await expect( page ).toHaveURL( /\/checkout\/?$/ );
} );
+
+ test.describe( 'optimistic updates', () => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(
+ ! config.features[ 'experimental-iapi-mini-cart' ],
+ 'These tests are only relevant for the iAPI mini cart.'
+ );
+
+ test( 'should show the server filtered item count in the mini-cart title', async ( {
+ page,
+ frontendUtils,
+ miniCartUtils,
+ requestUtils,
+ } ) => {
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-cart-contents-count-filter'
+ );
+
+ try {
+ await frontendUtils.goToShop();
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await miniCartUtils.openMiniCart();
+
+ // The filter overrides the count to 999. The mini-cart title should
+ // display this filtered value rather than the actual number of items.
+ const miniCartTitleItemsCounterBlock = page.locator(
+ '[data-block-name="woocommerce/mini-cart-title-items-counter-block"]'
+ );
+ await expect( miniCartTitleItemsCounterBlock ).toBeVisible();
+ await expect( miniCartTitleItemsCounterBlock ).toContainText(
+ '999'
+ );
+ } finally {
+ await requestUtils.deactivatePlugin(
+ 'woocommerce-blocks-test-cart-contents-count-filter'
+ );
+ }
+ } );
+ } );
} );
diff --git a/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php b/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
index 8904a863d7..19d831cf6e 100644
--- a/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
+++ b/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
@@ -24,7 +24,7 @@ class BlocksSharedState {
private static string $consent_statement = 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
/**
- * The namespace for the config.
+ * The namespace for interactivity config and state.
*
* @var string
*/
@@ -113,8 +113,13 @@ class BlocksSharedState {
self::prevent_cache();
}
+ wp_interactivity_config(
+ self::$settings_namespace,
+ array( 'nonOptimisticProperties' => self::get_non_optimistic_properties() )
+ );
+
wp_interactivity_state(
- 'woocommerce',
+ self::$settings_namespace,
array(
'cart' => self::$blocks_shared_cart_state,
'nonce' => wp_create_nonce( 'wc_store_api' ),
@@ -163,6 +168,28 @@ class BlocksSharedState {
);
}
+ /**
+ * Get cart properties that cannot use optimistic UI on the frontend.
+ *
+ * Detects whether third-party code has registered callbacks on filters that
+ * modify cart property values. When callbacks are present, the corresponding
+ * property must use the server-computed value instead of a client-side
+ * optimistic computation.
+ *
+ * `@return` string[] List of cart property paths (dot-delimited) that cannot be optimistic.
+ *
+ * @return string[] List of cart property paths (dot-delimited) that cannot be optimistic.
+ */
+ private static function get_non_optimistic_properties(): array {
+ $properties = array();
+
+ if ( has_filter( 'woocommerce_cart_contents_count' ) ) {
+ $properties[] = 'cart.items_count';
+ }
+
+ return $properties;
+ }
+
/**
* Load placeholder image into interactivity config.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
new file mode 100644
index 0000000000..0e9bef12b0
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
@@ -0,0 +1,104 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Utils;
+
+use Automattic\WooCommerce\Blocks\Utils\BlocksSharedState;
+
+/**
+ * Tests for the BlocksSharedState class.
+ */
+class BlocksSharedStateTest extends \WC_Unit_Test_Case {
+
+ /**
+ * The consent statement required by the private API.
+ *
+ * @var string
+ */
+ private string $consent = 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
+
+ /**
+ * Set up each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->reset_shared_state();
+ }
+
+ /**
+ * Tear down each test.
+ */
+ public function tearDown(): void {
+ remove_all_filters( 'woocommerce_cart_contents_count' );
+ $this->reset_shared_state();
+ parent::tearDown();
+ }
+
+ /**
+ * Reset the static flags and interactivity config so load_store_config()
+ * and load_cart_state() can run again with a clean slate.
+ */
+ private function reset_shared_state(): void {
+ $reflection = new \ReflectionClass( BlocksSharedState::class );
+
+ $prop = $reflection->getProperty( 'core_config_registered' );
+ $prop->setAccessible( true );
+ $prop->setValue( null, false );
+
+ $cart_state = $reflection->getProperty( 'blocks_shared_cart_state' );
+ $cart_state->setAccessible( true );
+ $cart_state->setValue( null, null );
+
+ $interactivity = wp_interactivity();
+ $interactivity_ref = new \ReflectionClass( $interactivity );
+ $config_data = $interactivity_ref->getProperty( 'config_data' );
+
+ $config_data->setAccessible( true );
+ $data = $config_data->getValue( $interactivity );
+ unset( $data['woocommerce'] );
+ $config_data->setValue( $interactivity, $data );
+ }
+
+ /**
+ * @testdox nonOptimisticProperties is empty when no filter is registered.
+ */
+ public function test_no_filter_returns_empty_non_optimistic_properties(): void {
+ BlocksSharedState::load_cart_state( $this->consent );
+
+ $config = wp_interactivity_config( 'woocommerce' );
+
+ $this->assertArrayHasKey( 'nonOptimisticProperties', $config );
+ $this->assertSame( array(), $config['nonOptimisticProperties'] );
+ }
+
+ /**
+ * @testdox nonOptimisticProperties contains items_count when a third-party filter is registered.
+ */
+ public function test_third_party_filter_detected(): void {
+ add_filter( 'woocommerce_cart_contents_count', fn( $count ) => $count + 1 );
+
+ BlocksSharedState::load_cart_state( $this->consent );
+
+ $config = wp_interactivity_config( 'woocommerce' );
+
+ $this->assertArrayHasKey( 'nonOptimisticProperties', $config );
+ $this->assertContains( 'cart.items_count', $config['nonOptimisticProperties'] );
+ }
+
+ /**
+ * @testdox nonOptimisticProperties is empty when a filter is added and then removed.
+ */
+ public function test_filter_added_then_removed_returns_empty(): void {
+ $callback = fn( $count ) => $count + 1;
+
+ add_filter( 'woocommerce_cart_contents_count', $callback );
+ remove_filter( 'woocommerce_cart_contents_count', $callback );
+
+ BlocksSharedState::load_cart_state( $this->consent );
+
+ $config = wp_interactivity_config( 'woocommerce' );
+
+ $this->assertArrayHasKey( 'nonOptimisticProperties', $config );
+ $this->assertSame( array(), $config['nonOptimisticProperties'] );
+ }
+}