Files
wp-graphql-woocommerce/tests/wpunit/ProductAttributeConnectionsTest.php
974393aeeb fix: Sanitize ProductAttribute name to match VariationAttribute [#965] (#966)
* 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>
2026-03-26 20:10:08 -04:00

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 );
}
}