Files
Geoff TaylorandGitHub 1807b2e798 Add authenticated download URLs for headless frontends (#995)
* feat: add authenticated download URLs for headless frontends

- Add downloadNonce and downloadUrl fields to DownloadableItem type
  using the existing Protected Router session transfer pattern
- Add preAuthDownloadUrl field (toggleable via settings) that generates
  tokenized download URLs for direct file access without cookie auth
- Add download_url nonce handling to Protected_Router
- Add enable_pre_auth_download_urls and download_url_nonce_param settings
- Add GraphQLE2E helpers for checkout and account shortcode pages
- Rewrite ProtectedRouterCest to test redirect flow without JS-dependent
  page content assertions, add account and payment method URL tests
- Add DownloadableItemAuthCest with 5 e2e tests covering both options

* fix: test suite stability and code coverage collection

- Add WC_Unit_Tests_Bootstrap stub to wpunit bootstrap to bypass
  wc_get_product_visibility_term_ids static cache between suites
- Add setWooGraphQLSetting helper to GraphQLE2E for safe individual
  field updates to woographql_settings option with proper defaults
- Update all functional tests to use setWooGraphQLSetting instead of
  replacing the entire woographql_settings option
- Fix createRelated factory to use explicit shared category instead
  of relying on default Uncategorized category
- Add download_url to ProtectedRouterTest nonce names assertion
- Remove debug logs from ProductQueriesTest and ProductsQueriesTest
- Fix CI workflow to run suites separately and aggregate coverage
  via phpcov merge
- Wire c3.php into WordPress index.php for remote coverage collection
- Update .coveralls.yml service_name to github-actions
- Clean up Xdebug 2 settings in Dockerfile

* chore: Linter compliances met

* fix: ensure tests/_output is writable for c3.php coverage collection

* devops: .env.docker removed from setup

* devops: split CI into separate jobs per suite with retry and coverage aggregation

* devops: add +Coverage indicator to CI job names

* devops: add --fail-fast to first run, fix retry to mark job as passing on retry success
2026-03-26 17:20:55 -04:00

1861 lines
50 KiB
PHP

<?php
class ProductsQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase {
private function createProducts() {
$products = [
$this->factory->product->createSimple([
'name' => 'Product Blue',
'slug' => 'product-blue',
'description' => 'A peach description',
'price' => 100,
'regular_price' => 100,
'sale_price' => 90,
'stock_status' => 'instock',
'stock_quantity' => 10,
'reviews_allowed' => true,
'average_rating' => 4.5,
]),
$this->factory->product->createSimple([
'name' => 'Product Green',
'slug' => 'product-green',
'description' => 'A turquoise description',
'sku' => 'green-sku',
'price' => 200,
'regular_price' => 200,
'sale_price' => 180,
'stock_status' => 'instock',
'stock_quantity' => 20,
'reviews_allowed' => true,
'average_rating' => 4.0,
]),
$this->factory->product->createSimple([
'name' => 'Product Red',
'slug' => 'product-red',
'description' => 'A maroon description',
'price' => 300,
'regular_price' => 300,
'sale_price' => 270,
'stock_status' => 'instock',
'stock_quantity' => 30,
'reviews_allowed' => true,
'average_rating' => 3.5,
]),
$this->factory->product->createSimple([
'name' => 'Product Yellow',
'slug' => 'product-yellow',
'description' => 'A teal description',
'price' => 400,
'regular_price' => 400,
'sale_price' => 360,
'stock_status' => 'instock',
'stock_quantity' => 40,
'reviews_allowed' => true,
'average_rating' => 3.0,
]),
$this->factory->product->createSimple([
'name' => 'Product Purple',
'slug' => 'product-purple',
'description' => 'A magenta description',
'price' => 500,
'regular_price' => 500,
'sale_price' => 450,
'stock_status' => 'instock',
'stock_quantity' => 50,
'reviews_allowed' => true,
'average_rating' => 2.5,
]),
];
$order_id = $this->factory->order->createNew(
[
'payment_method' => 'cod',
],
[
'line_items' => [
[
'product' => $products[0],
'qty' => 10,
],
[
'product' => $products[1],
'qty' => 8,
],
[
'product' => $products[2],
'qty' => 6,
],
[
'product' => $products[3],
'qty' => 4,
],
[
'product' => $products[4],
'qty' => 2,
],
],
]
);
$order = \wc_get_order( $order_id );
$order->calculate_totals();
$order->update_status( 'completed' );
wc_update_total_sales_counts( $order_id );
$review_one = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[0],
'comment_content' => 'It worked great!',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_one, 'rating', 5.0 );
$review_one = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[0],
'comment_content' => 'It worked great!',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_one, 'rating', 5.0 );
$review_two = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[2],
'comment_content' => 'It was basic',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_two, 'rating', 3.0 );
$review_three = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[2],
'comment_content' => 'Overpriced',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_three, 'rating', 2.0 );
$review_four = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[4],
'comment_content' => 'Overpriced',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_four, 'rating', 3.5 );
$review_five = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[4],
'comment_content' => 'Overpriced and ugly',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_five, 'rating', 2.5 );
$review_six = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[1],
'comment_content' => 'It was cheap!',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_six, 'rating', 4.2 );
$review_six = $this->factory()->comment->create(
[
'comment_author' => 'Customer',
'comment_author_email' => 'customer@example.com',
'comment_post_ID' => $products[1],
'comment_content' => 'It was cheap!',
'comment_approved' => 1,
'comment_type' => 'review',
]
);
update_comment_meta( $review_six, 'rating', 4.2 );
wc_update_product_lookup_tables();
return $products;
}
// Tests
public function testProductsQueryAndWhereArgs() {
$category_3 = $this->factory->product->createProductCategory( 'category-three' );
$category_4 = $this->factory->product->createProductCategory( 'category-four' );
$product_ids = [
$this->factory->product->createSimple(
[
'slug' => 'test-product-1',
'price' => 6000,
'regular_price' => 6000,
]
),
$this->factory->product->createSimple(
[
'price' => 2,
'regular_price' => 2,
'category_ids' => [ $category_3, $category_4 ],
]
),
$this->factory->product->createSimple(
[
'featured' => 'true',
'category_ids' => [ $category_3 ],
]
),
$this->factory->product->createExternal(),
$this->factory->product->createSimple(
[
'price' => 200,
'regular_price' => 300,
'sale_price' => 200,
'date_on_sale_from' => ( new \DateTime( 'yesterday' ) )->format( 'Y-m-d H:i:s' ),
'date_on_sale_to' => ( new \DateTime( 'tomorrow' ) )->format( 'Y-m-d H:i:s' ),
'stock_status' => 'outofstock',
]
),
];
$query = '
query (
$slugIn: [String],
$status: String,
$category: String,
$categoryIn: [String],
$categoryNotIn: [String],
$categoryId: Int,
$categoryIdIn: [Int]
$categoryIdNotIn: [Int]
$type: ProductTypesEnum,
$typeIn: [ProductTypesEnum],
$typeNotIn: [ProductTypesEnum],
$featured: Boolean,
$maxPrice: Float,
$orderby: [ProductsOrderbyInput]
$taxonomyFilter: ProductTaxonomyInput
$include: [Int]
$exclude: [Int]
$stockStatus: [StockStatusEnum]
) {
products( where: {
slugIn: $slugIn,
status: $status,
category: $category,
categoryIn: $categoryIn,
categoryNotIn: $categoryNotIn,
categoryId: $categoryId,
categoryIdIn: $categoryIdIn,
categoryIdNotIn: $categoryIdNotIn,
type: $type,
typeIn: $typeIn,
typeNotIn: $typeNotIn,
featured: $featured,
maxPrice: $maxPrice,
orderby: $orderby,
taxonomyFilter: $taxonomyFilter
include: $include
exclude: $exclude
stockStatus: $stockStatus
} ) {
nodes {
id
... on ProductWithPricing {
databaseId
price
}
... on InventoriedProduct {
stockStatus
}
}
}
}
';
$all_expected_product_nodes = array_map(
function ( $product_id ) {
return $this->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_id ) ) ]
);
},
$product_ids
);
/**
* Assertion One
*
* Tests query with no arguments, and expect all products to be returned.
*/
$response = $this->graphql( compact( 'query' ) );
$this->assertQuerySuccessful( $response, $all_expected_product_nodes );
/**
* Assertion Two
*
* Tests query with "slug" where argument, and expect the product with
* the slug "test-product-1" to be returned.
*/
$variables = [ 'slugIn' => [ 'test-product-1' ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids ) {
$product = \wc_get_product( $product_ids[ $index ] );
return 'test-product-1' === $product->get_slug();
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Three
*
* Tests query with "status" where argument, and expect the products with
* a status of "pending" to be returned, which there are none among the test
* product with that status.
*/
$variables = [ 'status' => 'pending' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [ $this->expectedField( 'products.nodes', [] ) ];
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Four
*
* Tests query with "type" where argument, and expect only "simple" products
* to be returned.
*/
$variables = [ 'type' => 'SIMPLE' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids ) {
$product = \wc_get_product( $product_ids[ $index ] );
return 'simple' === $product->get_type();
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Five
*
* Tests query with "typeIn" where argument, and expect only "simple" products
* to be returned.
*/
$variables = [ 'typeIn' => [ 'SIMPLE' ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
// No need to reassign the $expected for this assertion.
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Six
*
* Tests query with "typeNotIn" where argument, and expect all types of products
* with except "simple" to be returned.
*/
$variables = [ 'typeNotIn' => [ 'SIMPLE' ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids ) {
$product = \wc_get_product( $product_ids[ $index ] );
return 'simple' !== $product->get_type();
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Seven
*
* Tests query with "featured" where argument, expect only featured products
* to be returned.
*/
$variables = [ 'featured' => true ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids ) {
$product = \wc_get_product( $product_ids[ $index ] );
return $product->get_featured();
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Eight
*
* Tests query with "maxPrice" where argument, and expect all product
* with a price of 10.00+ to be returned.
*/
$variables = [ 'maxPrice' => 10.00 ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids ) {
$product = \wc_get_product( $product_ids[ $index ] );
return 10.00 >= floatval( $product->get_price() );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Nine
*
* Tests query with "orderby" where argument, and expect products to
* be return in descending order by "price".
*/
$variables = [
'orderby' => [
[
'field' => 'PRICE',
'order' => 'DESC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[0] ) ) ],
0
),
$this->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[1] ) ) ],
4
),
];
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Ten
*
* Tests query with "category" where argument, and expect products in
* the "category-three" category to be returned.
*/
$variables = [ 'category' => 'category-three' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_3 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return in_array( $category_3, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
$this->clearLoaderCache( 'wc_post' );
/**
* Assertion Eleven
*
* Tests query with "categoryIn" where argument, and expect products in
* the "category-three" category to be returned.
*/
$variables = [ 'categoryIn' => [ 'category-three' ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
// No need to reassign the $expected for this assertion.
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Twelve
*
* Tests query with "categoryId" where argument, and expect products in
* the "category-three" category to be returned.
*/
$variables = [ 'categoryId' => $category_3 ];
$response = $this->graphql( compact( 'query', 'variables' ) );
// No need to reassign the $expected for this assertion either.
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Thirteen
*
* Tests query with "categoryNotIn" where argument, and expect all products
* except products in the "category-four" category to be returned.
*/
$variables = [ 'categoryNotIn' => [ 'category-four' ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_4 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return ! in_array( $category_4, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Fourteen
*
* Tests query with "categoryIdNotIn" where argument, and expect all products
* except products in the "category-four" category to be returned.
*/
$variables = [ 'categoryIdNotIn' => [ $category_4 ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
// No need to reassign the $expected for this assertion.
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Fifteen
*
* Tests query with "categoryIdIn" where argument, and expect products in
* the "category-four" category to be returned.
*/
$variables = [ 'categoryIdIn' => [ $category_4 ] ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_4 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return in_array( $category_4, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Sixteen
*
* Tests "taxonomyFilter" where argument
*/
$variables = [
'taxonomyFilter' => [
'relation' => 'AND',
'filters' => [
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-three' ],
],
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-four' ],
'operator' => 'NOT_IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return ! in_array( $category_4, $product->get_category_ids(), true )
&& in_array( $category_3, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Seventeen
*
* Tests "taxonomyFilter" with new "or" syntax
*/
$variables = [
'taxonomyFilter' => [
'or' => [
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-three' ],
],
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-four' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return in_array( $category_4, $product->get_category_ids(), true )
|| in_array( $category_3, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion Eighteen
*
* Tests "taxonomyFilter" with new "and" syntax
*/
$variables = [
'taxonomyFilter' => [
'and' => [
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-three' ],
],
[
'taxonomy' => 'PRODUCT_CAT',
'terms' => [ 'category-four' ],
'operator' => 'NOT_IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = array_filter(
$all_expected_product_nodes,
static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) {
$product = \wc_get_product( $product_ids[ $index ] );
return ! in_array( $category_4, $product->get_category_ids(), true )
&& in_array( $category_3, $product->get_category_ids(), true );
},
ARRAY_FILTER_USE_BOTH
);
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion 19-20
*
* Tests "include" where argument
*/
$variables = [
'include' => [ $product_ids[0] ],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[0] ) ) ]
),
];
$this->assertQuerySuccessful( $response, $expected );
$variables = [
'include' => [ 1000 ],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField(
'products.nodes',
self::IS_FALSY
),
];
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion 19-20
*
* Tests "exclude" where argument
*/
$variables = [
'exclude' => [ $product_ids[0] ],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->not()->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[0] ) ) ]
),
];
$this->assertQuerySuccessful( $response, $expected );
$variables = [ 'exclude' => $product_ids ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField(
'products.nodes',
self::IS_FALSY
),
];
$this->assertQuerySuccessful( $response, $expected );
/**
* Assertion 21-22
*
* Tests "stockStatus" where argument
*/
$variables = [ 'stockStatus' => 'IN_STOCK' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->not()->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[4] ) ) ]
),
];
$this->assertQuerySuccessful( $response, $expected );
// Debug: check product state before stockStatus assertion.
$oos_product = wc_get_product( $product_ids[4] );
codecept_debug( 'OUT_OF_STOCK PRODUCT ID: ' . $product_ids[4] );
codecept_debug( 'OUT_OF_STOCK PRODUCT STOCK STATUS: ' . $oos_product->get_stock_status() );
codecept_debug( 'OUT_OF_STOCK PRODUCT POST STATUS: ' . get_post_status( $product_ids[4] ) );
codecept_debug( 'OUT_OF_STOCK PRODUCT TYPE: ' . $oos_product->get_type() );
codecept_debug( 'OUT_OF_STOCK PRODUCT VISIBILITY: ' . $oos_product->get_catalog_visibility() );
global $wpdb;
$meta_lookup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product_ids[4] ) ); // phpcs:ignore
codecept_debug( 'OUT_OF_STOCK META LOOKUP: ' . wp_json_encode( $meta_lookup ) );
$all_posts = $wpdb->get_results( "SELECT ID, post_type, post_status FROM {$wpdb->posts} WHERE post_type = 'product'" ); // phpcs:ignore
codecept_debug( 'ALL PRODUCT POSTS: ' . wp_json_encode( $all_posts ) );
$variables = [ 'stockStatus' => 'OUT_OF_STOCK' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[ $this->expectedField( 'id', $this->toRelayId( 'post', $product_ids[4] ) ) ],
0
),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testVariationsQueryAndWhereArgs() {
// Create product variations.
$products = $this->factory->product_variation->createSome(
$this->factory->product->createVariable()
);
$variation_id = $products['variations'][0];
$id = $this->toRelayId( 'post', $products['product'] );
$product = wc_get_product( $products['product'] );
$variations = $products['variations'];
$prices = $product->get_variation_prices( true );
$query = '
query (
$id: ID!,
$minPrice: Float,
$parent: Int,
$parentIn: [Int],
$parentNotIn: [Int]
) {
product( id: $id ) {
... on VariableProduct {
price
regularPrice
salePrice
variations( where: {
minPrice: $minPrice,
parent: $parent,
parentIn: $parentIn,
parentNotIn: $parentNotIn
} ) {
nodes {
id
price
}
}
}
}
}
';
/**
* Assertion One
*
* Test query with no arguments
*/
$this->loginAsShopManager();
$variables = [ 'id' => $id ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[0] ) ),
$this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[1] ) ),
$this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[2] ) ),
$this->expectedField(
'product.price',
\wc_graphql_price( current( $prices['price'] ) )
. ' - '
. \wc_graphql_price( end( $prices['price'] ) )
),
$this->expectedField(
'product.regularPrice',
\wc_graphql_price( current( $prices['regular_price'] ) )
. ' - '
. \wc_graphql_price( end( $prices['regular_price'] ) )
),
$this->expectedField( 'product.salePrice', self::IS_NULL ),
];
$this->assertQuerySuccessful( $response, $expected );
$this->clearLoaderCache( 'wc_post' );
/**
* Assertion Two
*
* Test "minPrice" where argument
*/
$variables = [
'id' => $id,
'minPrice' => 15,
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->not()->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[0] ) ),
$this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[1] ) ),
$this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'post', $variations[2] ) ),
];
$this->assertQuerySuccessful( $response, $expected );
}
public function testProductsOrderbyArg() {
// Create products.
$products = $this->createProducts();
// Query.
$query = 'query (
$first: Int
$last: Int
$after: String
$before: String
$orderby: [ProductsOrderbyInput]
) {
products(first: $first, last: $last, after: $after, before: $before, where: { orderby: $orderby }) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
id
name
averageRating
reviewCount
... on ProductWithPricing {
databaseId
price
}
}
}
}';
/**
* Assert sorting by price functions correctly.
*/
$variables = [
'first' => 2,
'orderby' => [
[
'field' => 'PRICE',
'order' => 'ASC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) )
],
1
),
],
'Failed to sort products by price in ascending order.'
);
/**
* Assert sorting by price functions correctly w/ pagination.
*/
$endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' );
$this->logData( $endCursor );
$variables = [
'first' => 2,
'after' => $endCursor,
'orderby' => [
[
'field' => 'PRICE',
'order' => 'ASC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[3] ) )
],
1
),
],
'Failed to sort products by price in ascending order with pagination.'
);
/**
* Assert sorting by popularity functions correctly.
*/
$variables = [
'first' => 2,
'orderby' => [
[
'field' => 'POPULARITY',
'order' => 'DESC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) )
],
1
),
],
'Failed to sort products by popularity in ascending order.'
);
/**
* Assert sorting by popularity functions correctly w/ pagination.
*/
$endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' );
$variables = [
'first' => 2,
'after' => $endCursor,
'orderby' => [
[
'field' => 'POPULARITY',
'order' => 'DESC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[3] ) )
],
1
),
],
'Failed to sort products by popularity in ascending order with pagination.'
);
/**
* Assert sorting by rating functions correctly.
*/
$variables = [
'first' => 2,
'orderby' => [
[
'field' => 'RATING',
'order' => 'DESC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) )
],
1
),
],
'Failed to sort products by rating in ascending order.'
);
/**
* Assert sorting by rating functions correctly w/ pagination.
*/
$endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' );
$variables = [
'first' => 2,
'after' => $endCursor,
'orderby' => [
[
'field' => 'RATING',
'order' => 'DESC',
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[4] ) )
],
0
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) )
],
1
),
],
'Failed to sort products by rating in ascending order with pagination.'
);
}
public function testProductsSearchArg() {
// Create products.
$products = $this->createProducts();
// Query.
$query = 'query (
$after: String,
$search: String
) {
products(first: 2, after: $after, where: { search: $search }) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
id
name
description
sku
... on ProductWithPricing {
databaseId
price
}
}
}
}';
/**
* Assert search by product title functions correctly.
*/
$variables = [
'search' => 'Green',
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) )
],
0
),
],
'Failed to search products by product title.'
);
/**
* Assert search by product sku.
*/
$variables = [ 'search' => 'green-sku' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) )
],
0
),
],
'Failed to search products by product sku.'
);
// Search by product description.
$variables = [ 'search' => 'magenta' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[4] ) )
],
0
),
],
'Failed to search products by product description content.'
);
// Search by slug.
$variables = [ 'search' => 'product-red' ];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful(
$response,
[
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) )
],
0
),
],
'Failed to search products by product slug.'
);
}
public function testProductsQueryWithNewTaxonomyFilterSyntax() {
// Create test categories using WooCommerce factory
$category1 = $this->factory->product->createProductCategory('electronics');
$category2 = $this->factory->product->createProductCategory('clothing');
$category3 = $this->factory->product->createProductCategory('books');
$products = [
// Product in Electronics category
$this->factory->product->createSimple([
'name' => 'Laptop',
'category_ids' => [$category1],
]),
// Product in Clothing category
$this->factory->product->createSimple([
'name' => 'T-shirt',
'category_ids' => [$category2],
]),
// Product in both Electronics and Books categories
$this->factory->product->createSimple([
'name' => 'E-book Reader',
'category_ids' => [$category1, $category3],
]),
// Product in Books category only
$this->factory->product->createSimple([
'name' => 'Novel',
'category_ids' => [$category3],
]),
];
// Query using new "or" syntax
$query = '
query testProductsWithTaxonomyOr($taxonomyFilter: ProductTaxonomyInput) {
products(where: {taxonomyFilter: $taxonomyFilter}) {
nodes {
id
databaseId
name
}
}
}
';
// Test OR syntax - should return products from Electronics OR Books
$variables = [
'taxonomyFilter' => [
'or' => [
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category1],
'operator' => 'IN',
],
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category3],
'operator' => 'IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) ),
$this->expectedField( 'name', 'Laptop' ),
]
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) ),
$this->expectedField( 'name', 'E-book Reader' ),
]
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[3] ) ),
$this->expectedField( 'name', 'Novel' ),
]
),
];
$this->assertQuerySuccessful( $response, $expected, 'OR taxonomy filter should work correctly' );
// Test AND syntax - should return products that have BOTH Electronics AND Books
$variables = [
'taxonomyFilter' => [
'and' => [
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category1],
'operator' => 'IN',
],
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category3],
'operator' => 'IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[2] ) ),
$this->expectedField( 'name', 'E-book Reader' ),
]
),
];
$this->assertQuerySuccessful( $response, $expected, 'AND taxonomy filter should work correctly' );
}
public function testProductsQueryWithLegacyTaxonomyFilterSyntax() {
// Create test categories using WooCommerce factory
$category1 = $this->factory->product->createProductCategory('legacy-electronics');
$category2 = $this->factory->product->createProductCategory('legacy-clothing');
$products = [
$this->factory->product->createSimple([
'name' => 'Legacy Laptop',
'category_ids' => [$category1],
]),
$this->factory->product->createSimple([
'name' => 'Legacy T-shirt',
'category_ids' => [$category2],
]),
];
// Test legacy syntax still works
$query = '
query testProductsWithLegacyTaxonomy($taxonomyFilter: ProductTaxonomyInput) {
products(where: {taxonomyFilter: $taxonomyFilter}) {
nodes {
id
databaseId
name
}
}
}
';
$variables = [
'taxonomyFilter' => [
'relation' => 'OR',
'filters' => [
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category1],
'operator' => 'IN',
],
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category2],
'operator' => 'IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) ),
$this->expectedField( 'name', 'Legacy Laptop' ),
]
),
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[1] ) ),
$this->expectedField( 'name', 'Legacy T-shirt' ),
]
),
];
$this->assertQuerySuccessful( $response, $expected, 'Legacy taxonomy filter syntax should work correctly' );
}
public function testTaxonomyFilterPriority() {
// Test that new syntax takes priority over legacy syntax
$category1 = $this->factory->product->createProductCategory('priority-electronics');
$category2 = $this->factory->product->createProductCategory('priority-clothing');
$products = [
$this->factory->product->createSimple([
'name' => 'Priority Laptop',
'category_ids' => [$category1],
]),
$this->factory->product->createSimple([
'name' => 'Priority T-shirt',
'category_ids' => [$category2],
]),
];
$query = '
query testTaxonomyPriority($taxonomyFilter: ProductTaxonomyInput) {
products(where: {taxonomyFilter: $taxonomyFilter}) {
nodes {
id
databaseId
name
}
}
}
';
// Use both new "or" syntax and legacy "filters" syntax - "or" should take priority
$variables = [
'taxonomyFilter' => [
'or' => [
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category1],
'operator' => 'IN',
],
],
'relation' => 'OR',
'filters' => [
[
'taxonomy' => 'PRODUCT_CAT',
'ids' => [$category2],
'operator' => 'IN',
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedNode(
'products.nodes',
[
$this->expectedField( 'id', $this->toRelayId( 'post', $products[0] ) ),
$this->expectedField( 'name', 'Priority Laptop' ),
]
),
];
$this->assertQuerySuccessful( $response, $expected, 'New OR syntax should take priority over legacy filters syntax' );
}
/**
* Test taxonomyFilter with multiple terms in a single filter entry.
*
* Reproduces the scenario from #821 where passing multiple terms like
* terms: ["zielony", "kremowy"] only returned products for the first term.
*
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/821
*/
public function testTaxonomyFilterWithMultipleTermsInSingleFilter() {
// Create the color attribute with all terms.
$color_attr = $this->factory->product->createAttribute( 'color', [ 'red', 'blue', 'green' ] );
// Get individual term IDs.
$red_term_id = get_term_by( 'slug', 'red', 'pa_color' )->term_id;
$blue_term_id = get_term_by( 'slug', 'blue', 'pa_color' )->term_id;
$green_term_id = get_term_by( 'slug', 'green', 'pa_color' )->term_id;
// Product with red + blue.
$red_blue_product = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $color_attr['attribute_id'],
'attribute_taxonomy' => $color_attr['attribute_taxonomy'],
'term_ids' => [ $red_term_id, $blue_term_id ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $red_blue_product,
'attributes' => [ 'pa_color' => 'red' ],
'image_id' => null,
'regular_price' => 10,
]
);
// Product with blue + green.
$blue_green_product = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $color_attr['attribute_id'],
'attribute_taxonomy' => $color_attr['attribute_taxonomy'],
'term_ids' => [ $blue_term_id, $green_term_id ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $blue_green_product,
'attributes' => [ 'pa_color' => 'green' ],
'image_id' => null,
'regular_price' => 15,
]
);
// Product with only green.
$green_product = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $color_attr['attribute_id'],
'attribute_taxonomy' => $color_attr['attribute_taxonomy'],
'term_ids' => [ $green_term_id ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $green_product,
'attributes' => [ 'pa_color' => 'green' ],
'image_id' => null,
'regular_price' => 20,
]
);
$this->clearSchema();
$query = '
query ($taxonomyFilter: ProductTaxonomyInput) {
products(where: { taxonomyFilter: $taxonomyFilter }) {
nodes {
databaseId
}
}
}
';
// Pass multiple terms in a single filter — should return products matching ANY of the terms.
// red + blue should match red_blue_product and blue_green_product (both have blue).
$variables = [
'taxonomyFilter' => [
'filters' => [
[
'taxonomy' => 'PA_COLOR',
'terms' => [ 'red', 'blue' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'products.nodes.#.databaseId', $red_blue_product ),
$this->expectedField( 'products.nodes.#.databaseId', $blue_green_product ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $green_product ),
];
$this->assertQuerySuccessful( $response, $expected );
// Also test with the new "or" syntax — red only should match red_blue_product.
$variables = [
'taxonomyFilter' => [
'or' => [
[
'taxonomy' => 'PA_COLOR',
'terms' => [ 'red' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'products.nodes.#.databaseId', $red_blue_product ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $blue_green_product ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $green_product ),
];
$this->assertQuerySuccessful( $response, $expected );
}
/**
* Test taxonomyFilter with multiple terms across multiple taxonomies.
*
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/821
*/
public function testTaxonomyFilterWithMultipleTermsAcrossMultipleTaxonomies() {
$shade_attr = $this->factory->product->createAttribute( 'shade', [ 'light', 'dark' ] );
$weight_attr = $this->factory->product->createAttribute( 'weight', [ 'light-weight', 'heavy-weight' ] );
$light_term = get_term_by( 'slug', 'light', 'pa_shade' )->term_id;
$dark_term = get_term_by( 'slug', 'dark', 'pa_shade' )->term_id;
$lw_term = get_term_by( 'slug', 'light-weight', 'pa_weight' )->term_id;
$hw_term = get_term_by( 'slug', 'heavy-weight', 'pa_weight' )->term_id;
// light shade + light-weight
$product_a = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $shade_attr['attribute_id'],
'attribute_taxonomy' => $shade_attr['attribute_taxonomy'],
'term_ids' => [ $light_term ],
],
[
'attribute_id' => $weight_attr['attribute_id'],
'attribute_taxonomy' => $weight_attr['attribute_taxonomy'],
'term_ids' => [ $lw_term ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $product_a,
'attributes' => [ 'pa_shade' => 'light', 'pa_weight' => 'light-weight' ],
'image_id' => null,
'regular_price' => 10,
]
);
// dark shade + heavy-weight
$product_b = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $shade_attr['attribute_id'],
'attribute_taxonomy' => $shade_attr['attribute_taxonomy'],
'term_ids' => [ $dark_term ],
],
[
'attribute_id' => $weight_attr['attribute_id'],
'attribute_taxonomy' => $weight_attr['attribute_taxonomy'],
'term_ids' => [ $hw_term ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $product_b,
'attributes' => [ 'pa_shade' => 'dark', 'pa_weight' => 'heavy-weight' ],
'image_id' => null,
'regular_price' => 20,
]
);
// light shade + heavy-weight
$product_c = $this->factory->product->createVariable(
[
'attribute_data' => [
[
'attribute_id' => $shade_attr['attribute_id'],
'attribute_taxonomy' => $shade_attr['attribute_taxonomy'],
'term_ids' => [ $light_term ],
],
[
'attribute_id' => $weight_attr['attribute_id'],
'attribute_taxonomy' => $weight_attr['attribute_taxonomy'],
'term_ids' => [ $hw_term ],
],
],
]
);
$this->factory->product_variation->create(
[
'parent_id' => $product_c,
'attributes' => [ 'pa_shade' => 'light', 'pa_weight' => 'heavy-weight' ],
'image_id' => null,
'regular_price' => 30,
]
);
$this->clearSchema();
$query = '
query ($taxonomyFilter: ProductTaxonomyInput) {
products(where: { taxonomyFilter: $taxonomyFilter }) {
nodes {
databaseId
}
}
}
';
// AND: shade IN (light) AND weight IN (heavy-weight) — should return only product_c.
$variables = [
'taxonomyFilter' => [
'and' => [
[
'taxonomy' => 'PA_SHADE',
'terms' => [ 'light' ],
],
[
'taxonomy' => 'PA_WEIGHT',
'terms' => [ 'heavy-weight' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'products.nodes.#.databaseId', $product_c ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $product_a ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $product_b ),
];
$this->assertQuerySuccessful( $response, $expected );
// OR: shade IN (dark) OR weight IN (light-weight) — should return product_a and product_b.
$variables = [
'taxonomyFilter' => [
'or' => [
[
'taxonomy' => 'PA_SHADE',
'terms' => [ 'dark' ],
],
[
'taxonomy' => 'PA_WEIGHT',
'terms' => [ 'light-weight' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'products.nodes.#.databaseId', $product_a ),
$this->expectedField( 'products.nodes.#.databaseId', $product_b ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $product_c ),
];
$this->assertQuerySuccessful( $response, $expected );
// AND with multiple terms: shade IN (light, dark) AND weight IN (heavy-weight)
// — should return product_b and product_c.
$variables = [
'taxonomyFilter' => [
'and' => [
[
'taxonomy' => 'PA_SHADE',
'terms' => [ 'light', 'dark' ],
],
[
'taxonomy' => 'PA_WEIGHT',
'terms' => [ 'heavy-weight' ],
],
],
],
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->expectedField( 'products.nodes.#.databaseId', $product_b ),
$this->expectedField( 'products.nodes.#.databaseId', $product_c ),
$this->not()->expectedField( 'products.nodes.#.databaseId', $product_a ),
];
$this->assertQuerySuccessful( $response, $expected );
}
/**
* Test basic products query with categoryId, status, visibility, and pagination.
*
* Mirrors the exact query pattern from #891 that stopped returning products.
*
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/891
*/
public function testProductsQueryWithCategoryStatusVisibilityAndPagination() {
$category = $this->factory->product->createProductCategory( 'wetsuits' );
// Create 3 published products in the category.
$product_ids = [];
for ( $i = 0; $i < 3; $i++ ) {
$product_ids[] = $this->factory->product->createSimple(
[
'category_ids' => [ $category ],
'status' => 'publish',
]
);
}
// Create a draft product in the same category — should not appear.
$draft_product = $this->factory->product->createSimple(
[
'category_ids' => [ $category ],
'status' => 'draft',
]
);
$query = '
query ($categoryId: Int, $first: Int, $after: String) {
products(
where: {
categoryId: $categoryId
status: "publish"
visibility: VISIBLE
}
first: $first
after: $after
) {
nodes {
... on Product {
databaseId
name
status
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
';
// First page — request 2 of 3 products.
$variables = [
'categoryId' => $category,
'first' => 2,
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$expected = [
$this->not()->expectedField( 'products.nodes.#.databaseId', $draft_product ),
];
$this->assertQuerySuccessful( $response, $expected );
$nodes = $this->lodashGet( $response, 'data.products.nodes', [] );
$this->assertCount( 2, $nodes, 'Should return 2 products on first page.' );
$has_next = $this->lodashGet( $response, 'data.products.pageInfo.hasNextPage' );
$this->assertTrue( $has_next, 'Should have a next page.' );
// Second page.
$end_cursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' );
$variables = [
'categoryId' => $category,
'first' => 2,
'after' => $end_cursor,
];
$response = $this->graphql( compact( 'query', 'variables' ) );
$this->assertQuerySuccessful( $response, [] );
$nodes = $this->lodashGet( $response, 'data.products.nodes', [] );
$this->assertCount( 1, $nodes, 'Should return 1 product on second page.' );
$has_next = $this->lodashGet( $response, 'data.products.pageInfo.hasNextPage' );
$this->assertFalse( $has_next, 'Should not have a next page.' );
// All returned products should be from the category.
foreach ( $product_ids as $id ) {
$product = wc_get_product( $id );
$this->assertContains( $category, $product->get_category_ids() );
}
}
}