Commit c43d54e61df for woocommerce
commit c43d54e61df9e0b953f706100711f7c4ecd3150b
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Sep 14 14:14:44 2026 +0300
Revert "Preserve local Product Collection constraints in filter counts" (#68692)
Revert "Preserve local Product Collection constraints in filter counts (#68507)"
This reverts commit 0f58c06978eb4f7cfd0089a579568350857ec9d4.
diff --git a/plugins/woocommerce/changelog/fix-product-filter-local-frontend-counts b/plugins/woocommerce/changelog/fix-product-filter-local-frontend-counts
deleted file mode 100644
index 16ba7c306a5..00000000000
--- a/plugins/woocommerce/changelog/fix-product-filter-local-frontend-counts
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Preserve saved local Product Collection constraints when calculating frontend filter counts.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 5c0a47e7b25..ad86d1d556c 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -50956,6 +50956,12 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/ProductCollection/Utils.php
+ -
+ message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCollection\\Utils\:\:remove_empty_array_recursive\(\) has no return type specified\.$#'
+ identifier: missingType.return
+ count: 1
+ path: src/Blocks/BlockTypes/ProductCollection/Utils.php
+
-
message: '#^Parameter \#1 \$block of function build_query_vars_from_query_block expects WP_Block, Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCollection\\WP_Block given\.$#'
identifier: argument.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index e27aca233c0..03240cc519a 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -522,13 +522,11 @@ class QueryBuilder {
// It is necessary explode the value because $attribute_value can be a string with multiple values (e.g. "red,blue").
$attribute_value = explode( ',', $attribute_value );
- $taxonomy = str_replace( AttributeFilter::FILTER_QUERY_VAR_PREFIX, 'pa_', $attribute_name );
- $acc[] = array(
- 'taxonomy' => $taxonomy,
- 'field' => 'slug',
- 'terms' => $attribute_value,
- 'operator' => 'and' === $attribute_query ? 'AND' : 'IN',
- 'filter_taxonomy' => $taxonomy,
+ $acc[] = array(
+ 'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR_PREFIX, 'pa_', $attribute_name ),
+ 'field' => 'slug',
+ 'terms' => $attribute_value,
+ 'operator' => 'and' === $attribute_query ? 'AND' : 'IN',
);
return $acc;
@@ -701,11 +699,10 @@ class QueryBuilder {
}
$tax_queries[] = array(
- 'taxonomy' => $taxonomy_slug,
- 'field' => 'slug',
- 'terms' => $term_slugs,
- 'operator' => 'IN',
- 'filter_taxonomy' => $taxonomy_slug,
+ 'taxonomy' => $taxonomy_slug,
+ 'field' => 'slug',
+ 'terms' => $term_slugs,
+ 'operator' => 'IN',
);
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Utils.php
index 33af697286d..8c23a68e684 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Utils.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Utils.php
@@ -69,7 +69,7 @@ class Utils {
* @return array Returns the constructed WP_Query arguments.
*/
public static function get_query_vars( $block, $page ) {
- if ( ! empty( $block->context['query'] ) && empty( $block->context['query']['inherit'] ) ) {
+ if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
return build_query_vars_from_query_block( $block, $page );
}
@@ -78,8 +78,8 @@ class Utils {
}
/**
- * Remove matching tax or meta query clauses and prune empty groups.
- * Preserve all values in the remaining clauses.
+ * Remove query array from tax or meta query by searching for arrays that
+ * contain exact key => value pair.
*
* @param array $queries tax_query or meta_query.
* @param string $key Array key to search for.
@@ -93,27 +93,16 @@ class Utils {
}
foreach ( $queries as $query_key => $query ) {
- if ( ! is_array( $query ) ) {
- continue;
- }
-
if ( isset( $query[ $key ] ) && $query[ $key ] === $value ) {
unset( $queries[ $query_key ] );
- continue;
- }
-
- // Leaf values can contain arrays and falsy values that must remain unchanged.
- if ( isset( $query['taxonomy'] ) || isset( $query['key'] ) || array_key_exists( 'terms', $query ) || array_key_exists( 'value', $query ) ) {
- continue;
}
- $queries[ $query_key ] = self::remove_query_array( $query, $key, $value );
- if ( empty( $queries[ $query_key ] ) ) {
- unset( $queries[ $query_key ] );
+ if ( isset( $query['relation'] ) || ! isset( $query[ $key ] ) ) {
+ $queries[ $query_key ] = self::remove_query_array( $query, $key, $value );
}
}
- return 1 === count( $queries ) && isset( $queries['relation'] ) ? array() : $queries;
+ return self::remove_empty_array_recursive( $queries );
}
/**
@@ -203,4 +192,19 @@ class Utils {
return $context;
}
+
+ /**
+ * Remove falsy item from array, recursively.
+ *
+ * @param array $array The input array to filter.
+ */
+ private static function remove_empty_array_recursive( $array ) {
+ $array = array_filter( $array );
+ foreach ( $array as $key => $item ) {
+ if ( is_array( $item ) ) {
+ $array[ $key ] = self::remove_empty_array_recursive( $item );
+ }
+ }
+ return $array;
+ }
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
index 7694a8aad28..24f68a915dc 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
@@ -277,8 +277,7 @@ final class ProductFilterAttribute extends AbstractBlock {
* @param string $query_type Query type, accept 'and' or 'or'.
*/
private function get_attribute_counts( $block, $slug, $query_type ) {
- $block_context = $block->context;
- if ( ! isset( $block_context['filterParams'] ) ) {
+ if ( ! isset( $block->context['filterParams'] ) ) {
return array();
}
@@ -295,13 +294,9 @@ final class ProductFilterAttribute extends AbstractBlock {
);
}
- $is_local_collection = true === ( $block_context['query']['isProductCollectionBlock'] ?? false )
- && empty( $block_context['query']['inherit'] );
-
- if ( ! empty( $query_vars['tax_query'] ) && ( ! $is_local_collection || 'and' !== strtolower( $query_type ) ) ) {
- $removal_key = $is_local_collection ? 'filter_taxonomy' : 'taxonomy';
+ if ( ! empty( $query_vars['tax_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
- $query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], $removal_key, $slug );
+ $query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $slug );
}
$container = wc_get_container();
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
index 46a28ba0ae5..9db3982fdd0 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
@@ -367,8 +367,7 @@ final class ProductFilterTaxonomy extends AbstractBlock {
* @return array Term counts with term_id as key and count as value.
*/
private function get_taxonomy_term_counts( $block, $taxonomy ) {
- $block_context = $block->context;
- if ( ! isset( $block_context['filterParams'] ) ) {
+ if ( ! isset( $block->context['filterParams'] ) ) {
return array();
}
@@ -397,13 +396,10 @@ final class ProductFilterTaxonomy extends AbstractBlock {
);
}
- $is_local_collection = true === ( $block_context['query']['isProductCollectionBlock'] ?? false )
- && empty( $block_context['query']['inherit'] );
-
+ // Remove from tax_query if present.
if ( ! empty( $query_vars['tax_query'] ) ) {
- $removal_key = $is_local_collection ? 'filter_taxonomy' : 'taxonomy';
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
- $query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], $removal_key, $taxonomy );
+ $query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $taxonomy );
}
$counts = $container->get( FilterDataProvider::class )->with( $container->get( QueryClauses::class ) )->get_taxonomy_counts( $query_vars, $taxonomy );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters-frontend.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters-frontend.block_theme.spec.ts
index afdb30d3155..9be2b47dd90 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters-frontend.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters-frontend.block_theme.spec.ts
@@ -8,182 +8,11 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
const compiler = await requestUtils.createTemplateFromFile(
'archive-product_attribute-filter'
);
- // eslint-disable-next-line react-hooks/rules-of-hooks -- Playwright's fixture `use`, not a React hook.
await use( compiler );
},
} );
test.describe( 'woocommerce/product-filters - Frontend', () => {
- test( 'preserves saved local categories in frontend filter counts', async ( {
- page,
- requestUtils,
- } ) => {
- const parentCategory = await requestUtils.rest( {
- method: 'POST',
- path: '/wc/v3/products/categories',
- data: { name: 'Collection parent' },
- } );
- const childCategory = await requestUtils.rest( {
- method: 'POST',
- path: '/wc/v3/products/categories',
- data: { name: 'Collection child', parent: parentCategory.id },
- } );
- const outsideCategory = await requestUtils.rest( {
- method: 'POST',
- path: '/wc/v3/products/categories',
- data: { name: 'Outside collection' },
- } );
-
- for ( const fixture of [
- {
- name: 'Direct parent product',
- category: parentCategory.id,
- price: '17',
- rating: 2,
- },
- {
- name: 'Child-only product',
- category: childCategory.id,
- price: '93',
- rating: 5,
- },
- {
- name: 'Outside product',
- category: outsideCategory.id,
- price: '93',
- rating: 4,
- },
- ] ) {
- const product = await requestUtils.rest( {
- method: 'POST',
- path: '/wc/v3/products',
- data: {
- name: fixture.name,
- type: 'simple',
- status: 'publish',
- regular_price: fixture.price,
- categories: [ { id: fixture.category } ],
- },
- } );
- await requestUtils.rest( {
- method: 'POST',
- path: '/wc/v3/products/reviews',
- data: {
- product_id: product.id,
- review: `Review for ${ fixture.name }`,
- reviewer: 'Test shopper',
- reviewer_email: 'shopper@example.com',
- rating: fixture.rating,
- status: 'approved',
- },
- } );
- }
-
- const collectionAttributes = JSON.stringify( {
- queryId: 1,
- query: {
- perPage: 9,
- pages: 0,
- offset: 0,
- postType: 'product',
- order: 'asc',
- orderBy: 'title',
- inherit: false,
- filterable: true,
- isProductCollectionBlock: true,
- taxQuery: { product_cat: [ parentCategory.id ] },
- },
- displayLayout: { type: 'flex', columns: 3 },
- } );
- const post = await requestUtils.rest( {
- method: 'POST',
- path: '/wp/v2/posts',
- data: {
- title: 'Local collection filter counts',
- status: 'publish',
- content: `<!-- wp:woocommerce/product-collection ${ collectionAttributes } -->
-<div class="wp-block-woocommerce-product-collection">
- <!-- wp:woocommerce/product-filters {"overlayMode":"off"} -->
- <div class="wp-block-woocommerce-product-filters wc-block-product-filters">
- <!-- wp:woocommerce/product-filter-taxonomy {"taxonomy":"product_cat","showCounts":true,"hideEmpty":true} -->
- <div class="wp-block-woocommerce-product-filter-taxonomy">
- <!-- wp:woocommerce/product-filter-checkbox-list -->
- <div class="wp-block-woocommerce-product-filter-checkbox-list wc-block-product-filter-checkbox-list"></div>
- <!-- /wp:woocommerce/product-filter-checkbox-list -->
- </div>
- <!-- /wp:woocommerce/product-filter-taxonomy -->
- <!-- wp:woocommerce/product-filter-rating {"showCounts":true} -->
- <div class="wp-block-woocommerce-product-filter-rating">
- <!-- wp:woocommerce/product-filter-checkbox-list -->
- <div class="wp-block-woocommerce-product-filter-checkbox-list wc-block-product-filter-checkbox-list"></div>
- <!-- /wp:woocommerce/product-filter-checkbox-list -->
- </div>
- <!-- /wp:woocommerce/product-filter-rating -->
- </div>
- <!-- /wp:woocommerce/product-filters -->
- <!-- wp:woocommerce/product-template -->
- <!-- wp:post-title {"level":3,"isLink":true,"__woocommerceNamespace":"woocommerce/product-collection/product-title"} /-->
- <!-- /wp:woocommerce/product-template -->
-</div>
-<!-- /wp:woocommerce/product-collection -->`,
- },
- } );
-
- await page.goto( post.link );
-
- const collection = page.locator(
- '.wp-block-woocommerce-product-collection'
- );
- const productTitles = collection.locator( '.wp-block-post-title' );
- const categoryFilter = collection.locator(
- '.wp-block-woocommerce-product-filter-taxonomy'
- );
- const parentCheckbox = categoryFilter.getByRole( 'checkbox', {
- name: /Collection parent/,
- } );
- const ratingFilter = collection.locator(
- '.wp-block-woocommerce-product-filter-rating'
- );
-
- await expect( productTitles ).toHaveText( [ 'Direct parent product' ] );
- await expect( parentCheckbox ).toBeVisible();
- await expect( categoryFilter.getByRole( 'checkbox' ) ).toHaveCount( 1 );
- await expect(
- categoryFilter.getByText( 'Collection child', { exact: true } )
- ).toHaveCount( 0 );
- await expect(
- categoryFilter.getByText( 'Outside collection', { exact: true } )
- ).toHaveCount( 0 );
- await expect(
- categoryFilter.locator(
- '.wc-block-product-filter-checkbox-list__count'
- )
- ).toHaveText( '(1)' );
- await expect(
- ratingFilter.getByRole( 'checkbox', { name: 'Rated 2 out of 5' } )
- ).toBeVisible();
- await expect( ratingFilter.getByRole( 'checkbox' ) ).toHaveCount( 1 );
-
- await parentCheckbox.check();
- await page.waitForURL(
- ( url ) =>
- url.searchParams.get( 'categories' ) === parentCategory.slug
- );
-
- await expect( parentCheckbox ).toBeChecked();
- await expect( productTitles ).toHaveText( [ 'Direct parent product' ] );
- await expect( categoryFilter.getByRole( 'checkbox' ) ).toHaveCount( 1 );
- await expect(
- categoryFilter.locator(
- '.wc-block-product-filter-checkbox-list__count'
- )
- ).toHaveText( '(1)' );
- await expect(
- ratingFilter.getByRole( 'checkbox', { name: 'Rated 2 out of 5' } )
- ).toBeVisible();
- await expect( ratingFilter.getByRole( 'checkbox' ) ).toHaveCount( 1 );
- } );
-
test.describe( 'Overlay', () => {
test.beforeEach( async ( { templateCompiler, page } ) => {
await templateCompiler.compile( {
@@ -261,6 +90,7 @@ test.describe( 'woocommerce/product-filters - Frontend', () => {
} );
// Skipping these tests until we can move this block to @wordpress/interactivity.
+ // eslint-disable-next-line playwright/no-skipped-test
test.skip( 'filter is working inside overlay', async ( { page } ) => {
await page.setViewportSize( { width: 400, height: 600 } );
await page.goto( '/shop' );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 397c25dac47..0728e2bd3cc 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -395,7 +395,7 @@ class QueryBuilder extends \WP_UnitTestCase {
}
/**
- * @testdox Test merging filter by stock status queries.
+ * Test merging filter by stock status queries.
*/
public function test_merging_filter_by_attribute_queries() {
// Mock the attribute data.
@@ -439,22 +439,20 @@ class QueryBuilder extends \WP_UnitTestCase {
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'pa_color',
- 'taxonomy' => 'pa_color',
- 'field' => 'slug',
- 'terms' => array( 'blue' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'pa_color',
+ 'field' => 'slug',
+ 'terms' => array( 'blue' ),
+ 'operator' => 'IN',
),
$attribute_queries
);
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'pa_size',
- 'taxonomy' => 'pa_size',
- 'field' => 'slug',
- 'terms' => array( 'xl', 'xxl' ),
- 'operator' => 'AND',
+ 'taxonomy' => 'pa_size',
+ 'field' => 'slug',
+ 'terms' => array( 'xl', 'xxl' ),
+ 'operator' => 'AND',
),
$attribute_queries
);
@@ -1127,7 +1125,7 @@ class QueryBuilder extends \WP_UnitTestCase {
}
/**
- * @testdox Test merging filter queries by Category Slug (e.g. ?categories=accessories).
+ * Test merging filter queries by Category Slug (e.g. ?categories=accessories).
*/
public function test_merging_filter_by_category_slug() {
// Set the URL query variables.
@@ -1140,11 +1138,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Assertions.
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'product_cat',
- 'taxonomy' => 'product_cat',
- 'field' => 'slug',
- 'terms' => array( 'accessories' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'accessories' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should contain correct product_cat tax query using slug.'
@@ -1155,7 +1152,7 @@ class QueryBuilder extends \WP_UnitTestCase {
}
/**
- * @testdox Test merging filter queries specifically for Tags.
+ * Test merging filter queries specifically for Tags.
* Scenario: ?tags=tag-new (Slug)
*/
public function test_merging_filter_by_tags() {
@@ -1169,11 +1166,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Assertions.
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'product_tag',
- 'taxonomy' => 'product_tag',
- 'field' => 'slug',
- 'terms' => array( 'tag-new' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_tag',
+ 'field' => 'slug',
+ 'terms' => array( 'tag-new' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should contain correct product_tag tax query with IN operator.'
@@ -1184,7 +1180,7 @@ class QueryBuilder extends \WP_UnitTestCase {
}
/**
- * @testdox Test merging filter queries for Categories, Tags, and Brands simultaneously.
+ * Test merging filter queries for Categories, Tags, and Brands simultaneously.
* Scenario: ?categories=accessories&tags=tag-new&brands=nike
*/
public function test_merging_filter_by_all_taxonomies_together() {
@@ -1201,11 +1197,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Verify Category.
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'product_cat',
- 'taxonomy' => 'product_cat',
- 'field' => 'slug',
- 'terms' => array( 'accessories' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'accessories' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should contain correct product_cat tax query.'
@@ -1214,11 +1209,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Verify Tag.
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'product_tag',
- 'taxonomy' => 'product_tag',
- 'field' => 'slug',
- 'terms' => array( 'tag-new' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_tag',
+ 'field' => 'slug',
+ 'terms' => array( 'tag-new' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should contain correct product_tag tax query.'
@@ -1227,11 +1221,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Verify Brand.
$this->assertContainsEquals(
array(
- 'filter_taxonomy' => 'product_brand',
- 'taxonomy' => 'product_brand',
- 'field' => 'slug',
- 'terms' => array( 'nike' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_brand',
+ 'field' => 'slug',
+ 'terms' => array( 'nike' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should contain correct product_brand tax query.'
@@ -1244,7 +1237,7 @@ class QueryBuilder extends \WP_UnitTestCase {
}
/**
- * @testdox Test that the strictly string-based filter logic works and SAFELY ignores arrays.
+ * Test that the strictly string-based filter logic works and SAFELY ignores arrays.
* Matches logic: if ( ! is_string($param_value) ) continue;
*/
public function test_filter_strict_string_handling() {
@@ -1260,11 +1253,10 @@ class QueryBuilder extends \WP_UnitTestCase {
// Assertion: The array input should have been ignored.
$this->assertNotContainsEquals(
array(
- 'filter_taxonomy' => 'product_cat',
- 'taxonomy' => 'product_cat',
- 'field' => 'slug',
- 'terms' => array( 'hats' ),
- 'operator' => 'IN',
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'hats' ),
+ 'operator' => 'IN',
),
$filter_clauses,
'Should not contain product_cat tax query because array input should be ignored.'
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/UtilsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/UtilsTest.php
deleted file mode 100644
index 79cf80976f7..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/UtilsTest.php
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\ProductCollection;
-
-use Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection\Utils as ProductCollectionUtils;
-use WC_Unit_Test_Case;
-use WP_Block;
-
-/**
- * Tests for Product Collection query utilities.
- */
-class UtilsTest extends WC_Unit_Test_Case {
-
- /**
- * @testdox Should remove nested matching clauses without changing saved query values.
- */
- public function test_removes_nested_filters_without_changing_saved_constraints(): void {
- $saved_taxonomy = array(
- 'taxonomy' => 'product_cat',
- 'terms' => array( 12, 0 ),
- 'include_children' => false,
- );
- $saved_meta = array(
- 'key' => '_price',
- 'value' => 0,
- 'compare' => '>=',
- );
- $queries = array(
- 'relation' => 'AND',
- 'saved' => $saved_taxonomy,
- 'price' => $saved_meta,
- 'shopper' => array(
- 'relation' => 'OR',
- 'other' => array(
- 'taxonomy' => 'product_tag',
- 'terms' => array( 23 ),
- ),
- 'rating' => array(
- 'taxonomy' => 'product_visibility',
- 'terms' => array( 34 ),
- 'rating_filter' => true,
- ),
- ),
- );
-
- $result = ProductCollectionUtils::remove_query_array( $queries, 'rating_filter', true );
-
- $this->assertArrayNotHasKey( 'rating', $result['shopper'] );
- $this->assertSame( $saved_taxonomy, $result['saved'], 'Saved taxonomy values, including false and zero, must remain exact.' );
- $this->assertSame( $saved_meta, $result['price'], 'Zero-valued price constraints must not change.' );
- $this->assertSame( 'OR', $result['shopper']['relation'] );
- $this->assertSame( array( 23 ), $result['shopper']['other']['terms'] );
- $this->assertSame( 'AND', $result['relation'] );
- }
-
- /**
- * @testdox Should prune groups after removing their last matching clause.
- */
- public function test_prunes_empty_query_groups(): void {
- $queries = array(
- 'relation' => 'AND',
- array(
- 'relation' => 'OR',
- array(
- 'taxonomy' => 'product_cat',
- 'terms' => array( 12 ),
- ),
- ),
- );
-
- $result = ProductCollectionUtils::remove_query_array( $queries, 'taxonomy', 'product_cat' );
-
- $this->assertSame( array(), $result );
- }
-
- /**
- * @testdox Should preserve empty term lists and falsy values in untouched clauses.
- */
- public function test_preserves_untouched_leaf_values(): void {
- $queries = array(
- array(
- 'taxonomy' => 'product_cat',
- 'terms' => array(),
- 'include_children' => false,
- ),
- array(
- 'key' => '_custom_value',
- 'value' => array( false, 0, '0', '', null ),
- ),
- );
-
- $result = ProductCollectionUtils::remove_query_array( $queries, 'rating_filter', true );
-
- $this->assertSame( $queries, $result );
- }
-
- /**
- * @testdox Should preserve empty term lists in clauses without a taxonomy.
- */
- public function test_preserves_term_taxonomy_id_clauses_without_taxonomy(): void {
- $queries = array(
- array(
- 'field' => 'term_taxonomy_id',
- 'terms' => array(),
- ),
- );
-
- $result = ProductCollectionUtils::remove_query_array( $queries, 'rating_filter', true );
-
- $this->assertSame( $queries, $result );
- }
-
- /**
- * @testdox Should match filter values strictly, including false.
- */
- public function test_matches_filter_values_strictly(): void {
- $queries = array(
- array(
- 'rating_filter' => true,
- 'taxonomy' => 'product_visibility',
- ),
- array(
- 'rating_filter' => false,
- 'taxonomy' => 'product_visibility',
- ),
- array(
- 'rating_filter' => 0,
- 'taxonomy' => 'product_visibility',
- ),
- );
-
- $result = ProductCollectionUtils::remove_query_array( $queries, 'rating_filter', false );
-
- $this->assertArrayNotHasKey( 1, $result );
- $this->assertTrue( $result[0]['rating_filter'] );
- $this->assertArrayHasKey( 'rating_filter', $result[2] );
- $this->assertSame( 0, $result[2]['rating_filter'] );
- }
-
- /**
- * @testdox Should build query vars from the block unless the collection inherits the global query.
- * @testWith [{"postType": "product", "inherit": false}, "block"]
- * [{"postType": "product"}, "block"]
- * [{"postType": "product", "inherit": true}, "global"]
- * [null, "global"]
- *
- * @param array|null $query_context Query block context, or null when the block has none.
- * @param string $expected_source Which query the vars must come from.
- */
- public function test_get_query_vars_uses_global_query_only_when_inherited( ?array $query_context, string $expected_source ): void {
- global $wp_query;
- $wp_query->query_vars = array( 'source' => 'global' );
- add_filter(
- 'query_loop_block_query_vars',
- static function () {
- return array( 'source' => 'block' );
- },
- PHP_INT_MAX
- );
-
- $block = new WP_Block(
- array( 'blockName' => 'core/post-template' ),
- null === $query_context ? array() : array( 'query' => $query_context )
- );
-
- $query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
-
- $this->assertSame( $expected_source, $query_vars['source'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterCollectionCountsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterCollectionCountsTest.php
deleted file mode 100644
index 59ed0c362bf..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterCollectionCountsTest.php
+++ /dev/null
@@ -1,196 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
-
-use Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection\QueryBuilder;
-use WC_Unit_Test_Case;
-use WP_Block;
-
-/**
- * Tests the query passed from Product Filter blocks to their count provider.
- */
-class ProductFilterCollectionCountsTest extends WC_Unit_Test_Case {
-
- /**
- * Restore the registered attribute taxonomy, including when fixture creation fails.
- */
- public function tearDown(): void {
- if ( taxonomy_exists( 'pa_material' ) ) {
- unregister_taxonomy( 'pa_material' );
- }
- parent::tearDown();
- }
-
- /**
- * @testdox Should retain saved taxonomy constraints only for explicitly local Product Collections.
- * @testWith ["product_cat", "local", "or", true, 1]
- * ["product_cat", "local", "or", false, 1]
- * ["product_cat", "local-without-inherit", "or", true, 1]
- * ["product_cat", "inherited", "or", true, 0]
- * ["product_cat", "standalone", "or", true, 0]
- * ["product_cat", "generic", "or", true, 0]
- * ["product_cat", "generic-without-marker", "or", true, 0]
- * ["pa_material", "local", "or", true, 1]
- * ["pa_material", "local", "or", false, 1]
- * ["pa_material", "local", "and", true, 2]
- * ["pa_material", "local", "AND", true, 2]
- * ["pa_material", "local-without-inherit", "or", true, 1]
- * ["pa_material", "local-without-inherit", "and", true, 2]
- * ["pa_material", "inherited", "or", true, 0]
- * ["pa_material", "standalone", "and", true, 0]
- * ["pa_material", "generic", "or", true, 0]
- * ["pa_material", "generic-without-marker", "or", true, 0]
- *
- * @param string $taxonomy Taxonomy whose counts are requested.
- * @param string $context_type Collection context to render in.
- * @param string $query_type Shopper attribute operator.
- * @param bool $has_selection Whether the shopper selected a term.
- * @param int $expected_clause_count Number of own-taxonomy constraints retained for counts.
- */
- public function test_count_query_preserves_local_collection_boundary( string $taxonomy, string $context_type, string $query_type, bool $has_selection, int $expected_clause_count ): void {
- $is_attribute = 'pa_material' === $taxonomy;
- $is_local = in_array( $context_type, array( 'local', 'local-without-inherit' ), true );
- $attributes = array(
- 'taxonomy' => $taxonomy,
- 'sortOrder' => 'name-asc',
- );
-
- if ( $is_attribute ) {
- $attribute_id = wc_create_attribute(
- array(
- 'name' => 'Material',
- 'slug' => 'material',
- )
- );
- $this->assertIsInt( $attribute_id );
- register_taxonomy( 'pa_material', array( 'product' ) );
- $attributes = array(
- 'attributeId' => $attribute_id,
- 'queryType' => $query_type,
- 'sortOrder' => 'name-asc',
- 'hideEmpty' => false,
- );
- }
-
- $own_param = $is_attribute ? 'filter_material' : 'categories';
- set_query_var( $own_param, $has_selection ? 'shopper-selection' : '' );
- set_query_var( 'query_type_material', $query_type );
- set_query_var( 'tags', 'other-facet' );
-
- $query_block = new WP_Block(
- array( 'blockName' => 'core/post-template' ),
- array(
- 'query' => array(
- 'postType' => 'product',
- 'taxQuery' => array( $taxonomy => array( 101, 102 ) ),
- ),
- )
- );
- $saved_query = build_query_vars_from_query_block( $query_block, 1 );
- $saved_clauses = self::get_taxonomy_clauses( $saved_query['tax_query'], $taxonomy );
- $this->assertCount( 1, $saved_clauses, 'WordPress must build the saved taxonomy constraint.' );
- $saved_clause = $saved_clauses[0];
- $this->assertSame( array( 101, 102 ), $saved_clause['terms'] );
-
- $builder = new QueryBuilder();
- $query_vars = $builder->get_final_frontend_query( array( 'name' => '' ), $saved_query );
-
- $query_vars[ $own_param ] = $has_selection ? 'shopper-selection' : '';
- $query_vars['query_type_material'] = $query_type;
- $query_vars['tags'] = 'other-facet';
-
- $initial_clauses = self::get_taxonomy_clauses( $query_vars['tax_query'], $taxonomy );
- $this->assertCount( $has_selection ? 2 : 1, $initial_clauses, 'Fixture must combine distinct saved and shopper constraints.' );
- $this->assertContains( $saved_clause, $initial_clauses );
-
- $query_context = array(
- 'postType' => 'product',
- 'inherit' => 'inherited' === $context_type,
- 'isProductCollectionBlock' => 'generic' !== $context_type,
- );
- if ( 'generic-without-marker' === $context_type ) {
- unset( $query_context['isProductCollectionBlock'] );
- }
- if ( 'local-without-inherit' === $context_type ) {
- unset( $query_context['inherit'] );
- }
- $context = array( 'filterParams' => array() );
- if ( 'standalone' !== $context_type ) {
- $context['query'] = $query_context;
- }
-
- global $wp_query;
- $wp_query->query_vars = $query_vars;
- add_filter(
- 'query_loop_block_query_vars',
- static function () use ( $query_vars ) {
- return $query_vars;
- },
- PHP_INT_MAX
- );
-
- $captured_query = null;
- add_filter(
- 'woocommerce_pre_product_filter_data',
- static function ( $counts, $filter_type, $count_query ) use ( &$captured_query, $is_attribute ) {
- if ( ( $is_attribute ? 'attribute' : 'taxonomy' ) === $filter_type ) {
- $captured_query = $count_query;
- return array();
- }
- return $counts;
- },
- 10,
- 3
- );
-
- $block = new WP_Block(
- array(
- 'blockName' => $is_attribute ? 'woocommerce/product-filter-attribute' : 'woocommerce/product-filter-taxonomy',
- 'attrs' => $attributes,
- 'innerBlocks' => array(),
- 'innerHTML' => '',
- 'innerContent' => array(),
- ),
- $context
- );
- $block->render();
-
- $this->assertIsArray( $captured_query, 'Rendering must reach the actual count provider.' );
- $own_clauses = self::get_taxonomy_clauses( $captured_query['tax_query'], $taxonomy );
- $this->assertCount( $expected_clause_count, $own_clauses, 'Local counts retain saved constraints, plus shopper AND selections.' );
- if ( $is_local ) {
- $this->assertContains( $saved_clause, $own_clauses, 'The retained clause must be the saved boundary, not the shopper selection.' );
- }
- $other_clauses = self::get_taxonomy_clauses( $captured_query['tax_query'], 'product_tag' );
- $this->assertCount( 1, $other_clauses, 'The other facet selection must remain applied.' );
- $this->assertSame( array( 'other-facet' ), $other_clauses[0]['terms'] );
- if ( $is_attribute && 'and' === strtolower( $query_type ) ) {
- $this->assertSame( 'shopper-selection', $captured_query[ $own_param ], 'AND selections must still constrain counts through QueryClauses.' );
- } else {
- $this->assertArrayNotHasKey( $own_param, $captured_query, 'OR counts must exclude the current shopper selection.' );
- }
- }
-
- /**
- * Collect the leaf clauses for one taxonomy without depending on group depth.
- *
- * @param array $queries Taxonomy query groups.
- * @param string $taxonomy Taxonomy to collect.
- * @return array
- */
- private static function get_taxonomy_clauses( array $queries, string $taxonomy ): array {
- $clauses = array();
- foreach ( $queries as $query ) {
- if ( ! is_array( $query ) ) {
- continue;
- }
- if ( ( $query['taxonomy'] ?? null ) === $taxonomy ) {
- $clauses[] = $query;
- } else {
- $clauses = array_merge( $clauses, self::get_taxonomy_clauses( $query, $taxonomy ) );
- }
- }
- return $clauses;
- }
-}