Commit 724ad0555ca for woocommerce

commit 724ad0555ca8ea1c66932896f0824a32db2fdc26
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Mar 4 11:42:25 2026 +0100

    Add to Cart + Options: Fix adding variable products to cart with 'any' as an attribute (#63526)

    * Add to Cart + Options: Fix adding variable products to cart with 'any' as an attribute

    * Add changelog file

    * Fix tests

    * Implement CodeRabbit suggestions

diff --git a/plugins/woocommerce/changelog/fix-add-to-cart-with-options-variation-with-any b/plugins/woocommerce/changelog/fix-add-to-cart-with-options-variation-with-any
new file mode 100644
index 00000000000..0717df629f3
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-add-to-cart-with-options-variation-with-any
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Add to Cart + Options: Fix adding variable products to cart with 'any' as an attribute
+
+
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 ba1f5161368..0ca5a694fd3 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
@@ -242,8 +242,7 @@ const doesCartItemMatchAttributes = (
 			selectedAttributes.some( ( item: SelectedAttributes ) => {
 				return (
 					item.attribute === raw_attribute &&
-					( item.value.toLowerCase() === value.toLowerCase() ||
-						( item.value && value === '' ) ) // Handle "any" attribute type
+					item.value.toLowerCase() === value?.toLowerCase()
 				);
 			} )
 	);
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/attribute-matching.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/attribute-matching.ts
index 4daf7b9977a..f824ff9b3f7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/attribute-matching.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/attribute-matching.ts
@@ -90,10 +90,11 @@ export const findMatchingVariation = (
 						).toLowerCase() === attrNameLower
 				);

-				// If variation attribute has empty value, it accepts "Any" value.
-				if ( attr.value === '' ) {
+				// If variation attribute is null, it accepts "Any" value.
+				if ( attr.value === null ) {
 					return (
-						selectedAttr !== undefined && selectedAttr.value !== ''
+						selectedAttr !== undefined &&
+						selectedAttr.value !== null
 					);
 				}

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 e71fe60b53e..d6878faf126 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
@@ -38,8 +38,7 @@ export const doesCartItemMatchAttributes = (
 			selectedAttributes.some( ( item: SelectedAttributes ) => {
 				return (
 					attributeNamesMatch( item.attribute, raw_attribute ) &&
-					( item.value.toLowerCase() === value.toLowerCase() ||
-						( item.value && value === '' ) ) // Handle "any" attribute type
+					item.value.toLowerCase() === value?.toLowerCase()
 				);
 			} )
 	);
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/test/attribute-matching.ts b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/test/attribute-matching.ts
index f3eb3c32459..763800f188e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/test/attribute-matching.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/utils/variations/test/attribute-matching.ts
@@ -157,7 +157,7 @@ describe( 'findMatchingVariation', () => {
 				{
 					id: 201,
 					attributes: [
-						{ name: 'Color', value: '' }, // "Any" color
+						{ name: 'Color', value: null }, // "Any" color
 						{ name: 'Size', value: 'Small' },
 					],
 				},
@@ -165,7 +165,7 @@ describe( 'findMatchingVariation', () => {
 					id: 202,
 					attributes: [
 						{ name: 'Color', value: 'Blue' },
-						{ name: 'Size', value: '' }, // "Any" size
+						{ name: 'Size', value: null }, // "Any" size
 					],
 				},
 			],
@@ -183,9 +183,9 @@ describe( 'findMatchingVariation', () => {
 			expect( result?.id ).toBe( 201 );
 		} );

-		it( 'does not match "Any" attribute when selected value is empty', () => {
+		it( 'does not match "Any" attribute when selected value is null', () => {
 			const selectedAttributes = [
-				{ attribute: 'Color', value: '' },
+				{ attribute: 'Color', value: null },
 				{ attribute: 'Size', value: 'Small' },
 			];
 			expect(
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
index 069a35bcb28..ca6b9c77b23 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
@@ -118,7 +118,7 @@ const isAttributeValueValid = ( {
 		// Skip variations that don't match the current attribute value.
 		if (
 			variationAttrValue !== attributeValue &&
-			variationAttrValue !== '' // "" is used for "any".
+			variationAttrValue !== null // null is used for "any".
 		) {
 			return false;
 		}
@@ -138,11 +138,11 @@ const isAttributeValueValid = ( {
 				) {
 					return true;
 				}
-				// If the current available variation has an empty value
+				// If the current available variation has a null value
 				// (matching any), count it if it refers to a different
 				// attribute or the attribute it refers matches the current
 				// selection.
-				if ( availableVariationAttributeValue === '' ) {
+				if ( availableVariationAttributeValue === null ) {
 					if (
 						! attributeNamesMatch(
 							selectedAttribute.attribute,
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 a9f93a204dd..372deef19ec 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
@@ -329,6 +329,48 @@ test.describe( 'Add to Cart + Options Block', () => {
 		} );
 	} );

+	test( 'allows adding variable products that have "any" as a variation attribute', async ( {
+		page,
+		pageObject,
+		editor,
+	} ) => {
+		await pageObject.updateSingleProductTemplate();
+
+		await editor.saveSiteEditorEntities( {
+			isOnlyCurrentEntityDirty: true,
+		} );
+
+		await page.goto( '/product/v-neck-t-shirt/' );
+
+		// The radio input is visually hidden and, thus, not clickable. That's
+		// why we need to select the <label> instead.
+		const colorBlueOption = page.locator( 'label:has-text("Blue")' );
+		const colorRedOption = page.locator( 'label:has-text("Red")' );
+		const sizeLargeOption = page.locator( 'label:has-text("Large")' );
+
+		await colorBlueOption.click();
+		await sizeLargeOption.click();
+
+		// We use the Add to Cart + Options class to make sure we don't select
+		// the Add to Cart button from the Related Products block.
+		const addToCartButton = page
+			.locator( '.wp-block-add-to-cart-with-options' )
+			.getByRole( 'button', { name: 'Add to cart' } );
+
+		// Note: The button is always enabled for accessibility reasons.
+		// Instead, we check directly for the "disabled" class, which grays
+		// out the button.
+		await expect( addToCartButton ).not.toHaveClass( /\bdisabled\b/ );
+
+		await addToCartButton.click();
+
+		await expect( page.getByText( '1 in cart' ) ).toBeVisible();
+
+		await colorRedOption.click();
+
+		await expect( page.getByText( '1 in cart' ) ).toBeHidden();
+	} );
+
 	test( 'allows adding grouped products to cart', async ( {
 		page,
 		pageObject,