Commit ece28054960 for woocommerce
commit ece280549604ed50b4ec1ed8a470f17e7ea8691f
Author: Tung Du <dinhtungdu@gmail.com>
Date: Thu Jul 9 08:32:30 2026 +0700
Improve Store API collection-data count tests (#65962)
* test: tighten Store API collection data tests
* fix: unregister globals and clear attribute cache for created attributes during the test
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
index a4deb5e552d..6524ff25242 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
@@ -14,12 +14,21 @@ use Automattic\WooCommerce\Tests\Blocks\Helpers\ValidateSchema;
*/
class ProductCollectionData extends ControllerTestCase {
+ /**
+ * Product attributes created during a test.
+ *
+ * @var array<string,int>
+ */
+ private $created_product_attributes = array();
+
/**
* Setup test product data. Called before every test.
*/
protected function setUp(): void {
parent::setUp();
+ $this->created_product_attributes = array();
+
$fixtures = new FixtureData();
$this->products = array(
@@ -41,6 +50,28 @@ class ProductCollectionData extends ControllerTestCase {
$fixtures->add_product_review( $this->products[1]->get_id(), 4 );
}
+ /**
+ * Cleanup test product data. Called after every test.
+ */
+ protected function tearDown(): void {
+ global $wc_product_attributes;
+
+ foreach ( $this->created_product_attributes as $taxonomy => $attribute_id ) {
+ wc_delete_attribute( $attribute_id );
+
+ if ( taxonomy_exists( $taxonomy ) ) {
+ unregister_taxonomy( $taxonomy );
+ }
+
+ unset( $wc_product_attributes[ $taxonomy ] );
+ }
+
+ delete_transient( 'wc_attribute_taxonomies' );
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
+
+ parent::tearDown();
+ }
+
/**
* Test getting items.
*/
@@ -81,7 +112,7 @@ class ProductCollectionData extends ControllerTestCase {
$product = $fixtures->get_variable_product(
array(),
array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
+ $this->create_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
)
);
$fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
@@ -292,10 +323,8 @@ class ProductCollectionData extends ControllerTestCase {
/**
* @testdox The count array params declare a default maxItems bound to limit query fan-out.
*/
- public function test_count_params_declare_default_max_items() {
- $routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
- $controller = $routes->get( 'product-collection-data' );
- $params = $controller->get_collection_params();
+ public function test_count_params_declare_default_max_items(): void {
+ $params = $this->get_collection_params();
$this->assertArrayHasKey( 'maxItems', $params['calculate_attribute_counts'], 'calculate_attribute_counts must be bounded.' );
$this->assertArrayHasKey( 'maxItems', $params['calculate_taxonomy_counts'], 'calculate_taxonomy_counts must be bounded.' );
@@ -306,8 +335,7 @@ class ProductCollectionData extends ControllerTestCase {
/**
* @testdox An oversized calculate_attribute_counts array is rejected with HTTP 400.
*/
- public function test_calculate_attribute_counts_rejects_oversized_array() {
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
+ public function test_calculate_attribute_counts_rejects_oversized_array(): void {
$too_many = array_fill(
0,
26,
@@ -316,9 +344,12 @@ class ProductCollectionData extends ControllerTestCase {
'query_type' => 'or',
)
);
- $request->set_param( 'calculate_attribute_counts', $too_many );
- $response = rest_get_server()->dispatch( $request );
+ $response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_attribute_counts' => $too_many,
+ )
+ );
$this->assertEquals( 400, $response->get_status(), 'More than 25 attribute-count entries should be rejected.' );
$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
@@ -327,450 +358,463 @@ class ProductCollectionData extends ControllerTestCase {
/**
* @testdox An oversized calculate_taxonomy_counts array is rejected with HTTP 400.
*/
- public function test_calculate_taxonomy_counts_rejects_oversized_array() {
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $too_many = array_fill( 0, 26, 'product_cat' );
- $request->set_param( 'calculate_taxonomy_counts', $too_many );
-
- $response = rest_get_server()->dispatch( $request );
+ public function test_calculate_taxonomy_counts_rejects_oversized_array(): void {
+ $response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_taxonomy_counts' => array_fill( 0, 26, 'product_cat' ),
+ )
+ );
$this->assertEquals( 400, $response->get_status(), 'More than 25 taxonomy-count entries should be rejected.' );
$this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] );
}
/**
- * @testdox An array exactly at the cap is accepted.
+ * @testdox Count arrays exactly at the cap are accepted.
*/
- public function test_calculate_taxonomy_counts_at_cap_is_accepted() {
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $at_cap = array_fill( 0, 25, 'product_cat' );
- $request->set_param( 'calculate_taxonomy_counts', $at_cap );
-
- $response = rest_get_server()->dispatch( $request );
+ public function test_count_arrays_at_cap_are_accepted(): void {
+ $attribute_response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_attribute_counts' => array_fill(
+ 0,
+ 25,
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ )
+ ),
+ )
+ );
+ $taxonomy_response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_taxonomy_counts' => array_fill( 0, 25, 'product_cat' ),
+ )
+ );
- $this->assertEquals( 200, $response->get_status(), 'Exactly 25 entries should be accepted.' );
+ $this->assertEquals( 200, $attribute_response->get_status(), 'Exactly 25 attribute-count entries should be accepted.' );
+ $this->assertEquals( 200, $taxonomy_response->get_status(), 'Exactly 25 taxonomy-count entries should be accepted.' );
}
/**
- * @testdox Repeating the same attribute taxonomy is de-duplicated to a single set of counts.
+ * @testdox Attribute count requests are normalized and deduplicated before filter data is queried.
*/
- public function test_calculate_attribute_counts_deduplicates_taxonomies() {
- $fixtures = new FixtureData();
- $product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
+ public function test_calculate_attribute_counts_normalizes_and_deduplicates_taxonomies(): void {
+ $this->create_size_attribute();
+ $calls = array();
+ $filter = $this->register_filter_data_spy( $calls, array( array( 101 => 2 ) ) );
- $single_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $single_request->set_param(
- 'calculate_attribute_counts',
- array(
+ try {
+ $response = $this->dispatch_collection_data_request(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- )
- );
- $single = rest_get_server()->dispatch( $single_request )->get_data();
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
+ array(
+ 'taxonomy' => ' pa_size ',
+ 'query_type' => 'or',
+ ),
+ array(
+ 'taxonomy' => 'PA_SIZE',
+ 'query_type' => 'or',
+ ),
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
- $repeated_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $repeated_request->set_param(
- 'calculate_attribute_counts',
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
- )
+ ),
+ $calls,
+ 'Text variants of the same attribute taxonomy should fan out to one filter-data call.'
);
- $repeated = rest_get_server()->dispatch( $repeated_request )->get_data();
-
- $this->assertNotEmpty( $single['attribute_counts'], 'Baseline single-taxonomy request should return counts.' );
$this->assertEquals(
- $single['attribute_counts'],
- $repeated['attribute_counts'],
- 'Requesting the same taxonomy multiple times must not duplicate or alter the counts.'
+ array(
+ (object) array(
+ 'term' => 101,
+ 'count' => 2,
+ ),
+ ),
+ $response->get_data()['attribute_counts']
);
}
/**
- * @testdox The same attribute requested with both "or" and "and" query types is counted separately for each type.
+ * @testdox The same attribute requested with both query types is counted once per query type.
*/
- public function test_calculate_attribute_counts_keeps_query_types_separate() {
- $fixtures = new FixtureData();
-
- // Two products with different sizes so that an active filter makes the "or" and "and" counts diverge.
- $large_product = $fixtures->get_variable_product(
- array(),
+ public function test_calculate_attribute_counts_keeps_query_types_separate(): void {
+ $this->create_size_attribute();
+ $calls = array();
+ $filter = $this->register_filter_data_spy(
+ $calls,
array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
+ array( 201 => 3 ),
+ array( 202 => 1 ),
)
);
- $fixtures->get_taxonomy_and_term( $large_product, 'pa_size', 'large', 'large' );
- $small_product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $small_product, 'pa_size', 'small', 'small' );
-
- // Shopper has selected "large". "or" counts ignore that selection (faceted what-if counts) while
- // "and" counts respect it, so the two query types must produce different counts for pa_size.
- $active_filter = array(
- array(
- 'attribute' => 'pa_size',
- 'operator' => 'in',
- 'slug' => array( 'large' ),
- ),
- );
-
- $get_counts = function ( array $entries ) use ( $active_filter ) {
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $request->set_param( 'attributes', $active_filter );
- $request->set_param( 'calculate_attribute_counts', $entries );
-
- return rest_get_server()->dispatch( $request )->get_data()['attribute_counts'];
- };
+ try {
+ $response = $this->dispatch_collection_data_request(
+ array(
+ 'attributes' => array(
+ array(
+ 'attribute' => 'pa_size',
+ 'operator' => 'in',
+ 'slug' => array( 'large' ),
+ ),
+ ),
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'and',
+ ),
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
- $or_only = $get_counts(
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
- )
- );
- $and_only = $get_counts(
- array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'and',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
- )
+ ),
+ $calls,
+ 'The route should keep OR and AND fan-out separate for the same taxonomy.'
);
- $both = $get_counts(
+ $this->assertEquals(
array(
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ (object) array(
+ 'term' => 201,
+ 'count' => 3,
),
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'and',
+ (object) array(
+ 'term' => 202,
+ 'count' => 1,
),
- )
- );
-
- $this->assertNotEmpty( $or_only, 'The "or" request should return counts.' );
- $this->assertNotEmpty( $and_only, 'The "and" request should return counts.' );
- $this->assertNotEquals(
- $or_only,
- $and_only,
- 'For the same taxonomy, "or" and "and" must produce different counts when it is an active filter.'
- );
- $this->assertCount(
- count( $or_only ) + count( $and_only ),
- $both,
- 'Requesting both query types must keep both result sets, not collapse them into one.'
+ ),
+ $response->get_data()['attribute_counts'],
+ 'Both query-type result sets should be returned, not merely the right number of rows.'
);
}
/**
- * @testdox Attribute taxonomies are normalized before dedup so case and whitespace variants collapse to one entry.
+ * @testdox Invalid attribute taxonomies are skipped before filter data is queried.
*/
- public function test_calculate_attribute_counts_normalizes_taxonomy_before_dedup() {
- $fixtures = new FixtureData();
- $product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
+ public function test_calculate_attribute_counts_skips_invalid_taxonomies(): void {
+ $this->create_size_attribute();
+ $calls = array();
+ $filter = $this->register_filter_data_spy( $calls, array( array( 301 => 4 ) ) );
- $single_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $single_request->set_param(
- 'calculate_attribute_counts',
+ try {
+ $response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
+ array(
+ 'taxonomy' => 'product_cat',
+ 'query_type' => 'or',
+ ),
+ array(
+ 'taxonomy' => 'pa_does_not_exist',
+ 'query_type' => 'or',
+ ),
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
- )
+ ),
+ $calls,
+ 'Non-attribute and missing taxonomies should not reach filter data.'
);
- $single = rest_get_server()->dispatch( $single_request )->get_data();
+ }
- $variants_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $variants_request->set_param(
- 'calculate_attribute_counts',
+ /**
+ * @testdox Numeric attribute IDs are resolved before filter data is queried.
+ */
+ public function test_calculate_attribute_counts_accepts_numeric_attribute_id(): void {
+ $attribute_id = $this->create_size_attribute();
+ $calls = array();
+ $filter = $this->register_filter_data_spy( $calls, array( array( 401 => 5 ) ) );
+
+ try {
+ $response = $this->dispatch_collection_data_request(
+ array(
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => (string) $attribute_id,
+ 'query_type' => 'or',
+ ),
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
+ ),
+ $calls,
+ 'Numeric attribute IDs should resolve to the canonical taxonomy name.'
+ );
+ }
+
+ /**
+ * @testdox Taxonomy count requests are normalized and filtered before filter data is queried.
+ */
+ public function test_calculate_taxonomy_counts_normalizes_deduplicates_and_skips_invalid_taxonomies(): void {
+ $calls = array();
+ $filter = $this->register_filter_data_spy( $calls, array( array( 501 => 6 ) ) );
+
+ try {
+ $response = $this->dispatch_collection_data_request(
array(
- 'taxonomy' => ' pa_size ',
- 'query_type' => 'or',
- ),
+ 'calculate_taxonomy_counts' => array(
+ 'product_cat',
+ ' product_cat ',
+ 'PRODUCT_CAT',
+ 'does_not_exist_tax',
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
+ array(
array(
- 'taxonomy' => 'PA_SIZE',
- 'query_type' => 'or',
+ 'type' => 'taxonomy',
+ 'taxonomy' => 'product_cat',
),
- )
+ ),
+ $calls,
+ 'Text variants and missing taxonomies should fan out to one taxonomy filter-data call.'
);
- $variants = rest_get_server()->dispatch( $variants_request )->get_data();
-
- $this->assertNotEmpty( $single['attribute_counts'], 'Baseline single-taxonomy request should return counts.' );
$this->assertEquals(
- $single['attribute_counts'],
- $variants['attribute_counts'],
- 'Case and whitespace variants of the same taxonomy must be counted once, not duplicated.'
+ array(
+ (object) array(
+ 'term' => 501,
+ 'count' => 6,
+ ),
+ ),
+ $response->get_data()['taxonomy_counts']
);
}
/**
- * @testdox Non-attribute and non-existent taxonomies are skipped in calculate_attribute_counts.
+ * @testdox Attribute counts are computed through the filter-data provider path.
*/
- public function test_calculate_attribute_counts_skips_invalid_taxonomies() {
- $fixtures = new FixtureData();
- $product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
+ public function test_calculate_attribute_counts_uses_filter_data_provider(): void {
+ $this->create_size_attribute();
+ $calls = array();
+ $filter = $this->register_filter_data_spy( $calls, array( array( 601 => 7 ) ) );
- $baseline_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $baseline_request->set_param(
- 'calculate_attribute_counts',
- array(
+ try {
+ $response = $this->dispatch_collection_data_request(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- )
- );
- $baseline = rest_get_server()->dispatch( $baseline_request )->get_data();
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
+ ),
+ )
+ );
+ } finally {
+ remove_filter( 'woocommerce_pre_product_filter_data', $filter, 10 );
+ }
- $with_junk_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $with_junk_request->set_param(
- 'calculate_attribute_counts',
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertSame(
array(
array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'type' => 'attribute',
+ 'taxonomy' => 'pa_size',
),
- array(
- // Exists as a taxonomy but is not a product attribute.
- 'taxonomy' => 'product_cat',
- 'query_type' => 'or',
- ),
- array(
- // Not a registered taxonomy at all.
- 'taxonomy' => 'pa_does_not_exist',
- 'query_type' => 'or',
- ),
- )
+ ),
+ $calls,
+ 'Attribute counts should use the shared filter-data provider path.'
);
- $with_junk = rest_get_server()->dispatch( $with_junk_request )->get_data();
-
- $this->assertNotEmpty( $baseline['attribute_counts'], 'Baseline attribute request should return counts.' );
$this->assertEquals(
- $baseline['attribute_counts'],
- $with_junk['attribute_counts'],
- 'Non-attribute and non-existent taxonomies must be skipped, not counted.'
+ array(
+ (object) array(
+ 'term' => 601,
+ 'count' => 7,
+ ),
+ ),
+ $response->get_data()['attribute_counts']
);
}
/**
- * @testdox A numeric attribute ID resolves to the same counts as its taxonomy name.
+ * @testdox Repeated attribute-count requests return stable counts.
*/
- public function test_calculate_attribute_counts_accepts_numeric_attribute_id() {
+ public function test_calculate_attribute_counts_stable_across_repeated_requests(): void {
$fixtures = new FixtureData();
$product = $fixtures->get_variable_product(
array(),
array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
+ $this->create_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
)
);
$fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
- $attribute_id = wc_attribute_taxonomy_id_by_name( 'pa_size' );
- $this->assertNotEmpty( $attribute_id, 'Test attribute should resolve to an ID.' );
-
- $by_name_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $by_name_request->set_param(
- 'calculate_attribute_counts',
+ $first = $this->dispatch_collection_data_request(
array(
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
),
)
- );
- $by_name = rest_get_server()->dispatch( $by_name_request )->get_data();
-
- $by_id_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $by_id_request->set_param(
- 'calculate_attribute_counts',
+ )->get_data();
+ $second = $this->dispatch_collection_data_request(
array(
- array(
- 'taxonomy' => (string) $attribute_id,
- 'query_type' => 'or',
+ 'calculate_attribute_counts' => array(
+ array(
+ 'taxonomy' => 'pa_size',
+ 'query_type' => 'or',
+ ),
),
)
- );
- $by_id = rest_get_server()->dispatch( $by_id_request )->get_data();
+ )->get_data();
- $this->assertNotEmpty( $by_name['attribute_counts'], 'Baseline name-based request should return counts.' );
+ $this->assertNotEmpty( $first['attribute_counts'], 'First request should return counts.' );
$this->assertEquals(
- $by_name['attribute_counts'],
- $by_id['attribute_counts'],
- 'A numeric attribute ID must resolve to the same counts as its taxonomy name.'
+ $first['attribute_counts'],
+ $second['attribute_counts'],
+ 'Repeated identical requests must return identical counts.'
);
}
/**
- * @testdox Taxonomies are normalized before dedup so case and whitespace variants collapse to one entry.
+ * Get collection params for the product collection data route.
+ *
+ * @return array
*/
- public function test_calculate_taxonomy_counts_normalizes_taxonomy_before_dedup() {
- $category = wp_insert_term( 'Normalized Category', 'product_cat' );
- wp_set_post_terms( $this->products[0]->get_id(), array( $category['term_id'] ), 'product_cat' );
-
- $single_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $single_request->set_param( 'calculate_taxonomy_counts', array( 'product_cat' ) );
- $single = rest_get_server()->dispatch( $single_request )->get_data();
-
- $variants_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $variants_request->set_param( 'calculate_taxonomy_counts', array( 'product_cat', ' product_cat ', 'PRODUCT_CAT' ) );
- $variants = rest_get_server()->dispatch( $variants_request )->get_data();
+ private function get_collection_params(): array {
+ $routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
+ $controller = $routes->get( 'product-collection-data' );
- $this->assertNotEmpty( $single['taxonomy_counts'], 'Baseline single-taxonomy request should return counts.' );
- $this->assertEquals(
- $single['taxonomy_counts'],
- $variants['taxonomy_counts'],
- 'Case and whitespace variants of the same taxonomy must be counted once, not duplicated.'
- );
+ return $controller->get_collection_params();
}
/**
- * @testdox Non-existent taxonomies are skipped in calculate_taxonomy_counts.
+ * Dispatch a product collection data request.
+ *
+ * @param array $params Request params.
+ * @return \WP_REST_Response
*/
- public function test_calculate_taxonomy_counts_skips_nonexistent_taxonomies() {
- $category = wp_insert_term( 'Skip Test Category', 'product_cat' );
- wp_set_post_terms( $this->products[0]->get_id(), array( $category['term_id'] ), 'product_cat' );
-
- $baseline_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $baseline_request->set_param( 'calculate_taxonomy_counts', array( 'product_cat' ) );
- $baseline = rest_get_server()->dispatch( $baseline_request )->get_data();
+ private function dispatch_collection_data_request( array $params ): \WP_REST_Response {
+ $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $with_junk_request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $with_junk_request->set_param( 'calculate_taxonomy_counts', array( 'product_cat', 'does_not_exist_tax', 'another_missing_tax' ) );
- $with_junk = rest_get_server()->dispatch( $with_junk_request )->get_data();
+ foreach ( $params as $key => $value ) {
+ $request->set_param( $key, $value );
+ }
- $this->assertNotEmpty( $baseline['taxonomy_counts'], 'Baseline taxonomy request should return counts.' );
- $this->assertEquals(
- $baseline['taxonomy_counts'],
- $with_junk['taxonomy_counts'],
- 'Non-existent taxonomies must be skipped, not counted.'
- );
+ return rest_get_server()->dispatch( $request );
}
/**
- * @testdox Attribute counts are computed through the cached filter-data path.
+ * Create a product attribute taxonomy and track it for cleanup.
+ *
+ * @param string $raw_name Attribute name.
+ * @param array $terms Attribute terms.
+ * @return array Attribute data and created terms.
*/
- public function test_calculate_attribute_counts_uses_filter_data_cache() {
- global $wpdb;
-
- $fixtures = new FixtureData();
- $product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
-
- // Clear any filter-data transients left by other requests so this assertion is isolated. The
- // entry-count counter is excluded so the assertion below proves a real data entry was written.
- $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_wc_filter_data_%' AND option_name <> '_transient_wc_filter_data_entry_count'" );
+ private function create_product_attribute( string $raw_name, array $terms ): array {
+ $attribute_name = wc_sanitize_taxonomy_name( $raw_name );
+ $existing_id = wc_attribute_taxonomy_id_by_name( $attribute_name );
+ $attribute = FixtureData::get_product_attribute( $raw_name, $terms );
+ $attribute_id = (int) $attribute['attribute_id'];
+ $taxonomy = $attribute['attribute_taxonomy'];
+
+ if ( ! $existing_id && $attribute_id ) {
+ $this->created_product_attributes[ $taxonomy ] = $attribute_id;
+ }
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $request->set_param(
- 'calculate_attribute_counts',
- array(
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- )
- );
- $response = rest_get_server()->dispatch( $request );
- $data = $response->get_data();
+ return $attribute;
+ }
- $this->assertEquals( 200, $response->get_status() );
- // The returned counts must be correct, not merely present.
- $this->assertNotEmpty( $data['attribute_counts'], 'Attribute counts should be returned.' );
+ /**
+ * Create the size product attribute taxonomy.
+ *
+ * @return int
+ */
+ private function create_size_attribute(): int {
+ $attribute = $this->create_product_attribute( 'size', array( 'small', 'medium', 'large' ) );
- // A data-cache entry (not just the entry-count counter) must have been written.
- $cached_entries = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_wc_filter_data_%' AND option_name <> '_transient_wc_filter_data_entry_count'" );
- $this->assertGreaterThan(
- 0,
- $cached_entries,
- 'Attribute counts should be written to the shared filter-data cache (proves the cached path is used).'
- );
+ return (int) $attribute['attribute_id'];
}
/**
- * @testdox Repeated attribute-count requests return stable, correct counts (cache does not corrupt results).
+ * Register a filter-data spy that returns canned counts.
+ *
+ * @param array $calls Captured filter-data calls.
+ * @param array $results_by_call Count results keyed by call index.
+ * @return callable
*/
- public function test_calculate_attribute_counts_stable_across_repeated_requests() {
- $fixtures = new FixtureData();
- $product = $fixtures->get_variable_product(
- array(),
- array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $fixtures->get_taxonomy_and_term( $product, 'pa_size', 'large', 'large' );
+ private function register_filter_data_spy( array &$calls, array $results_by_call = array() ): callable {
+ $filter = function ( $pre_filter_counts, $type, $_query_vars, $context ) use ( &$calls, $results_by_call ) {
+ if ( ! in_array( $type, array( 'attribute', 'taxonomy' ), true ) || empty( $context['taxonomy'] ) ) {
+ return $pre_filter_counts;
+ }
- $make_request = function () {
- $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/collection-data' );
- $request->set_param(
- 'calculate_attribute_counts',
- array(
- array(
- 'taxonomy' => 'pa_size',
- 'query_type' => 'or',
- ),
- )
+ $calls[] = array(
+ 'type' => $type,
+ 'taxonomy' => $context['taxonomy'],
);
- return rest_get_server()->dispatch( $request )->get_data();
+ $call_index = count( $calls ) - 1;
+
+ return $results_by_call[ $call_index ] ?? array( 1000 + $call_index => $call_index + 1 );
};
- $first = $make_request();
- $second = $make_request();
+ add_filter( 'woocommerce_pre_product_filter_data', $filter, 10, 4 );
- $this->assertNotEmpty( $first['attribute_counts'], 'First request should return counts.' );
- $this->assertEquals(
- $first['attribute_counts'],
- $second['attribute_counts'],
- 'Repeated identical requests must return identical counts.'
- );
+ return $filter;
}
/**
@@ -781,7 +825,7 @@ class ProductCollectionData extends ControllerTestCase {
$product = $fixtures->get_variable_product(
array(),
array(
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
+ $this->create_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
)
);