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,