mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* feat: Add support for brands (#918) * feat: add product brand support with where args, taxonomy filter, and taxonomy mutation input fields - Register product_brand taxonomy with WPGraphQL (fix indentation from #964) - Add productBrand/productBrandIn/productBrandNotIn/productBrandId/productBrandIdIn/productBrandIdNotIn where args on products connection - Add product_brand to taxonomy args map and case statements in product connection resolver - Register additional input fields (display, menuOrder, imageId) on CreateProductCategoryInput and UpdateProductCategoryInput mutations - Split ProductTaxonomyQueriesTest into ProductCategoryQueriesTest, ProductTagQueriesTest, ProductCategoryMutationsTest, ProductTagMutationsTest - Add ProductBrandQueriesTest with 8 tests covering queries, connections, hierarchy, where args, taxonomy filter, and CRUD mutations --------- Co-authored-by: Geoff Taylor <geoff@axistaylor.com>
463 lines
11 KiB
PHP
463 lines
11 KiB
PHP
<?php
|
|
|
|
class ProductBrandQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase {
|
|
/**
|
|
* Helper: create a product brand term.
|
|
*
|
|
* @param string $name Term name.
|
|
* @param int $parent_id Optional parent term ID.
|
|
*
|
|
* @return int Term ID.
|
|
*/
|
|
private function createProductBrand( $name, $parent_id = 0 ) {
|
|
$args = [];
|
|
if ( $parent_id ) {
|
|
$args['parent'] = $parent_id;
|
|
}
|
|
|
|
$existing = term_exists( $name, 'product_brand' );
|
|
if ( $existing ) {
|
|
return (int) $existing['term_id'];
|
|
}
|
|
|
|
$term = wp_insert_term( $name, 'product_brand', $args );
|
|
$this->assertNotWPError( $term, "Failed to create product_brand term: {$name}" );
|
|
|
|
return (int) $term['term_id'];
|
|
}
|
|
|
|
/**
|
|
* Helper: create a simple product assigned to the given brand term IDs.
|
|
*
|
|
* @param int[] $brand_ids Brand term IDs.
|
|
*
|
|
* @return int Product ID.
|
|
*/
|
|
private function createProductWithBrands( array $brand_ids ) {
|
|
$product_id = $this->factory->product->createSimple();
|
|
wp_set_object_terms( $product_id, $brand_ids, 'product_brand' );
|
|
|
|
return $product_id;
|
|
}
|
|
|
|
/**
|
|
* Test that the productBrands root query returns brands.
|
|
*/
|
|
public function testProductBrandsQuery() {
|
|
$brand_a = $this->createProductBrand( 'brand-a' );
|
|
$brand_b = $this->createProductBrand( 'brand-b' );
|
|
|
|
$query = '
|
|
query {
|
|
productBrands(first: 100) {
|
|
nodes {
|
|
databaseId
|
|
name
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
$expected = [
|
|
$this->expectedField( 'productBrands.nodes.#.databaseId', $brand_a ),
|
|
$this->expectedField( 'productBrands.nodes.#.databaseId', $brand_b ),
|
|
$this->expectedField( 'productBrands.nodes.#.slug', 'brand-a' ),
|
|
$this->expectedField( 'productBrands.nodes.#.slug', 'brand-b' ),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test that a single productBrand can be queried by slug.
|
|
*/
|
|
public function testSingleProductBrandBySlug() {
|
|
$brand_id = $this->createProductBrand( 'nike' );
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
productBrand(id: $id, idType: SLUG) {
|
|
databaseId
|
|
name
|
|
slug
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [ 'id' => 'nike' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedField( 'productBrand.databaseId', $brand_id ),
|
|
$this->expectedField( 'productBrand.slug', 'nike' ),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test the connection from productBrand to products.
|
|
*/
|
|
public function testProductBrandToProductsConnection() {
|
|
$brand_a = $this->createProductBrand( 'acme' );
|
|
$brand_b = $this->createProductBrand( 'globex' );
|
|
|
|
$acme_product_ids = [
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
];
|
|
$globex_product_ids = [
|
|
$this->createProductWithBrands( [ $brand_b ] ),
|
|
$this->createProductWithBrands( [ $brand_b ] ),
|
|
];
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
productBrand(id: $id, idType: SLUG) {
|
|
databaseId
|
|
slug
|
|
products(first: 100) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Query acme brand — should contain acme products, not globex.
|
|
$variables = [ 'id' => 'acme' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = array_merge(
|
|
[
|
|
$this->expectedField( 'productBrand.databaseId', $brand_a ),
|
|
$this->expectedField( 'productBrand.slug', 'acme' ),
|
|
],
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->expectedField( 'productBrand.products.nodes.#.databaseId', $id );
|
|
},
|
|
$acme_product_ids,
|
|
),
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->not()->expectedField( 'productBrand.products.nodes.#.databaseId', $id );
|
|
},
|
|
$globex_product_ids,
|
|
),
|
|
);
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// Query globex brand — should contain globex products, not acme.
|
|
$variables = [ 'id' => 'globex' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = array_merge(
|
|
[
|
|
$this->expectedField( 'productBrand.databaseId', $brand_b ),
|
|
$this->expectedField( 'productBrand.slug', 'globex' ),
|
|
],
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->expectedField( 'productBrand.products.nodes.#.databaseId', $id );
|
|
},
|
|
$globex_product_ids,
|
|
),
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->not()->expectedField( 'productBrand.products.nodes.#.databaseId', $id );
|
|
},
|
|
$acme_product_ids,
|
|
),
|
|
);
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test the connection from product to productBrands.
|
|
*/
|
|
public function testProductToProductBrandsConnection() {
|
|
$brand_a = $this->createProductBrand( 'adidas' );
|
|
$brand_b = $this->createProductBrand( 'puma' );
|
|
$brand_c = $this->createProductBrand( 'reebok' );
|
|
|
|
// Product with two brands.
|
|
$product_id = $this->createProductWithBrands( [ $brand_a, $brand_b ] );
|
|
|
|
$query = '
|
|
query ($id: ID!) {
|
|
product(id: $id, idType: DATABASE_ID) {
|
|
databaseId
|
|
productBrands {
|
|
nodes {
|
|
databaseId
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [ 'id' => $product_id ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = [
|
|
$this->expectedField( 'product.databaseId', $product_id ),
|
|
$this->expectedField( 'product.productBrands.nodes.#.databaseId', $brand_a ),
|
|
$this->expectedField( 'product.productBrands.nodes.#.databaseId', $brand_b ),
|
|
$this->not()->expectedField( 'product.productBrands.nodes.#.databaseId', $brand_c ),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test hierarchical (parent/child) brand queries.
|
|
*/
|
|
public function testHierarchicalProductBrands() {
|
|
$parent_brand = $this->createProductBrand( 'sportswear' );
|
|
$child_a = $this->createProductBrand( 'running', $parent_brand );
|
|
$child_b = $this->createProductBrand( 'basketball', $parent_brand );
|
|
|
|
// Query top-level brands with children.
|
|
$query = '
|
|
query {
|
|
productBrands(where: { parent: 0 }) {
|
|
nodes {
|
|
databaseId
|
|
slug
|
|
children {
|
|
nodes {
|
|
databaseId
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
$expected = [
|
|
$this->expectedField( 'productBrands.nodes.#.slug', 'sportswear' ),
|
|
];
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// Find the sportswear node and verify children.
|
|
$sportswear_node = null;
|
|
foreach ( $response['data']['productBrands']['nodes'] as $node ) {
|
|
if ( 'sportswear' === $node['slug'] ) {
|
|
$sportswear_node = $node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->assertNotNull( $sportswear_node, 'sportswear brand should exist in response.' );
|
|
$this->assertCount( 2, $sportswear_node['children']['nodes'], 'sportswear should have 2 children.' );
|
|
|
|
$child_slugs = array_column( $sportswear_node['children']['nodes'], 'slug' );
|
|
$this->assertContains( 'running', $child_slugs );
|
|
$this->assertContains( 'basketball', $child_slugs );
|
|
}
|
|
|
|
/**
|
|
* Test filtering products by brand using the where clause.
|
|
*/
|
|
public function testProductsFilteredByBrand() {
|
|
$brand_a = $this->createProductBrand( 'apple' );
|
|
$brand_b = $this->createProductBrand( 'samsung' );
|
|
|
|
$apple_ids = [
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
];
|
|
$samsung_ids = [
|
|
$this->createProductWithBrands( [ $brand_b ] ),
|
|
];
|
|
|
|
$query = '
|
|
query ($brandSlug: String!) {
|
|
products(where: { productBrand: $brandSlug }) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [ 'brandSlug' => 'apple' ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = array_merge(
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->expectedField( 'products.nodes.#.databaseId', $id );
|
|
},
|
|
$apple_ids,
|
|
),
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->not()->expectedField( 'products.nodes.#.databaseId', $id );
|
|
},
|
|
$samsung_ids,
|
|
),
|
|
);
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test filtering products by brand using the taxonomyFilter parameter.
|
|
*/
|
|
public function testProductsFilteredByBrandViaTaxonomyFilter() {
|
|
$brand_a = $this->createProductBrand( 'toyota' );
|
|
$brand_b = $this->createProductBrand( 'honda' );
|
|
|
|
$toyota_ids = [
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
$this->createProductWithBrands( [ $brand_a ] ),
|
|
];
|
|
$honda_ids = [
|
|
$this->createProductWithBrands( [ $brand_b ] ),
|
|
];
|
|
|
|
$query = '
|
|
query ($input: ProductTaxonomyInput) {
|
|
products(where: { taxonomyFilter: $input }) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [
|
|
'input' => [
|
|
'filters' => [
|
|
[
|
|
'taxonomy' => 'PRODUCT_BRAND',
|
|
'terms' => [ 'toyota' ],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
$expected = array_merge(
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->expectedField( 'products.nodes.#.databaseId', $id );
|
|
},
|
|
$toyota_ids,
|
|
),
|
|
array_map(
|
|
function ( $id ) {
|
|
return $this->not()->expectedField( 'products.nodes.#.databaseId', $id );
|
|
},
|
|
$honda_ids,
|
|
),
|
|
);
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
/**
|
|
* Test CRUD operations on product brands using WPGraphQL core term mutations.
|
|
*/
|
|
public function testProductBrandCrudMutations() {
|
|
$this->loginAsShopManager();
|
|
|
|
// Create.
|
|
$create_query = '
|
|
mutation ($input: CreateProductBrandInput!) {
|
|
createProductBrand(input: $input) {
|
|
productBrand {
|
|
databaseId
|
|
name
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql(
|
|
[
|
|
'query' => $create_query,
|
|
'variables' => [
|
|
'input' => [
|
|
'name' => 'New Brand',
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$expected = [
|
|
$this->expectedField( 'createProductBrand.productBrand.databaseId', self::NOT_NULL ),
|
|
$this->expectedField( 'createProductBrand.productBrand.name', 'New Brand' ),
|
|
$this->expectedField( 'createProductBrand.productBrand.slug', 'new-brand' ),
|
|
];
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$brand_id = $response['data']['createProductBrand']['productBrand']['databaseId'];
|
|
|
|
// Update.
|
|
$update_query = '
|
|
mutation ($input: UpdateProductBrandInput!) {
|
|
updateProductBrand(input: $input) {
|
|
productBrand {
|
|
databaseId
|
|
name
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql(
|
|
[
|
|
'query' => $update_query,
|
|
'variables' => [
|
|
'input' => [
|
|
'id' => $this->toRelayId( 'term', $brand_id ),
|
|
'name' => 'Updated Brand',
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$expected = [
|
|
$this->expectedField( 'updateProductBrand.productBrand.databaseId', $brand_id ),
|
|
$this->expectedField( 'updateProductBrand.productBrand.name', 'Updated Brand' ),
|
|
];
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
// Delete.
|
|
$delete_query = '
|
|
mutation ($input: DeleteProductBrandInput!) {
|
|
deleteProductBrand(input: $input) {
|
|
productBrand {
|
|
databaseId
|
|
name
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql(
|
|
[
|
|
'query' => $delete_query,
|
|
'variables' => [
|
|
'input' => [
|
|
'id' => $this->toRelayId( 'term', $brand_id ),
|
|
],
|
|
],
|
|
]
|
|
);
|
|
$expected = [
|
|
$this->expectedField( 'deleteProductBrand.productBrand.databaseId', $brand_id ),
|
|
];
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$this->assertNull( term_exists( $brand_id, 'product_brand' ) );
|
|
}
|
|
}
|