Commit 9bd258eb401 for woocommerce

commit 9bd258eb401fb622f4e67099c8f1b5a67d0ae7f6
Author: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
Date:   Fri Mar 20 12:40:19 2026 +0100

    Fix: variable products not working as expected when name is different than slug (#63736)

    * Use name instead of slug in variation selector

    * Match by term slug in cart items

    * Fall back to term name

    * Add stores to `jest.config.json`

    * Fix dropdown

    * Use `attributeNamesMathc

    * Add e2e test

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Update phpstan-baseline file

    * Revert "Update phpstan-baseline file"

    This reverts commit 6e2b45d7b6765f996090512986f5f69169bbeb8d.

    * Fix phpstan

    * Fix error

    * Clean up product and attributes after tests

    * Remove `afterAll`

    * Remove try/catch in e2e test

    * Fix format

    * Try: commenting new test

    * Revert "Try: commenting new test"

    This reverts commit 22efc673438ee9bf078a3191d0e848f310179eb6.

    * Skip test for versions lower than 6.9

    * Revert "Skip test for versions lower than 6.9"

    This reverts commit 4fdd19f43dee92299a505b3f2491b83dd6736796.

    * Calling `store` before accessing `state`

    * Destructure actions in second store call

    * Make e2e test slugs more specific

    Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/63736-fix-find-matching-variations-by-name b/plugins/woocommerce/changelog/63736-fix-find-matching-variations-by-name
new file mode 100644
index 00000000000..c8a191b9106
--- /dev/null
+++ b/plugins/woocommerce/changelog/63736-fix-find-matching-variations-by-name
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix variable products not working when name is different than the slug
\ 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 ecca02fe331..3419d58197a 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
@@ -278,9 +278,13 @@ async function sendCartRequest(

 	return cartQueue.submit( options );
 }
+// Stores are locked to prevent 3PD usage until the API is stable.
+const universalLock =
+	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';

 // Todo: export this store once the store is public.
-const { state, actions } = store< Store >(
+const { state } = store< Store >( 'woocommerce', {}, { lock: universalLock } );
+const { actions } = store< Store >(
 	'woocommerce',
 	{
 		state: {
@@ -796,7 +800,7 @@ const { state, actions } = store< Store >(
 			},
 		},
 	},
-	{ lock: true }
+	{ lock: universalLock }
 );

 // Trigger initial cart refresh.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/product-data.ts b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/product-data.ts
index c2fbf351d8c..0d5f61db701 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/product-data.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/product-data.ts
@@ -23,7 +23,8 @@ const productDataStore = store< {
 	actions: {
 		setVariationId: ( variationId: number | null ) => void;
 	};
-} >(
+} >( 'woocommerce/product-data', {}, { lock: universalLock } );
+store(
 	'woocommerce/product-data',
 	{
 		state: {
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
index c7e6b69a67f..8e4ba562254 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/does-cart-item-match-attributes.ts
@@ -1,16 +1,29 @@
 /**
  * External dependencies
  */
+import { store } from '@wordpress/interactivity';
 import type {
 	OptimisticCartItem,
 	SelectedAttributes,
 } from '@woocommerce/stores/woocommerce/cart';
+import '@woocommerce/stores/woocommerce/products';
+import type { ProductsStore } from '@woocommerce/stores/woocommerce/products';

 /**
  * Internal dependencies
  */
 import { attributeNamesMatch } from './attribute-matching';

+// Stores are locked to prevent 3PD usage until the API is stable.
+const universalLock =
+	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
+
+const { state: productsState } = store< ProductsStore >(
+	'woocommerce/products',
+	{},
+	{ lock: universalLock }
+);
+
 export const doesCartItemMatchAttributes = (
 	cartItem: OptimisticCartItem,
 	selectedAttributes: SelectedAttributes[]
@@ -26,25 +39,24 @@ export const doesCartItemMatchAttributes = (
 		return false;
 	}

-	return cartItem.variation.every(
-		( {
-			attribute,
-			// eslint-disable-next-line
-			raw_attribute,
-			value,
-		}: {
-			attribute: string;
-			raw_attribute: string;
-			value: string;
-		} ) =>
-			selectedAttributes.some( ( item: SelectedAttributes ) => {
-				return (
-					attributeNamesMatch(
-						item.attribute,
-						// It needs to check both because it uses different keys from the same value depending on the context.
-						raw_attribute ?? attribute
-					) && item.value.toLowerCase() === value?.toLowerCase()
-				);
-			} )
+	const parentProductId =
+		productsState.productVariations[ cartItem.id ]?.parent;
+	const productAttributes =
+		productsState.products[ parentProductId ]?.attributes ?? [];
+
+	return cartItem.variation.every( ( { attribute, value: termName } ) =>
+		selectedAttributes.some( ( selectedAttr: SelectedAttributes ) => {
+			// Find the term matching the cart item's value label.
+			const terms = productAttributes.find( ( attr ) =>
+				attributeNamesMatch( attribute, attr.name )
+			)?.terms;
+			const termSlug =
+				terms?.find( ( term ) => term.name === termName )?.slug ||
+				termName; // Fallback to termName if no matching term is found.
+			return (
+				attributeNamesMatch( selectedAttr.attribute, attribute ) &&
+				selectedAttr.value.toLowerCase() === termSlug?.toLowerCase()
+			);
+		} )
 	);
 };
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
index c5d8a449013..e31c6a8746a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
@@ -141,11 +141,12 @@ export type AddToCartWithOptionsStore = {
 	};
 };

-const { actions, state } = store<
+const { state } = store<
 	AddToCartWithOptionsStore &
 		Partial< GroupedProductAddToCartWithOptionsStore > &
 		Partial< VariableProductAddToCartWithOptionsStore >
->(
+>( 'woocommerce/add-to-cart-with-options', {}, { lock: universalLock } );
+const { actions } = store(
 	'woocommerce/add-to-cart-with-options',
 	{
 		state: {
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
index d8854e73079..56b8cebe9d4 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
@@ -376,6 +376,72 @@ test.describe( 'Add to Cart + Options Block', () => {
 		await expect( page.getByText( '1 in cart' ) ).toBeVisible();
 	} );

+	test( 'allows adding variable products with custom attribute slugs', async ( {
+		page,
+		pageObject,
+		editor,
+	} ) => {
+		// Create a global attribute where the slug intentionally differs from the name.
+		const attrOutput = await wpCLI(
+			`wc product_attribute create --name="Taille" --slug="custom-waist" --user=1`
+		);
+		const attrId = attrOutput.stdout.match(
+			/product_attribute\s+(\d+)/
+		)?.[ 1 ];
+
+		// Create terms with custom slugs that also differ from the names.
+		await wpCLI(
+			`wc product_attribute_term create ${ attrId } --name="Petit" --slug="s-m" --user=1`
+		);
+		await wpCLI(
+			`wc product_attribute_term create ${ attrId } --name="Grand" --slug="m-l" --user=1`
+		);
+
+		// Create a variable product using the global attribute.
+		const prodOutput = await wpCLI(
+			`wc product create --user=1 --slug="custom-slug-variable" --name="Custom Slug Variable" --type="variable" --attributes='${ JSON.stringify(
+				[
+					{
+						id: Number( attrId ),
+						visible: true,
+						variation: true,
+						options: [ 'Petit', 'Grand' ],
+					},
+				]
+			) }'`
+		);
+		const productId = prodOutput.stdout.match( /product\s+(\d+)/ )?.[ 1 ];
+
+		// Create a single "Any" variation (empty attributes = matches all terms).
+		await wpCLI(
+			`wc product_variation create "${ productId }" --user=1 --regular_price="19.99" --attributes='[]'`
+		);
+
+		await pageObject.updateSingleProductTemplate();
+
+		await editor.saveSiteEditorEntities( {
+			isOnlyCurrentEntityDirty: true,
+		} );
+
+		await page.goto( '/product/custom-slug-variable/' );
+
+		// Verify the pills show term names (not slugs).
+		const petitOption = page.locator( 'label:has-text("Petit")' );
+		const grandOption = page.locator( 'label:has-text("Grand")' );
+		const addToCartButton = page.getByRole( 'button', {
+			name: 'Add to cart',
+			exact: true,
+		} );
+
+		await expect( petitOption ).not.toBeDisabled();
+		await expect( grandOption ).not.toBeDisabled();
+
+		await petitOption.click();
+		await expect( addToCartButton ).not.toBeDisabled();
+		await addToCartButton.click();
+		await expect( page.getByText( '1 in cart' ) ).toBeVisible();
+	} );
+
 	test( 'allows adding grouped products to cart', async ( {
 		page,
 		pageObject,
diff --git a/plugins/woocommerce/client/blocks/tests/js/jest.config.json b/plugins/woocommerce/client/blocks/tests/js/jest.config.json
index dcd9a683da5..8875d80decf 100644
--- a/plugins/woocommerce/client/blocks/tests/js/jest.config.json
+++ b/plugins/woocommerce/client/blocks/tests/js/jest.config.json
@@ -42,6 +42,7 @@
 		"@woocommerce/utils": "assets/js/utils",
 		"@woocommerce/test-utils/msw": "tests/js/config/msw-setup.js",
 		"@woocommerce/entities": "assets/js/entities",
+		"@woocommerce/stores/(.*)$": "assets/js/base/stores/$1",
 		"^react$": "<rootDir>/node_modules/react",
 		"^react-dom$": "<rootDir>/node_modules/react-dom",
 		"^(.+)/build-module/(.*)$": "$1/build/$2"
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 6de4dd00def..a9a9655d185 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -51657,30 +51657,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeName.php

-		-
-			message: '#^Access to property \$context on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 10
-			path: src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
-
-		-
-			message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\VariationSelectorAttributeOptions\:\:render\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
-
-		-
-			message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\VariationSelectorAttributeOptions\:\:render_dropdown\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
-
-		-
-			message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\VariationSelectorAttributeOptions\:\:render_pills\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WP_Block\.$#'
-			identifier: class.notFound
-			count: 1
-			path: src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts\:\:enqueue_data\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
index b27a5fafa6f..83b1bb31fb9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
@@ -25,9 +25,9 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
 	/**
 	 * Render the block.
 	 *
-	 * @param array    $attributes Block attributes.
-	 * @param string   $content Block content.
-	 * @param WP_Block $block Block instance.
+	 * @param array     $attributes Block attributes.
+	 * @param string    $content Block content.
+	 * @param \WP_Block $block Block instance.
 	 * @return string Rendered block output.
 	 */
 	protected function render( $attributes, $content, $block ): string {
@@ -133,9 +133,9 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
 	/**
 	 * Render the attribute options as pills.
 	 *
-	 * @param array    $attributes Block attributes.
-	 * @param string   $content Block content.
-	 * @param WP_Block $block Block instance.
+	 * @param array     $attributes Block attributes.
+	 * @param string    $content Block content.
+	 * @param \WP_Block $block Block instance.
 	 * @return string The pills.
 	 */
 	protected function render_pills( $attributes, $content, $block ) {
@@ -190,7 +190,7 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
 					'id'              => $attribute_id,
 					'aria-labelledby' => $attribute_id . '_label',
 					'data-wp-context' => array(
-						'name'          => $attribute_slug,
+						'name'          => wc_attribute_label( $block->context['woocommerce/attributeName'] ),
 						'options'       => $attribute_terms,
 						'selectedValue' => $this->get_default_selected_attribute( $attribute_slug, $attribute_terms ),
 						'focused'       => '',
@@ -206,9 +206,9 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
 	/**
 	 * Render the attribute options as a dropdown.
 	 *
-	 * @param array    $attributes Block attributes.
-	 * @param string   $content Block content.
-	 * @param WP_Block $block Block instance.
+	 * @param array     $attributes Block attributes.
+	 * @param string    $content Block content.
+	 * @param \WP_Block $block Block instance.
 	 * @return string The dropdown.
 	 */
 	protected function render_dropdown( $attributes, $content, $block ) {
@@ -264,7 +264,7 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
 					'class'              => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__dropdown',
 					'id'                 => $attribute_id,
 					'data-wp-context'    => array(
-						'name'          => $attribute_slug,
+						'name'          => wc_attribute_label( $block->context['woocommerce/attributeName'] ),
 						'options'       => $attribute_terms,
 						'selectedValue' => $selected_attribute,
 						'autoselect'    => $autoselect,