mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* 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
1861 lines
50 KiB
PHP
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() );
|
|
}
|
|
}
|
|
}
|