diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index a13b1aa2..fccb726a 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -61,6 +61,7 @@ public function init() { Type\WPInputObject\Product_Taxonomy_Input::register(); Type\WPInputObject\Orderby_Inputs::register(); Type\WPInputObject\Collection_Stats_Query_Input::register(); + Type\WPInputObject\Collection_Stats_Where_Args::register(); /** * Interfaces. diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index 5d5b9966..7cb2fbcf 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -295,6 +295,7 @@ private function includes() { require $include_directory_path . 'type/input/class-shipping-line-input.php'; require $include_directory_path . 'type/input/class-tax-rate-connection-orderby-input.php'; require $include_directory_path . 'type/input/class-collection-stats-query-input.php'; + require $include_directory_path . 'type/input/class-collection-stats-where-args.php'; // Include mutation type class files. require $include_directory_path . 'mutation/class-cart-add-fee.php'; diff --git a/includes/connection/class-products.php b/includes/connection/class-products.php index 555d94e7..951ee44f 100644 --- a/includes/connection/class-products.php +++ b/includes/connection/class-products.php @@ -427,6 +427,10 @@ public static function get_connection_args( $extra_args = [] ): array { 'type' => 'Boolean', 'description' => __( 'Include variations in the result set.', 'wp-graphql-woocommerce' ), ], + 'rating' => [ + 'type' => [ 'list_of' => 'Integer' ], + 'description' => __( 'Limit result set to products with a specific average rating. Must be between 1 and 5', 'wp-graphql-woocommerce' ), + ], ]; if ( wc_tax_enabled() ) { diff --git a/includes/data/connection/class-product-connection-resolver.php b/includes/data/connection/class-product-connection-resolver.php index ba233bf5..3bf45e0f 100644 --- a/includes/data/connection/class-product-connection-resolver.php +++ b/includes/data/connection/class-product-connection-resolver.php @@ -466,6 +466,18 @@ public function sanitize_input_fields( array $where_args ) { }//end switch }//end if + if ( ! empty( $where_args['rating'] ) ) { + $rating_terms = []; + foreach ( $rating as $value ) { + $rating_terms[] = 'rated-' . $value; + } + $tax_query[] = [ + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => $rating_terms, + ]; + } + // Process "taxonomyFilter". $tax_filter_query = []; if ( ! empty( $where_args['taxonomyFilter'] ) ) { @@ -563,6 +575,8 @@ public function sanitize_input_fields( array $where_args ) { $query_args[ $on_sale_key ] = $on_sale_ids; } + + /** * {@inheritDoc} */ diff --git a/includes/type/input/class-collection-stats-where-args.php b/includes/type/input/class-collection-stats-where-args.php new file mode 100644 index 00000000..25965f4c --- /dev/null +++ b/includes/type/input/class-collection-stats-where-args.php @@ -0,0 +1,110 @@ + __( 'Arguments used to filter the collection results', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'search' => [ + 'type' => 'String', + 'description' => __( 'Limit result set to products based on a keyword search.', 'wp-graphql-woocommerce' ), + ], + 'slugIn' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Limit result set to products with specific slugs.', 'wp-graphql-woocommerce' ), + ], + 'typeIn' => [ + 'type' => [ 'list_of' => 'ProductTypesEnum' ], + 'description' => __( 'Limit result set to products assigned to a group of specific types.', 'wp-graphql-woocommerce' ), + ], + 'exclude' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Ensure result set excludes specific IDs.', 'wp-graphql-woocommerce' ), + ], + 'include' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Limit result set to specific ids.', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'wp-graphql-woocommerce' ), + ], + 'featured' => [ + 'type' => 'Boolean', + 'description' => __( 'Limit result set to featured products.', 'wp-graphql-woocommerce' ), + ], + 'parentIn' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Specify objects whose parent is in an array.', 'wp-graphql-woocommerce' ), + ], + 'parentNotIn' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Specify objects whose parent is not in an array.', 'wp-graphql-woocommerce' ), + ], + 'categoryIn' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Limit result set to products assigned to a group of specific categories by name.', 'wp-graphql-woocommerce' ), + ], + 'categoryIdIn' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Limit result set to products assigned to a specific group of category IDs.', 'wp-graphql-woocommerce' ), + ], + 'tagIn' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'Limit result set to products assigned to a specific group of tags by name.', 'wp-graphql-woocommerce' ), + ], + 'tagIdIn' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Limit result set to products assigned to a specific group of tag IDs.', 'wp-graphql-woocommerce' ), + ], + 'attributes' => [ + 'type' => [ 'list_of' => 'ProductTaxonomyFilterInput' ], + 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => [ 'list_of' => 'StockStatusEnum' ], + 'description' => __( 'Limit result set to products in stock or out of stock.', 'wp-graphql-woocommerce' ), + ], + 'onSale' => [ + 'type' => 'Boolean', + 'description' => __( 'Limit result set to products on sale.', 'wp-graphql-woocommerce' ), + ], + 'minPrice' => [ + 'type' => 'Float', + 'description' => __( 'Limit result set to products based on a minimum price.', 'wp-graphql-woocommerce' ), + ], + 'maxPrice' => [ + 'type' => 'Float', + 'description' => __( 'Limit result set to products based on a maximum price.', 'wp-graphql-woocommerce' ), + ], + 'visibility' => [ + 'type' => 'CatalogVisibilityEnum', + 'description' => __( 'Limit result set to products with a specific visibility level.', 'wp-graphql-woocommerce' ), + ], + 'rating' => [ + 'type' => [ 'list_of' => 'Integer' ], + 'description' => __( 'Limit result set to products with a specific average rating. Must be between 1 and 5', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-collection-stats-type.php b/includes/type/object/class-collection-stats-type.php index 404e7756..3af7bbac 100644 --- a/includes/type/object/class-collection-stats-type.php +++ b/includes/type/object/class-collection-stats-type.php @@ -21,59 +21,76 @@ class Collection_Stats_Type { */ public static function register() { register_graphql_object_type( - 'PriceRangeStats', + 'PriceRange', [ 'eagerlyLoadType' => true, - 'description' => __( 'Price range stats', 'wp-graphql-woocommerce' ), + 'description' => __( 'Price range', 'wp-graphql-woocommerce' ), 'fields' => [ 'minPrice' => [ - 'type' => 'Float', + 'type' => 'String', 'description' => __( 'Minimum price', 'wp-graphql-woocommerce' ), 'resolve' => function( $source ) { - return ! empty( $source['min_price'] ) ? $source['min_price'] : null; + return ! empty( $source['min_price'] ) ? wc_graphql_price( $source['min_price'] ) : null; } ], 'maxPrice' => [ - 'type' => 'Float', + 'type' => 'String', 'description' => __( 'Maximum price', 'wp-graphql-woocommerce' ), 'resolve' => function( $source ) { - return ! empty( $source['max_price'] ) ? $source['max_price'] : null; + return ! empty( $source['max_price'] ) ? wc_graphql_price( $source['max_price'] ) : null; } ], ], ] ); - register_graphql_object_type ( + register_graphql_object_type( 'AttributeCount', [ 'eagerlyLoadType' => true, - 'description' => __( 'Attribute count', 'wp-graphql-woocommerce' ), - 'fields' => [ + 'description' => __( 'Product attribute terms count', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Attribute name', 'wp-graphql-woocommerce' ), + ], + 'terms' => [ + 'type' => [ 'list_of' => 'SingleAttributeCount' ], + 'description' => __( 'Attribute terms', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + + register_graphql_object_type( + 'SingleAttributeCount', + [ + 'eagerlyLoadType' => true, + 'description' => __( 'Single attribute term count', 'wp-graphql-woocommerce' ), + 'fields' => [ 'termId' => [ 'type' => [ 'non_null' => 'ID' ], 'description' => __( 'Term ID', 'wp-graphql-woocommerce' ), ], - 'count' => [ + 'count' => [ 'type' => 'Int', - 'description' => __( 'Filtered Term Count', 'wp-graphql-woocommerce' ), + 'description' => __( 'Number of products.', 'wp-graphql-woocommerce' ), ], - ], - 'connections' => [ - 'term' => [ - 'toType' => 'TermNode', - 'oneToOne' => true, - 'resolve' => static function ( $source, $args, $context, $info ) { - $term_id = ! empty ( $source->termId ) ? $source->termId : 0; - $taxonomy = ! empty ( $source->taxonomy ) ? $source->taxonomy : null; - $resolver = new TermObjectConnectionResolver( $source, $args, $context, $info, $taxonomy ); - - return $resolver->one_to_one() - ->set_query_arg( 'include', [ $term_id ] ) - ->get_connection(); - }, + 'node' => [ + 'type' => 'TermNode', + 'description' => __( 'Term object.', 'wp-graphql-woocommerce' ), + 'resolve' => function( $source ) { + if ( empty( $source->termId ) ) { + return null; + } + $term = get_term( $source->termId ); + if ( is_wp_error( $term ) ) { + return null; + } + return new \WPGraphQL\Model\Term( $term ); + } ], - ] + ], ] ); @@ -81,15 +98,33 @@ public static function register() { 'RatingCount', [ 'eagerlyLoadType' => true, - 'description' => __( 'Rating count', 'wp-graphql-woocommerce' ), + 'description' => __( 'Single rating count', 'wp-graphql-woocommerce' ), 'fields' => [ 'rating' => [ 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'Rating', 'wp-graphql-woocommerce' ), + 'description' => __( 'Average rating', 'wp-graphql-woocommerce' ), + ], + 'count' => [ + 'type' => 'Int', + 'description' => __( 'Number of products', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + + register_graphql_object_type( + 'StockStatusCount', + [ + 'eagerlyLoadType' => true, + 'description' => __( 'Single stock status count', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'status' => [ + 'type' => [ 'non_null' => 'StockStatusEnum' ], + 'description' => __( 'Status', 'wp-graphql-woocommerce' ), ], 'count' => [ 'type' => 'Int', - 'description' => __( 'Filtered Rating Count', 'wp-graphql-woocommerce' ), + 'description' => __( 'Number of products.', 'wp-graphql-woocommerce' ), ], ], ] @@ -101,8 +136,8 @@ public static function register() { 'description' => __( '', 'wp-graphql-woocommerce' ), 'fields' => [ 'priceRange' => [ - 'type' => 'PriceRangeStats', - 'description' => __( 'Price range', 'wp-graphql-woocommerce' ), + 'type' => 'PriceRange', + 'description' => __( 'Min and max prices found in collection of products, provided using the smallest unit of the currency', 'wp-graphql-woocommerce' ), 'resolve' => function( $source ) { $min_price = ! empty( $source['min_price'] ) ? $source['min_price'] : null; $max_price = ! empty( $source['max_price'] ) ? $source['max_price'] : null; @@ -114,17 +149,17 @@ public static function register() { 'args' => [ 'page' => [ 'type' => 'Int', - 'description' => __( 'Page of results to return.', 'wp-graphql-woocommerce' ), + 'description' => __( 'Page of results to return', 'wp-graphql-woocommerce' ), ], 'perPage' => [ 'type' => 'Int', - 'description' => __( 'Number of results to return per page.', 'wp-graphql-woocommerce' ), + 'description' => __( 'Number of results to return per page', 'wp-graphql-woocommerce' ), ], ], - 'description' => __( 'Attribute counts', 'wp-graphql-woocommerce' ), - 'resolve' => function ( $source, $args ) { - $page = ! empty( $args['page'] ) ? $args['page'] : 1; - $per_page = ! empty( $args['perPage'] ) ? $args['perPage'] : 0; + 'description' => __( 'Returns number of products within attribute terms', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, $args ) { + $page = ! empty( $args['page'] ) ? $args['page'] : 1; + $per_page = ! empty( $args['perPage'] ) ? $args['perPage'] : 0; $attribute_counts = ! empty( $source['attribute_counts'] ) ? $source['attribute_counts'] : []; $attribute_counts = array_slice( $attribute_counts, @@ -132,7 +167,13 @@ public static function register() { 0 < $per_page ? $per_page : null ); - return $attribute_counts; + return array_map( + static function( $name, $terms ) { + return (object) compact( 'name', 'terms' ); + }, + array_keys( $attribute_counts ), + array_values( $attribute_counts ) + ); } ], 'ratingCounts' => [ @@ -140,17 +181,17 @@ public static function register() { 'args' => [ 'page' => [ 'type' => 'Int', - 'description' => __( 'Page of results to return.', 'wp-graphql-woocommerce' ), + 'description' => __( 'Page of results to return', 'wp-graphql-woocommerce' ), ], 'perPage' => [ 'type' => 'Int', - 'description' => __( 'Number of results to return per page.', 'wp-graphql-woocommerce' ), + 'description' => __( 'Number of results to return per page', 'wp-graphql-woocommerce' ), ], ], - 'description' => __( 'Rating counts', 'wp-graphql-woocommerce' ), - 'resolve' => function ( $source, $args ) { - $page = ! empty( $args['page'] ) ? $args['page'] : 1; - $per_page = ! empty( $args['perPage'] ) ? $args['perPage'] : 0; + 'description' => __( 'Returns number of products with each average rating', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, $args ) { + $page = ! empty( $args['page'] ) ? $args['page'] : 1; + $per_page = ! empty( $args['perPage'] ) ? $args['perPage'] : 0; $rating_counts = ! empty( $source['rating_counts'] ) ? $source['rating_counts'] : []; $rating_counts = array_slice( $rating_counts, @@ -161,8 +202,121 @@ public static function register() { return $rating_counts; } ], + 'stockStatusCounts' => [ + 'type' => [ 'list_of' => 'StockStatusCount' ], + 'args' => [ + 'page' => [ + 'type' => 'Int', + 'description' => __( 'Page of results to return', 'wp-graphql-woocommerce' ), + ], + 'perPage' => [ + 'type' => 'Int', + 'description' => __( 'Number of results to return per page', 'wp-graphql-woocommerce' ), + ], + ], + 'description' => __( 'Returns number of products with each stock status', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source, $args ) { + $page = ! empty( $args['page'] ) ? $args['page'] : 1; + $per_page = ! empty( $args['perPage'] ) ? $args['perPage'] : 0; + $stock_status_counts = ! empty( $source['stock_status_counts'] ) ? $source['stock_status_counts'] : []; + $stock_status_counts = array_slice( + $stock_status_counts, + ( $page - 1 ) * $per_page, + 0 < $per_page ? $per_page : null + ); + + return $stock_status_counts; + } + ], ], ] ); } + + + /** + * Prepare the WP_Rest_Request instance used for the resolution of a + * statistics for a product connection. + * + * @param array $where_args Arguments used to filter the connection results. + * + * @return \WP_REST_Request + */ + public static function prepare_rest_request( array $where_args = [] ) { + $request = new \WP_REST_Request(); + if ( empty( $where_args ) ) { + return $request; + } + + $key_mapping = [ + 'slugIn' => 'slug', + 'typeIn' => 'type', + 'categoryIdIn' => 'category', + 'tagIn' => 'tag', + 'onSale' => 'on_sale', + 'stockStatus' => 'stock_status', + 'visibility' => 'catalog_visibility', + 'minPrice' => 'min_price', + 'maxPrice' => 'max_price', + ]; + + $needs_formatting = [ 'attributes', 'categoryIn']; + foreach( $where_args as $key => $value ) { + if ( in_array( $key, $needs_formatting, true ) ) { + continue; + } + + $request->set_param( $key_mapping[ $key ] ?? $key, $value ); + } + + if ( ! empty( $where_args['categoryIn'] ) ) { + $category_ids = array_map( + static function ( $category ) { + $term = get_term_by( 'slug', $category, 'product_cat' ); + if ( is_object( $term ) ) { + return $term->term_id; + } + return 0; + }, + $where_args['categoryIn'] + ); + $setCategory = $request->get_param( 'category' ); + if ( ! empty( $setCategory ) ) { + $category_ids[] = $setCategory; + $request->set_param( 'category', $category_ids ); + } else { + $request->set_param( 'category', $category_ids); + } + $request->set_param( 'category_operator', 'in' ); + } + + if ( ! empty( $where_args['attributes'] ) ) { + $attributes = []; + foreach( $where_args['attributes'] as $filter ) { + if ( str_starts_with( $filter['taxonomy'], 'pa_' ) ) { + $attribute = []; + $attribute['attribute'] = $filter['taxonomy']; + if ( ! empty( $filter['terms'] ) ) { + $attribute['slug'] = $filter['terms']; + } elseif ( ! empty( $filter['ids'] ) ) { + $attribute['term_id'] = $filter['ids']; + } + $attribute['operator'] = ! empty( $filter['operator'] ) ? strtolower( $filter['operator'] ) : 'in'; + $attributes[] = $attribute; + } else { + if ( ! empty( $filter['ids'] ) ) { + continue; + } + $taxonomy = $filter['taxonomy']; + $request->set_param( "_unstable_tax_{$taxonomy}", $filter['ids'] ); + $request->set_param( "_unstable_tax_{$taxonomy}_operator", strtolower( $filter['operator'] ) ); + } + } + if ( ! empty( $attributes ) ) { + $request->set_param( 'attributes', $attributes ); + } + } + + return $request; + } } diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index c815f387..f164a804 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -519,18 +519,28 @@ public static function register_fields() { 'collectionStats' => [ 'type' => 'CollectionStats', 'args' => [ - 'calculatePriceRange' => [ - 'type' => 'Boolean', + 'calculatePriceRange' => [ + 'type' => 'Boolean', + 'description' => __( 'If true, calculates the minimum and maximum product prices for the collection.', 'wp-graphql-woocommerce' ) ], 'calculateRatingCounts' => [ - 'type' => 'Boolean', + 'type' => 'Boolean', + 'description' => __( 'If true, calculates rating counts for products in the collection.', 'wp-graphql-woocommerce' ), ], - 'taxonomies' => [ + 'calculateStockStatusCounts' => [ + 'type' => 'Boolean', + 'description' => __( 'If true, calculates stock counts for products in the collection.', 'wp-graphql-woocommerce' ) + ], + 'taxonomies' => [ 'type' => [ 'list_of' => 'CollectionStatsQueryInput' ], ], + 'where' => [ + 'type' => 'CollectionStatsWhereArgs', + ], ], 'description' => __( 'Statistics for a product taxonomy query', 'wp-graphql-woocommerce' ), - 'resolve' => function ( $_, $args ) { + 'resolve' => static function ( $_, $args ) { + $filters = new ProductQueryFilters(); $data = [ 'min_price' => null, 'max_price' => null, @@ -538,14 +548,28 @@ public static function register_fields() { 'stock_status_counts' => null, 'rating_counts' => null, ]; - $filters = new ProductQueryFilters(); - $request = new \WP_REST_Request(); - $request->set_param( 'calculate_attribute_counts', ! empty( $args['taxonomies'] ) ? $args['taxonomies'] : null ); - $request->set_param( 'calculate_price_range', ! empty( $args['calculatePriceRange'] ) ); - $request->set_param( 'calculate_stock_status_counts', ! empty( $args['calculateStockStatusCounts'] ) ); - $request->set_param( 'calculate_rating_counts', ! empty( $args['calculateRatingCounts'] ) ); + // Process client-side filters. + $request = Collection_Stats_Type::prepare_rest_request( $args['where'] ?? [] ); + + // Format taxonomies. + if ( ! empty( $args['taxonomies'] ) ) { + $calculate_attribute_counts = []; + foreach ( $args['taxonomies'] as $attribute_to_count ) { + $calculate_attribute_counts[] = [ + 'taxonomy' => $attribute_to_count['taxonomy'], + 'query_type' => strtolower( $attribute_to_count['relation'] ), + ]; + } + $request->set_param( 'calculate_attribute_counts', $calculate_attribute_counts ); + } + + $request->set_param( 'calculate_price_range', $args['calculatePriceRange'] ?? false ); + $request->set_param( 'calculate_stock_status_counts', $args['calculateStockStatusCounts'] ?? false ); + $request->set_param( 'calculate_rating_counts', $args['calculateRatingCounts'] ?? false ); + + if ( ! empty( $request['calculate_price_range'] ) ) { $filter_request = clone $request; $filter_request->set_param( 'min_price', null ); @@ -575,13 +599,15 @@ public static function register_fields() { if ( ! isset( $attributes_to_count['taxonomy'] ) ) { continue; } - - $counts = $filters->get_attribute_counts( $request, $attributes_to_count['taxonomy'] ); - + + $taxonomy = $attributes_to_count['taxonomy']; + $counts = $filters->get_attribute_counts( $request, $taxonomy ); + + $data['attribute_counts'][ $taxonomy ] = []; foreach ( $counts as $key => $value ) { - $data['attribute_counts'][] = (object) [ - 'taxonomy' => $attributes_to_count['taxonomy'], - 'termId' => $key, + $data['attribute_counts'][ $taxonomy ][] = (object) [ + 'taxonomy' => $taxonomy, + 'termId' => $key, 'count' => $value, ]; } @@ -601,8 +627,6 @@ public static function register_fields() { } } - //wp_send_json( $data ); - return $data; }, ] diff --git a/tests/wpunit/CollectionStatsQueryTest.php b/tests/wpunit/CollectionStatsQueryTest.php new file mode 100644 index 00000000..cbea0979 --- /dev/null +++ b/tests/wpunit/CollectionStatsQueryTest.php @@ -0,0 +1,124 @@ +factory->product_variation->createSome( + $this->factory->product->createVariable() + ); + $this->factory->product->createSimple(); + $this->factory->product->createSimple(); + $this->factory->product_variation->createSome( + $this->factory->product->createVariable() + ); + + $query = ' + query ($where: CollectionStatsWhereArgs, $taxonomies: [CollectionStatsQueryInput]) { + collectionStats( + calculatePriceRange: true + calculateRatingCounts: true + calculateStockStatusCounts: true + taxonomies: $taxonomies + where: $where + ) { + attributeCounts { + name + terms { + node { slug } + termId + count + } + } + stockStatusCounts { + status + count + } + } + } + '; + + $variables = [ + 'where' => [ + 'attributes' => [ + [ + 'taxonomy' => 'PACOLOR', + 'terms' => 'red', + 'operator' => 'IN', + ], + [ + 'taxonomy' => 'PASIZE', + 'terms' => 'large', + 'operator' => 'IN', + ] + ] + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PACOLOR', + 'relation' => 'AND', + ] + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField('name', 'pa_color' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'red' ), + $this->expectedField( 'count', 2 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'blue' ), + $this->expectedField( 'count', 2 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'green' ), + $this->expectedField( 'count', 2 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + ], + 0 + ), + $this->expectedNode( + 'collectionStats.stockStatusCounts', + [ + $this->expectedField( 'status', 'IN_STOCK' ), + $this->expectedField( 'count', 2 ), + ] + ), + $this->expectedNode( + 'collectionStats.stockStatusCounts', + [ + $this->expectedField( 'status', 'OUT_OF_STOCK' ), + $this->expectedField( 'count', 0 ), + ] + ), + $this->expectedNode( + 'collectionStats.stockStatusCounts', + [ + $this->expectedField( 'status', 'ON_BACKORDER' ), + $this->expectedField( 'count', 0 ), + ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + } +} \ No newline at end of file