Merge branch 'add/49335-related-products-collection' into add/pc-hand-picked-collection

This commit is contained in:
Christopher Allford 2024-09-18 16:32:10 -07:00 committed by GitHub
commit 06acb46aec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 614 additions and 70 deletions

View File

@ -21,6 +21,7 @@ import bestSellers from './best-sellers';
import onSale from './on-sale';
import featured from './featured';
import handPicked from './hand-picked';
import related from './related';
const collections: BlockVariation[] = [
productCollection,
@ -30,6 +31,7 @@ const collections: BlockVariation[] = [
bestSellers,
newArrivals,
handPicked,
related,
];
export const registerCollections = () => {

View File

@ -0,0 +1,55 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, loop } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { INNER_BLOCKS_PRODUCT_TEMPLATE } from '../constants';
import { CoreCollectionNames, LayoutOptions } from '../types';
const collection = {
name: CoreCollectionNames.RELATED,
title: __( 'Related Products', 'woocommerce' ),
icon: <Icon icon={ loop } />,
description: __( 'Recommend products like this one.', 'woocommerce' ),
keywords: [ 'product collection' ],
scope: [],
usesReference: [ 'product' ],
};
const attributes = {
displayLayout: {
type: LayoutOptions.GRID,
columns: 4,
shrinkColumns: true,
},
query: {
perPage: 4,
pages: 1,
},
};
const heading: InnerBlockTemplate = [
'core/heading',
{
textAlign: 'center',
level: 2,
content: __( 'Related Products', 'woocommerce' ),
style: { spacing: { margin: { bottom: '1rem' } } },
},
];
const innerBlocks: InnerBlockTemplate[] = [
heading,
INNER_BLOCKS_PRODUCT_TEMPLATE,
];
export default {
...collection,
attributes,
innerBlocks,
};

View File

@ -159,6 +159,7 @@ export enum CoreCollectionNames {
ON_SALE = 'woocommerce/product-collection/on-sale',
TOP_RATED = 'woocommerce/product-collection/top-rated',
HAND_PICKED = 'woocommerce/product-collection/hand-picked',
RELATED = 'woocommerce/product-collection/related',
}
export enum CoreFilterNames {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
New "Related Products" Product Collection type.

View File

@ -3,6 +3,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use InvalidArgumentException;
use WP_Query;
use WC_Tax;
@ -18,6 +19,14 @@ class ProductCollection extends AbstractBlock {
*/
protected $block_name = 'product-collection';
/**
* An associative array of collection handlers.
*
* @var array<string, callable> $collection_handler_store
* Keys are collection names, values are callable handlers for custom collection behavior.
*/
protected $collection_handler_store = array();
/**
* The Block with its attributes before it gets rendered
*
@ -123,6 +132,8 @@ class ProductCollection extends AbstractBlock {
// Disable client-side-navigation if incompatible blocks are detected.
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
$this->register_core_collections();
}
/**
@ -624,22 +635,34 @@ class ProductCollection extends AbstractBlock {
/**
* Update the query for the product query block in Editor.
*
* @param array $args Query args.
* @param array $query Query args.
* @param WP_REST_Request $request Request.
*/
public function update_rest_query_in_editor( $args, $request ): array {
public function update_rest_query_in_editor( $query, $request ): array {
// Only update the query if this is a product collection block.
$is_product_collection_block = $request->get_param( 'isProductCollectionBlock' );
if ( ! $is_product_collection_block ) {
return $args;
return $query;
}
// Is this a preview mode request?
// If yes, short-circuit the query and return the preview query args.
$product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
$is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
if ( 'true' === $is_preview ) {
return $this->get_preview_query_args( $args, $request );
$collection_args = array(
'name' => $product_collection_query_context['collection'] ?? '',
// The editor uses a REST query to grab product post types. This means we don't have a block
// instance to work with and the client needs to provide the location context.
'productCollectionLocation' => $request->get_param( 'productCollectionLocation' ),
);
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['editor_args'] ) ) {
$collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query, $request );
}
// When requested, short-circuit the query and return the preview query args.
$preview_state = $request->get_param( 'previewState' );
if ( isset( $preview_state['isPreview'] ) && 'true' === $preview_state['isPreview'] ) {
return $this->get_preview_query_args( $collection_args, $query, $request );
}
$orderby = $request->get_param( 'orderBy' );
@ -652,10 +675,11 @@ class ProductCollection extends AbstractBlock {
$price_range = $request->get_param( 'priceRange' );
// This argument is required for the tests to PHP Unit Tests to run correctly.
// Most likely this argument is being accessed in the test environment image.
$args['author'] = '';
$query['author'] = '';
$final_query_args = $this->get_final_query_args(
$args,
$final_query = $this->get_final_query_args(
$collection_args,
$query,
array(
'collection' => $product_collection_query_context['collection'] ?? '',
'orderby' => $orderby,
@ -669,7 +693,7 @@ class ProductCollection extends AbstractBlock {
)
);
return $final_query_args;
return $final_query;
}
/**
@ -726,28 +750,40 @@ class ProductCollection extends AbstractBlock {
$is_exclude_applied_filters = ! ( $inherit || $filterable );
return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
$collection_args = array(
'name' => $block->context['collection'] ?? '',
'productCollectionLocation' => $block->context['productCollectionLocation'] ?? null,
);
return $this->get_final_frontend_query(
$collection_args,
$block_context_query,
$page,
$is_exclude_applied_filters
);
}
/**
* Get the final query arguments for the frontend.
*
* @param array $collection_args Any special arguments that should change the behavior of the query.
* @param array $query The query arguments.
* @param int $page The page number.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_frontend_query( $query, $page = 1, $is_exclude_applied_filters = false ) {
private function get_final_frontend_query( $collection_args, $query, $page = 1, $is_exclude_applied_filters = false ) {
$product_ids = $query['post__in'] ?? array();
$offset = $query['offset'] ?? 0;
$per_page = $query['perPage'] ?? 9;
$common_query_values = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(),
'posts_per_page' => $query['perPage'],
'posts_per_page' => $per_page,
'order' => $query['order'],
'offset' => ( $per_page * ( $page - 1 ) ) + $offset,
'post__in' => array(),
'post__in' => $product_ids,
'post_status' => 'publish',
'post_type' => 'product',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
@ -763,7 +799,14 @@ class ProductCollection extends AbstractBlock {
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['frontend_args'] ) ) {
$collection_args = call_user_func( $handlers['frontend_args'], $collection_args, $query );
}
$final_query = $this->get_final_query_args(
$collection_args,
$common_query_values,
array(
'collection' => $this->parsed_block['attrs']['collection'] ?? '',
@ -786,14 +829,19 @@ class ProductCollection extends AbstractBlock {
/**
* Get final query args based on provided values
*
* @param array $collection_args Any special arguments that should change the behavior of the query.
* @param array $common_query_values Common query values.
* @param array $query Query from block context.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'], $query['collection'] );
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
private function get_final_query_args(
$collection_args,
$common_query_values,
$query,
$is_exclude_applied_filters = false
) {
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : array();
$featured_query = $this->get_featured_query( $query['featured'] ?? false );
@ -802,43 +850,54 @@ class ProductCollection extends AbstractBlock {
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
$merged_query = $this->merge_queries(
// Allow collections to provide their own query parameters.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['build_query'] ) ) {
$collection_query = call_user_func(
$handlers['build_query'],
$collection_args,
$common_query_values,
$query,
$is_exclude_applied_filters
);
} else {
$collection_query = array();
}
return $this->merge_queries(
$common_query_values,
$orderby_query,
$handpicked_query,
$on_sale_query,
$stock_query,
$tax_query,
$applied_filters_query,
$date_query,
$price_query_args
$price_query_args,
$handpicked_query,
$collection_query
);
return $merged_query;
}
/**
* Get query args for preview mode. These query args will be used with WP_Query to fetch the products.
*
* @param array $collection_args Any collection-specific arguments.
* @param array $args Query args.
* @param WP_REST_Request $request Request.
*/
private function get_preview_query_args( $args, $request ) {
private function get_preview_query_args( $collection_args, $args, $request ) {
$collection_query = array();
/**
* In future, Here we will modify the preview query based on the collection name. For example:
*
* $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
* $collection_name = $product_collection_query_context['collection'] ?? '';
* if ( 'woocommerce/product-collection/on-sale' === $collection_name ) {
* $collection_query = $this->get_on_sale_products_query( true );
* }.
*/
// Allow collections to override the preview mode behavior.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['preview_query'] ) ) {
$collection_query = call_user_func( $handlers['preview_query'], $collection_args, $args, $request );
}
$args = $this->merge_queries( $args, $collection_query );
return $args;
@ -867,38 +926,85 @@ class ProductCollection extends AbstractBlock {
* @return array
*/
private function merge_queries( ...$queries ) {
// Rather than a simple merge, some query vars should be held aside and merged differently.
$special_query_vars = array(
'post__in' => array(),
);
$special_query_keys = array_keys( $special_query_vars );
$merged_query = array_reduce(
$queries,
function ( $acc, $query ) {
function ( $acc, $query ) use ( $special_query_keys, &$special_query_vars ) {
if ( ! is_array( $query ) ) {
return $acc;
}
// If the $query doesn't contain any valid query keys, we unpack/spread it then merge.
if ( empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
// When the $query has keys but doesn't contain any valid query keys, we unpack/spread it then merge.
if ( ! empty( $query ) && empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
return $this->merge_queries( $acc, ...array_values( $query ) );
}
$result = $this->array_merge_recursive_replace_non_array_properties( $acc, $query );
// The post__in query needs special handling, as it should be an
// intersection of all post__in queries.
if (
is_array( $acc['post__in'] ?? null ) &&
! empty( $acc['post__in'] ) &&
is_array( $query['post__in'] ?? null ) &&
! empty( $query['post__in'] )
) {
$result['post__in'] = array_intersect( $acc['post__in'], $query['post__in'] );
// Pull out the special query vars so we can merge them separately.
foreach ( $special_query_keys as $query_var ) {
if ( isset( $query[ $query_var ] ) ) {
$special_query_vars[ $query_var ][] = $query[ $query_var ];
unset( $query[ $query_var ] );
}
}
return $result;
return $this->array_merge_recursive_replace_non_array_properties( $acc, $query );
},
array()
);
// Perform any necessary special merges.
$merged_query['post__in'] = $this->merge_post__in( ...$special_query_vars['post__in'] );
return $merged_query;
}
/**
* Merge all of the 'post__in' values and return an array containing only values that are present in all arrays.
*
* @param int[][] ...$post__in The 'post__in' values to be merged.
*
* @return int[] The merged 'post__in' values.
*/
private function merge_post__in( ...$post__in ) {
if ( empty( $post__in ) ) {
return array();
}
// Since we're using array_intersect, any array that is empty will result
// in an empty output array. To avoid this we need to make sure every
// argument is a non-empty array.
$post__in = array_filter(
$post__in,
function ( $val ) {
return is_array( $val ) && ! empty( $val );
}
);
if ( empty( $post__in ) ) {
return array();
}
// Since the 'post__in' filter is exclusionary we need to use an intersection of
// all of the arrays. This ensures one query doesn't add options that another
// has otherwise excluded from the results.
if ( count( $post__in ) > 1 ) {
$post__in = array_intersect( ...$post__in );
// An empty array means that there was no overlap between the filters and so
// the query should return no results.
if ( empty( $post__in ) ) {
return array( -1 );
}
} else {
$post__in = reset( $post__in );
}
return array_values( array_unique( $post__in, SORT_NUMERIC ) );
}
/**
* Return query params to support custom sort values
*
@ -1175,6 +1281,23 @@ class ProductCollection extends AbstractBlock {
);
}
/**
* Generates a post__in query to filter products to the set of provided IDs.
*
* @param int[]|false $handpicked_products The products to filter.
*
* @return array The post__in query.
*/
private function get_handpicked_query( $handpicked_products ) {
if ( false === $handpicked_products ) {
return array();
}
return array(
'post__in' => $handpicked_products,
);
}
/**
* Merge tax_queries from various queries.
@ -1703,4 +1826,87 @@ class ProductCollection extends AbstractBlock {
return $price_filter + array_sum( $taxes );
}
/**
* Registers handlers for a collection.
*
* @param string $collection_name The name of the custom collection.
* @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
* @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
* @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
* @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
*
* @throws \InvalidArgumentException If collection handlers are already registered for the given collection name.
*/
protected function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
if ( isset( $this->collection_handler_store[ $collection_name ] ) ) {
throw new \InvalidArgumentException( 'Collection handlers already registered for ' . esc_html( $collection_name ) );
}
$this->collection_handler_store[ $collection_name ] = array(
'build_query' => $build_query,
'frontend_args' => $frontend_args,
'editor_args' => $editor_args,
'preview_query' => $preview_query,
);
}
/**
* Registers any handlers for the core collections.
*/
protected function register_core_collections() {
$this->register_collection_handlers(
'woocommerce/product-collection/related',
function ( $collection_args ) {
// No products should be shown if no related product reference is set.
if ( empty( $collection_args['relatedProductReference'] ) ) {
return array(
'post__in' => array( -1 ),
);
}
$related_products = wc_get_related_products(
$collection_args['relatedProductReference'],
// Use a higher limit so that the result set contains enough products for the collection to subsequently filter.
100
);
if ( empty( $related_products ) ) {
return array(
'post__in' => array( -1 ),
);
}
// Have it filter the results to products related to the one provided.
return array(
'post__in' => $related_products,
);
},
function ( $collection_args, $query ) {
$product_reference = $query['productReference'] ?? null;
// Infer the product reference from the location if an explicit product is not set.
if ( empty( $product_reference ) ) {
$location = $collection_args['productCollectionLocation'];
if ( isset( $location['type'] ) && 'product' === $location['type'] ) {
$product_reference = $location['sourceData']['productId'];
}
}
$collection_args['relatedProductReference'] = $product_reference;
return $collection_args;
},
function ( $collection_args, $query, $request ) {
$product_reference = $request->get_param( 'productReference' );
// In some cases the editor will send along block location context that we can infer the product reference from.
if ( empty( $product_reference ) ) {
$location = $collection_args['productCollectionLocation'];
if ( isset( $location['type'] ) && 'product' === $location['type'] ) {
$product_reference = $location['sourceData']['productId'];
}
}
$collection_args['relatedProductReference'] = $product_reference;
return $collection_args;
}
);
}
}

View File

@ -60,8 +60,9 @@ class ProductCollection extends \WP_UnitTestCase {
* Build the merged_query for testing
*
* @param array $parsed_block Parsed block data.
* @param array $query Query data.
*/
private function initialize_merged_query( $parsed_block = array() ) {
private function initialize_merged_query( $parsed_block = array(), $query = array() ) {
if ( empty( $parsed_block ) ) {
$parsed_block = $this->get_base_parsed_block();
}
@ -71,8 +72,6 @@ class ProductCollection extends \WP_UnitTestCase {
$block = new \stdClass();
$block->context = $parsed_block['attrs'];
$query = build_query_vars_from_query_block( $block, 1 );
return $this->block_instance->build_frontend_query( $query, $block, 1 );
}
@ -598,14 +597,27 @@ class ProductCollection extends \WP_UnitTestCase {
* - Product tags
*/
public function test_merging_taxonomies_query() {
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['query']['taxQuery'] = array(
'product_cat' => array( 1, 2 ),
'product_tag' => array( 3, 4 ),
$merged_query = $this->initialize_merged_query(
null,
// Since we aren't calling the Query Loop build function, we need to provide
// a tax_query rather than relying on it generating one from the input.
array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array(
array(
'taxonomy' => 'product_cat',
'terms' => array( 1, 2 ),
'include_children' => false,
),
array(
'taxonomy' => 'product_tag',
'terms' => array( 3, 4 ),
'include_children' => false,
),
),
)
);
$merged_query = $this->initialize_merged_query( $parsed_block );
$this->assertContains(
array(
'taxonomy' => 'product_cat',
@ -632,7 +644,9 @@ class ProductCollection extends \WP_UnitTestCase {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );
$args = array();
$args = array(
'posts_per_page' => 9,
);
$request = $this->build_request();
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
@ -666,7 +680,9 @@ class ProductCollection extends \WP_UnitTestCase {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );
$args = array();
$args = array(
'posts_per_page' => 9,
);
$time_frame_date = gmdate( 'Y-m-d H:i:s' );
$params = array(
'featured' => 'true',
@ -937,4 +953,240 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertStringContainsString( "( wc_product_meta_lookup.tax_class = 'collection-test' AND wc_product_meta_lookup.`min_price` >= 1.", $query->request );
$this->assertStringContainsString( "( wc_product_meta_lookup.tax_class = 'collection-test' AND wc_product_meta_lookup.`max_price` <= 2.", $query->request );
}
/**
* Test handpicked products queries.
*/
public function test_handpicked_products_queries() {
$handpicked_product_ids = array( 1, 2, 3, 4 );
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
$merged_query = $this->initialize_merged_query( $parsed_block );
foreach ( $handpicked_product_ids as $id ) {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
$this->assertCount( 4, $merged_query['post__in'] );
}
/**
* Test merging exclusive id filters.
*/
public function test_merges_post__in() {
$existing_id_filter = array( 1, 4 );
$handpicked_product_ids = array( 3, 4, 5, 6 );
// The only ID present in ALL of the exclusive filters is 4.
$expected_product_ids = array( 4 );
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['query']['post__in'] = $existing_id_filter;
$parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
$merged_query = $this->initialize_merged_query( $parsed_block );
foreach ( $expected_product_ids as $id ) {
$this->assertContainsEquals( $id, $merged_query['post__in'] );
}
$this->assertCount( 1, $merged_query['post__in'] );
}
/**
* Test merging exclusive id filters with no intersection.
*/
public function test_merges_post__in_empty_result_without_intersection() {
$existing_id_filter = array( 1, 4 );
$handpicked_product_ids = array( 2, 3 );
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['query']['post__in'] = $existing_id_filter;
$parsed_block['attrs']['query']['woocommerceHandPickedProducts'] = $handpicked_product_ids;
$merged_query = $this->initialize_merged_query( $parsed_block );
$this->assertEquals( array( -1 ), $merged_query['post__in'] );
}
/**
* Test for frontend collection handlers.
*/
public function test_frontend_collection_handlers() {
$build_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$frontend_args = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers( 'test-collection', $build_query, $frontend_args );
$frontend_args->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$collection_args['test'] = 'test-arg';
return $collection_args;
}
);
$build_query->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$this->assertArrayHasKey( 'test', $collection_args );
$this->assertEquals( 'test-arg', $collection_args['test'] );
return array(
'post__in' => array( 111 ),
);
}
);
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['collection'] = 'test-collection';
$merged_query = $this->initialize_merged_query( $parsed_block );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 111, $merged_query['post__in'] );
}
/**
* Test for editor collection handlers.
*/
public function test_editor_collection_handlers() {
$build_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$editor_args = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers( 'test-collection', $build_query, null, $editor_args );
$editor_args->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$collection_args['test'] = 'test-arg';
return $collection_args;
}
);
$build_query->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$this->assertArrayHasKey( 'test', $collection_args );
$this->assertEquals( 'test-arg', $collection_args['test'] );
return array(
'post__in' => array( 111 ),
);
}
);
$args = array();
$request = $this->build_request();
$request->set_param(
'productCollectionQueryContext',
array(
'collection' => 'test-collection',
)
);
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 111, $updated_query['post__in'] );
}
/**
* Test for the editor preview collection handler.
*/
public function test_editor_preview_collection_handler() {
$preview_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers(
'test-collection',
function () {
return array();
},
null,
null,
$preview_query
);
$preview_query->expects( $this->once() )
->method( '__invoke' )
->willReturn(
array(
'post__in' => array( 123 ),
)
);
$args = array();
$request = $this->build_request();
$request->set_param(
'productCollectionQueryContext',
array(
'collection' => 'test-collection',
)
);
$request->set_param(
'previewState',
array(
'isPreview' => 'true',
)
);
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 123, $updated_query['post__in'] );
}
/**
* Tests that the related products collection handler works as expected.
*/
public function test_collection_related_products() {
$related_filter = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$expected_product_ids = array( 2, 3, 4 );
// This filter will turn off the data store so we don't need dummy products.
add_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
$related_filter->expects( $this->exactly( 2 ) )
->method( '__invoke' )
->with( array(), 1 )
->willReturn( $expected_product_ids );
add_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ), 10, 2 );
// Frontend.
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['collection'] = 'woocommerce/product-collection/related';
$parsed_block['attrs']['query']['productReference'] = 1;
$result_frontend = $this->initialize_merged_query( $parsed_block );
// Editor.
$request = $this->build_request(
array( 'productReference' => 1 )
);
$request->set_param(
'productCollectionQueryContext',
array(
'collection' => 'woocommerce/product-collection/related',
)
);
$result_editor = $this->block_instance->update_rest_query_in_editor( array(), $request );
remove_filter( 'woocommerce_product_related_posts_force_display', '__return_true', 0 );
remove_filter( 'woocommerce_related_products', array( $related_filter, '__invoke' ) );
$this->assertEqualsCanonicalizing( $expected_product_ids, $result_frontend['post__in'] );
$this->assertEqualsCanonicalizing( $expected_product_ids, $result_editor['post__in'] );
}
}

View File

@ -9,6 +9,8 @@ use Automattic\WooCommerce\Blocks\Assets\Api;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
// phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
/**
* ProductCollectionMock used to test Product Query block functions.
*/
@ -26,10 +28,10 @@ class ProductCollectionMock extends ProductCollection {
}
/**
* For now don't need to initialize anything in tests so let's
* just override the default behaviour.
* Override the normal initialization behavior to prevent registering the block with WordPress filters.
*/
protected function initialize() {
$this->register_core_collections();
}
/**
@ -49,4 +51,26 @@ class ProductCollectionMock extends ProductCollection {
public function set_attributes_filter_query_args( $data ) {
$this->attributes_filter_query_args = $data;
}
/**
* Makes a protected method public so that it can be used in tests.
*
* @param string $collection_name The name of the custom collection.
* @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
* @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
* @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
* @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
*/
public function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
parent::register_collection_handlers( $collection_name, $build_query, $frontend_args, $editor_args, $preview_query );
}
/**
* Removes any custom collection handlers for the given collection.
*
* @param string $collection_name The name of the collection to unregister.
*/
public function unregister_collection_handlers( $collection_name ) {
unset( $this->collection_handlers[ $collection_name ] );
}
}