mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* fix: Sanitize ProductAttribute name to match VariationAttribute [#965] * devops: add unit test for attribute name consistency between product and variation (#965) * devops: fix phpcov merge by using .cov extension and pinning phpcov to v8 * chore: Lintercompliances met * fix: update test expectation for sanitized local attribute name --------- Co-authored-by: Geoff Taylor <geoff@axistaylor.com>
421 lines
11 KiB
PHP
421 lines
11 KiB
PHP
<?php
|
|
|
|
class ProductAttributeConnectionsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase {
|
|
/**
|
|
* Test the root productAttributes query returns global attributes.
|
|
*/
|
|
public function testProductAttributesRootQuery() {
|
|
$color_attr = $this->factory->product->createAttribute( 'color', [ 'red', 'blue', 'green' ] );
|
|
$material_attr = $this->factory->product->createAttribute( 'material', [ 'cotton', 'polyester', 'silk' ] );
|
|
|
|
$this->clearSchema();
|
|
|
|
$query = '
|
|
query {
|
|
productAttributes {
|
|
nodes {
|
|
id
|
|
name
|
|
label
|
|
options
|
|
scope
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
$expected = [
|
|
$this->expectedNode(
|
|
'productAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'pa_color' ),
|
|
$this->expectedField( 'label', self::NOT_NULL ),
|
|
$this->expectedField( 'options', self::NOT_NULL ),
|
|
$this->expectedField( 'scope', 'GLOBAL' ),
|
|
]
|
|
),
|
|
$this->expectedNode(
|
|
'productAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'pa_material' ),
|
|
$this->expectedField( 'label', self::NOT_NULL ),
|
|
$this->expectedField( 'options', self::NOT_NULL ),
|
|
$this->expectedField( 'scope', 'GLOBAL' ),
|
|
]
|
|
),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test the products connection on a GlobalProductAttribute resolved from a product.
|
|
*/
|
|
public function testGlobalProductAttributeToProductsConnection() {
|
|
$weight_attr = $this->factory->product->createAttribute( 'weight', [ 'light', 'heavy' ] );
|
|
|
|
$product_a = $this->factory->product->createVariable(
|
|
[
|
|
'attribute_data' => [ $weight_attr ],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $product_a,
|
|
'attributes' => [ 'weight' => 'light' ],
|
|
'image_id' => null,
|
|
'regular_price' => 10,
|
|
]
|
|
);
|
|
|
|
$product_b = $this->factory->product->createVariable(
|
|
[
|
|
'attribute_data' => [ $weight_attr ],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $product_b,
|
|
'attributes' => [ 'weight' => 'heavy' ],
|
|
'image_id' => null,
|
|
'regular_price' => 20,
|
|
]
|
|
);
|
|
|
|
$unrelated_product = $this->factory->product->createSimple();
|
|
|
|
$this->clearSchema();
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
product(id: $id, idType: DATABASE_ID) {
|
|
... on ProductWithAttributes {
|
|
globalAttributes {
|
|
nodes {
|
|
name
|
|
products {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [ 'id' => $product_a ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedNode(
|
|
'product.globalAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'pa_weight' ),
|
|
]
|
|
),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// The products connection on the attribute should include both products that use this attribute.
|
|
$weight_node = null;
|
|
$global_attrs = $this->lodashGet( $response, 'data.product.globalAttributes.nodes', [] );
|
|
foreach ( $global_attrs as $node ) {
|
|
if ( 'pa_weight' === $node['name'] ) {
|
|
$weight_node = $node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->assertNotNull( $weight_node, 'pa_weight attribute should exist.' );
|
|
$product_ids = array_column( $weight_node['products']['nodes'], 'databaseId' );
|
|
$this->assertContains( $product_a, $product_ids );
|
|
$this->assertContains( $product_b, $product_ids );
|
|
$this->assertNotContains( $unrelated_product, $product_ids );
|
|
}
|
|
|
|
/**
|
|
* Test the products connection on a LocalProductAttribute resolved from a product.
|
|
*/
|
|
public function testLocalProductAttributeToProductsConnection() {
|
|
// Create a product with a local (custom) attribute.
|
|
$local_attr = new \WC_Product_Attribute();
|
|
$local_attr->set_name( 'Engraving' );
|
|
$local_attr->set_options( [ 'yes', 'no' ] );
|
|
$local_attr->set_position( 0 );
|
|
$local_attr->set_visible( true );
|
|
$local_attr->set_variation( false );
|
|
|
|
$product_id = $this->factory->product->createSimple(
|
|
[
|
|
'attributes' => [ $local_attr ],
|
|
]
|
|
);
|
|
|
|
$other_product = $this->factory->product->createSimple();
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
product(id: $id, idType: DATABASE_ID) {
|
|
... on ProductWithAttributes {
|
|
localAttributes {
|
|
nodes {
|
|
name
|
|
options
|
|
products {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [ 'id' => $product_id ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedNode(
|
|
'product.localAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'engraving' ),
|
|
]
|
|
),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// The local attribute's products connection should include the source product.
|
|
$local_attrs = $this->lodashGet( $response, 'data.product.localAttributes.nodes', [] );
|
|
$engraving_node = null;
|
|
foreach ( $local_attrs as $node ) {
|
|
if ( 'engraving' === $node['name'] ) {
|
|
$engraving_node = $node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->assertNotNull( $engraving_node, 'Engraving attribute should exist.' );
|
|
$product_ids = array_column( $engraving_node['products']['nodes'], 'databaseId' );
|
|
$this->assertContains( $product_id, $product_ids );
|
|
$this->assertNotContains( $other_product, $product_ids );
|
|
}
|
|
|
|
/**
|
|
* Test querying attributes available within a specific product category.
|
|
*/
|
|
public function testProductCategoryToAttributesConnection() {
|
|
$size_attr = $this->factory->product->createAttribute( 'fit', [ 'slim', 'regular', 'loose' ] );
|
|
$color_attr = $this->factory->product->createAttribute( 'shade', [ 'light', 'dark' ] );
|
|
|
|
$clothing_id = $this->factory->product->createProductCategory( 'clothing-attr-test' );
|
|
$food_id = $this->factory->product->createProductCategory( 'food-attr-test' );
|
|
|
|
// Create a product in clothing with 'fit' attribute.
|
|
$clothing_product_id = $this->factory->product->createVariable(
|
|
[
|
|
'category_ids' => [ $clothing_id ],
|
|
'attribute_data' => [ $size_attr ],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $clothing_product_id,
|
|
'attributes' => [ 'fit' => 'slim' ],
|
|
'image_id' => null,
|
|
'regular_price' => 25,
|
|
]
|
|
);
|
|
|
|
// Create a product in clothing with 'shade' attribute.
|
|
$clothing_product_id_2 = $this->factory->product->createVariable(
|
|
[
|
|
'category_ids' => [ $clothing_id ],
|
|
'attribute_data' => [ $color_attr ],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $clothing_product_id_2,
|
|
'attributes' => [ 'shade' => 'dark' ],
|
|
'image_id' => null,
|
|
'regular_price' => 30,
|
|
]
|
|
);
|
|
|
|
// Create a product in food with NO attributes.
|
|
$this->factory->product->createSimple( [ 'category_ids' => [ $food_id ] ] );
|
|
|
|
$this->clearSchema();
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
productCategory(id: $id, idType: SLUG) {
|
|
slug
|
|
productAttributes {
|
|
nodes {
|
|
name
|
|
label
|
|
options
|
|
scope
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Clothing should have both fit and shade attributes.
|
|
$variables = [ 'id' => 'clothing-attr-test' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedField( 'productCategory.slug', 'clothing-attr-test' ),
|
|
$this->expectedNode(
|
|
'productCategory.productAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'pa_fit' ),
|
|
$this->expectedField( 'scope', 'GLOBAL' ),
|
|
]
|
|
),
|
|
$this->expectedNode(
|
|
'productCategory.productAttributes.nodes',
|
|
[
|
|
$this->expectedField( 'name', 'pa_shade' ),
|
|
$this->expectedField( 'scope', 'GLOBAL' ),
|
|
]
|
|
),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// Food should have no attributes.
|
|
$variables = [ 'id' => 'food-attr-test' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful( $response, [] );
|
|
$attribute_nodes = $this->lodashGet( $response, 'data.productCategory.productAttributes.nodes', [] );
|
|
$this->assertEmpty( $attribute_nodes, 'Food category should have no product attributes.' );
|
|
}
|
|
|
|
/**
|
|
* Test filtering products by categoryId on an attribute term's products connection.
|
|
*
|
|
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/730
|
|
*/
|
|
public function testAttributeTermProductsFilteredByCategory() {
|
|
$seller_attr = $this->factory->product->createAttribute( 'seller', [ 'acme-store', 'globex-store' ] );
|
|
|
|
$acme_term_id = get_term_by( 'slug', 'acme-store', 'pa_seller' )->term_id;
|
|
$globex_term_id = get_term_by( 'slug', 'globex-store', 'pa_seller' )->term_id;
|
|
|
|
$cat_fruits = $this->factory->product->createProductCategory( 'fruits' );
|
|
$cat_vegs = $this->factory->product->createProductCategory( 'vegetables' );
|
|
|
|
// Acme sells fruits.
|
|
$acme_fruit = $this->factory->product->createVariable(
|
|
[
|
|
'category_ids' => [ $cat_fruits ],
|
|
'attribute_data' => [
|
|
[
|
|
'attribute_id' => $seller_attr['attribute_id'],
|
|
'attribute_taxonomy' => $seller_attr['attribute_taxonomy'],
|
|
'term_ids' => [ $acme_term_id ],
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $acme_fruit,
|
|
'attributes' => [ 'pa_seller' => 'acme-store' ],
|
|
'image_id' => null,
|
|
'regular_price' => 5,
|
|
]
|
|
);
|
|
|
|
// Acme also sells vegetables.
|
|
$acme_veg = $this->factory->product->createVariable(
|
|
[
|
|
'category_ids' => [ $cat_vegs ],
|
|
'attribute_data' => [
|
|
[
|
|
'attribute_id' => $seller_attr['attribute_id'],
|
|
'attribute_taxonomy' => $seller_attr['attribute_taxonomy'],
|
|
'term_ids' => [ $acme_term_id ],
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $acme_veg,
|
|
'attributes' => [ 'pa_seller' => 'acme-store' ],
|
|
'image_id' => null,
|
|
'regular_price' => 3,
|
|
]
|
|
);
|
|
|
|
// Globex sells fruits.
|
|
$globex_fruit = $this->factory->product->createVariable(
|
|
[
|
|
'category_ids' => [ $cat_fruits ],
|
|
'attribute_data' => [
|
|
[
|
|
'attribute_id' => $seller_attr['attribute_id'],
|
|
'attribute_taxonomy' => $seller_attr['attribute_taxonomy'],
|
|
'term_ids' => [ $globex_term_id ],
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$this->factory->product_variation->create(
|
|
[
|
|
'parent_id' => $globex_fruit,
|
|
'attributes' => [ 'pa_seller' => 'globex-store' ],
|
|
'image_id' => null,
|
|
'regular_price' => 8,
|
|
]
|
|
);
|
|
|
|
$this->clearSchema();
|
|
|
|
// Query acme-store's products filtered by fruits category.
|
|
$query = '
|
|
query ($slug: [String!], $categoryId: Int) {
|
|
allPaSeller(where: { slug: $slug }) {
|
|
nodes {
|
|
name
|
|
products(where: { categoryId: $categoryId }) {
|
|
nodes {
|
|
databaseId
|
|
productCategories {
|
|
nodes {
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [
|
|
'slug' => [ 'acme-store' ],
|
|
'categoryId' => $cat_fruits,
|
|
];
|
|
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedField( 'allPaSeller.nodes.0.products.nodes.#.databaseId', $acme_fruit ),
|
|
$this->not()->expectedField( 'allPaSeller.nodes.0.products.nodes.#.databaseId', $acme_veg ),
|
|
$this->not()->expectedField( 'allPaSeller.nodes.0.products.nodes.#.databaseId', $globex_fruit ),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
}
|