From ff197207ef9e5a6d84be365440497ba9ac0785a3 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Fri, 30 Aug 2024 08:48:54 +0200 Subject: [PATCH] Product Collection: Don't render when empty unless the no results block is present (#50404) * Disable PC render when content empty unless no results block is present * Introduce empty PC classname * Add E2E test * Add changelog entry * Ensure the render is preserved as expected * Do not render empty div * Update tests * Try a different logic * Add hint to the docblock * Remove obsolete e2e test * Update render handler name to acknowledge context * Fix tests failing due to recent v3 update * Restore block interactivity --------- Co-authored-by: Manish Menaria --- .../product-collection.block_theme.spec.ts | 81 +++++++++++++++++++ .../fix-pc-prevent-rendering-on-empty-query | 4 + .../Blocks/BlockTypes/ProductCollection.php | 76 ++++++++++++++++- 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-pc-prevent-rendering-on-empty-query diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts index 788f27b123a..af0839c5ca1 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts @@ -81,6 +81,87 @@ test.describe( 'Product Collection', () => { ).toBeVisible(); } ); + test.describe( 'when no results are found', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'does not render', async ( { page, editor, pageObject } ) => { + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( 'featured' ); + await pageObject.addFilter( 'Price Range' ); + await pageObject.setPriceRange( { + max: '1', + } ); + + const featuredBlock = editor.canvas.getByLabel( 'Block: Featured' ); + + await expect( + featuredBlock.getByText( 'Featured products' ) + ).toBeVisible(); + // The "No results found" info is rendered in editor for all collections. + await expect( + featuredBlock.getByText( 'No results found' ) + ).toBeVisible(); + + await pageObject.publishAndGoToFrontend(); + + const content = page.locator( 'main' ); + + await expect( content ).not.toContainText( 'Featured products' ); + await expect( content ).not.toContainText( 'No results found' ); + } ); + + // This test ensures the runtime render state is correctly reset for + // each block. + test( 'does not prevent subsequent blocks from render', async ( { + page, + pageObject, + } ) => { + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( 'featured' ); + await pageObject.addFilter( 'Price Range' ); + await pageObject.setPriceRange( { + max: '1', + } ); + + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( 'topRated' ); + + await pageObject.refreshLocators( 'editor' ); + await expect( pageObject.products ).toHaveCount( 5 ); + + await pageObject.publishAndGoToFrontend(); + + await pageObject.refreshLocators( 'frontend' ); + await expect( pageObject.products ).toHaveCount( 5 ); + await expect( page.locator( 'main' ) ).not.toContainText( + 'Featured products' + ); + } ); + + test( 'renders if No Results block is present', async ( { + page, + editor, + pageObject, + } ) => { + await pageObject.insertProductCollection(); + await pageObject.chooseCollectionInPost( 'productCatalog' ); + await pageObject.addFilter( 'Price Range' ); + await pageObject.setPriceRange( { + max: '1', + } ); + + await expect( + editor.canvas.getByText( 'No results found' ) + ).toBeVisible(); + + await pageObject.publishAndGoToFrontend(); + + await expect( page.getByText( 'No results found' ) ).toBeVisible(); + } ); + } ); + test.describe( 'Renders correctly with all Product Elements', () => { const expectedProductContent = [ 'Beanie', // core/post-title diff --git a/plugins/woocommerce/changelog/fix-pc-prevent-rendering-on-empty-query b/plugins/woocommerce/changelog/fix-pc-prevent-rendering-on-empty-query new file mode 100644 index 00000000000..bbdaf94ece0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-pc-prevent-rendering-on-empty-query @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Product Collection: Don't render when empty unless the no results block is present. diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php index bf42d344a52..f2f9f72328a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php @@ -47,6 +47,18 @@ class ProductCollection extends AbstractBlock { protected $custom_order_opts = array( 'popularity', 'rating' ); + /** + * The render state of the product collection block. + * + * These props are runtime-based and reinitialize for every block on a page. + * + * @var array + */ + private $render_state = array( + 'has_results' => false, + 'has_no_results_block' => false, + ); + /** * Initialize this block type. * @@ -80,8 +92,30 @@ class ProductCollection extends AbstractBlock { // Provide location context into block's context. add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 ); + // Disable block render if the ProductTemplate block is empty. + add_filter( + 'render_block_woocommerce/product-template', + function ( $html ) { + $this->render_state['has_results'] = ! empty( $html ); + return $html; + }, + 100, + 1 + ); + + // Enable block render if the ProductCollectionNoResults block is rendered. + add_filter( + 'render_block_woocommerce/product-collection-no-results', + function ( $html ) { + $this->render_state['has_no_results_block'] = ! empty( $html ); + return $html; + }, + 100, + 1 + ); + // Interactivity API: Add navigation directives to the product collection block. - add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 ); + add_filter( 'render_block_woocommerce/product-collection', array( $this, 'handle_rendering' ), 10, 2 ); add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 ); add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 ); @@ -90,6 +124,46 @@ class ProductCollection extends AbstractBlock { add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 ); } + /** + * Handle the rendering of the block. + * + * @param string $block_content The block content about to be rendered. + * @param array $block The block being rendered. + * + * @return string + */ + public function handle_rendering( $block_content, $block ) { + if ( $this->should_prevent_render() ) { + return ''; // Prevent rendering. + } + + // Reset the render state for the next render. + $this->reset_render_state(); + + return $this->enhance_product_collection_with_interactivity( $block_content, $block ); + } + + /** + * Check if the block should be prevented from rendering. + * + * @return bool + */ + private function should_prevent_render() { + return ! $this->render_state['has_results'] && ! $this->render_state['has_no_results_block']; + } + + /** + * Reset the render state. + */ + private function reset_render_state() { + $this->render_state = array( + 'has_results' => false, + 'has_no_results_block' => false, + ); + } + + + /** * Provides the location context to each inner block of the product collection block. * Hint: Only blocks using the 'query' context will be affected.