Implement the Block Hooks API to automatically inject the Mini-Cart block (https://github.com/woocommerce/woocommerce-blocks/pull/11745)

* Change the default for Mini Cart block

The Block Hooks API currently doesn’t allow for setting the default state of the block injected into content so this ensures the mini-cart block has a better default state for injection. The current default (displaying total value in cart) takes up more width increasing the risk of poor layout.

* Utilize Block Hooks to automatically inject mini-cart block.

* include experimental prefix on filters

* Fix filter name.

* remove experimental prefix.

On thinking about this, I don’t think these need to be experimental. They are intentionally provided as escape hatches for hosts/themes that want to opt-in/out so we’ll have to support them when this is shipped (at least until its no longer needed!)

* fix variable name!

* fix unit tests because of new default

* remove another incorrect text expectation

Defaults for the block affect this expectation.

* fix E2E tests

* Mini Cart Block: improve E2E test

* fix: improve check for the Product Collection block

---------

Co-authored-by: Luigi Teschio <gigitux@gmail.com>
This commit is contained in:
Darren Ethier 2023-12-04 11:27:27 -05:00 committed by GitHub
parent 5b0e74383b
commit 795f008952
8 changed files with 148 additions and 39 deletions

View File

@ -35,7 +35,7 @@
},
"hasHiddenPrice": {
"type": "boolean",
"default": false
"default": true
},
"cartAndCheckoutRenderStyle": {
"type": "string",

View File

@ -58,7 +58,7 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
contents = '',
miniCartIcon,
addToCartBehaviour = 'none',
hasHiddenPrice = false,
hasHiddenPrice = true,
priceColor = defaultColorItem,
iconColor = defaultColorItem,
productCountColor = defaultColorItem,

View File

@ -202,7 +202,7 @@ describe( 'Testing Mini-Cart', () => {
it( 'renders cart price if "Hide Cart Price" setting is not enabled', async () => {
mockEmptyCart();
render( <MiniCartBlock /> );
render( <MiniCartBlock hasHiddenPrice={ false } /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
await waitFor( () =>
@ -212,9 +212,7 @@ describe( 'Testing Mini-Cart', () => {
it( 'does not render cart price if "Hide Cart Price" setting is enabled', async () => {
mockEmptyCart();
const { container } = render(
<MiniCartBlock hasHiddenPrice={ true } />
);
const { container } = render( <MiniCartBlock /> );
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
await waitFor( () =>

View File

@ -72,6 +72,7 @@ class MiniCart extends AbstractBlock {
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_empty_cart_message_block_pattern' ) );
add_action( 'hooked_block_types', array( $this, 'register_auto_insert' ), 10, 4 );
add_action( 'wp_print_footer_scripts', array( $this, 'print_lazy_load_scripts' ), 2 );
}
@ -577,6 +578,104 @@ class MiniCart extends AbstractBlock {
);
}
/**
* Callback for `hooked_block_types` to auto-inject the mini-cart block into headers after navigation.
*
* @param array $hooked_blocks An array of block slugs hooked into a given context.
* @param string $position Position of the block insertion point.
* @param string $anchor_block The block acting as the anchor for the inserted block.
* @param \WP_Block_Template|array $context Where the block is embedded.
* @since $VID:$
* @return array An array of block slugs hooked into a given context.
*/
public function register_auto_insert( $hooked_blocks, $position, $anchor_block, $context ) {
// Cache for active theme.
static $active_theme_name = null;
if ( is_null( $active_theme_name ) ) {
$active_theme_name = wp_get_theme()->get( 'Name' );
}
/**
* A list of pattern slugs to exclude from auto-insert (useful when
* there are patterns that have a very specific location for the block)
*
* @since $VID:$
*/
$pattern_exclude_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_pattern_exclude_list', [] );
/**
* A list of theme slugs to execute this with. This is a temporary
* measure until improvements to the Block Hooks API allow for exposing
* to all block themes.
*
* @since $VID:$
*/
$theme_include_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_theme_include_list', [ 'Twenty Twenty-Four' ] );
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
if (
'after' === $position &&
'core/navigation' === $anchor_block &&
$this->is_header_part_or_pattern( $context ) &&
! $this->pattern_is_excluded( $context, $pattern_exclude_list ) &&
! $this->has_mini_cart_block( $context )
) {
$hooked_blocks[] = 'woocommerce/' . $this->block_name;
}
}
return $hooked_blocks;
}
/**
* Returns whether the pattern is excluded or not
*
* @param array|\WP_Block_Template $context Where the block is embedded.
* @param array $pattern_exclude_list List of pattern slugs to exclude.
* @since $VID:$
* @return boolean
*/
private function pattern_is_excluded( $context, $pattern_exclude_list ) {
$pattern_slug = is_array( $context ) && isset( $context['slug'] ) ? $context['slug'] : '';
return in_array( $pattern_slug, $pattern_exclude_list, true );
}
/**
* Checks if the provided context contains a mini-cart block.
*
* @param array|\WP_Block_Template $context Where the block is embedded.
* @since $VID:$
* @return boolean
*/
private function has_mini_cart_block( $context ) {
/**
* Note: this won't work for parsing WP_Block_Template instance until it's fixed in core
* because $context->content is set as the result of `traverse_and_serialize_blocks` so
* the filter callback doesn't get the original content.
*
* @see https://core.trac.wordpress.org/ticket/59882
*/
$content = is_array( $context ) && isset( $context['content'] ) ? $context['content'] : '';
$content = '' === $content && $context instanceof \WP_Block_Template ? $context->content : $content;
return strpos( $content, 'wp:woocommerce/mini-cart' ) !== false;
}
/**
* Given a provided context, returns whether the context refers to header content.
*
* @param array|\WP_Block_Template $context Where the block is embedded.
* @since $VID:$
* @return boolean
*/
private function is_header_part_or_pattern( $context ) {
$is_header_pattern = is_array( $context ) &&
(
( isset( $context['blockTypes'] ) && in_array( 'core/template-part/header', $context['blockTypes'], true ) ) ||
( isset( $context['categories'] ) && in_array( 'header', $context['categories'], true ) )
);
$is_header_part = $context instanceof \WP_Block_Template && 'header' === $context->area;
return ( $is_header_pattern || $is_header_part );
}
/**
* Returns whether the Mini-Cart should be rendered or not.
*

View File

@ -100,14 +100,6 @@ describe( 'Shopper → Mini-Cart', () => {
await expect( page ).toMatchElement(
'.wc-block-mini-cart__quantity-badge'
);
// Make sure the initial quantity is 0.
await expect( page ).toMatchElement(
'.wc-block-mini-cart__amount',
{
text: '$0',
}
);
await expect( page ).toMatchElement( '.wc-block-mini-cart__badge', {
text: '',
} );

View File

@ -1,5 +0,0 @@
<!-- wp:woocommerce/mini-cart /-->
<!-- wp:woocommerce/all-products {"columns":3,"rows":3,"alignButtons":false,"contentVisibility":{"orderBy":true},"orderby":"date","layoutConfig":[["woocommerce/product-image",{"imageSizing":"thumbnail"}],["woocommerce/product-title"],["woocommerce/product-price"],["woocommerce/product-rating"],["woocommerce/product-button"]]} -->
<div class="wp-block-woocommerce-all-products wc-block-all-products" data-attributes="{&quot;alignButtons&quot;:false,&quot;columns&quot;:3,&quot;contentVisibility&quot;:{&quot;orderBy&quot;:true},&quot;isPreview&quot;:false,&quot;layoutConfig&quot;:[[&quot;woocommerce/product-image&quot;,{&quot;imageSizing&quot;:&quot;thumbnail&quot;}],[&quot;woocommerce/product-title&quot;],[&quot;woocommerce/product-price&quot;],[&quot;woocommerce/product-rating&quot;],[&quot;woocommerce/product-button&quot;]],&quot;orderby&quot;:&quot;date&quot;,&quot;rows&quot;:3}"></div>
<!-- /wp:woocommerce/all-products -->

View File

@ -2,11 +2,13 @@
* External dependencies
*/
import { test, expect } from '@woocommerce/e2e-playwright-utils';
import { Page } from '@playwright/test';
import { FrontendUtils } from '@woocommerce/e2e-utils';
const openMiniCart = async ( page: Page ) => {
await page.getByLabel( 'items in cart,' ).hover();
await page.getByLabel( 'items in cart,' ).click();
const blockName = 'woocommerce/mini-cart';
const openMiniCart = async ( frontendUtils: FrontendUtils ) => {
const block = await frontendUtils.getBlockByName( blockName );
await block.click();
};
test.describe( `Mini Cart Block`, () => {
@ -24,11 +26,29 @@ test.describe( `Mini Cart Block`, () => {
} );
test.beforeEach( async ( { page } ) => {
await page.goto( `/mini-cart-block`, { waitUntil: 'commit' } );
await page.goto( `/shop`, { waitUntil: 'commit' } );
} );
test( 'should open the empty cart drawer', async ( { page } ) => {
await openMiniCart( page );
test( 'should the Mini Cart block be present near the navigation block', async ( {
page,
frontendUtils,
} ) => {
const block = await frontendUtils.getBlockByName( blockName );
// The Mini Cart block should be present near the navigation block.
const navigationBlock = page.locator(
`//div[@data-block-name='${ blockName }']/preceding-sibling::nav[contains(@class, 'wp-block-navigation')]`
);
await expect( navigationBlock ).toBeVisible();
await expect( block ).toBeVisible();
} );
test( 'should open the empty cart drawer', async ( {
page,
frontendUtils,
} ) => {
await openMiniCart( frontendUtils );
await expect( page.getByRole( 'dialog' ) ).toContainText(
'Your cart is currently empty!'
@ -37,8 +57,9 @@ test.describe( `Mini Cart Block`, () => {
test( 'should close the drawer when clicking on the close button', async ( {
page,
frontendUtils,
} ) => {
await openMiniCart( page );
await openMiniCart( frontendUtils );
await expect( page.getByRole( 'dialog' ) ).toContainText(
'Your cart is currently empty!'
@ -51,26 +72,26 @@ test.describe( `Mini Cart Block`, () => {
test( 'should close the drawer when clicking outside the drawer', async ( {
page,
frontendUtils,
} ) => {
await openMiniCart( page );
await openMiniCart( frontendUtils );
await expect( page.getByRole( 'dialog' ) ).toContainText(
'Your cart is currently empty!'
);
await expect(
page.getByRole( 'button', { name: 'Close' } )
).toBeInViewport();
await page.mouse.click( 50, 200 );
await page.mouse.click( 0, 0 );
await expect( page.getByRole( 'dialog' ) ).toHaveCount( 0 );
} );
test( 'should open the filled cart drawer', async ( { page } ) => {
test( 'should open the filled cart drawer', async ( {
page,
frontendUtils,
} ) => {
await page.click( 'text=Add to cart' );
await openMiniCart( page );
await openMiniCart( frontendUtils );
await expect( page.getByRole( 'dialog' ) ).toContainText(
'Your cart (1 item)'

View File

@ -368,7 +368,7 @@ class ProductCollectionPage {
* Private methods to be used by the class.
*/
private async refreshLocators( currentUI: 'editor' | 'frontend' ) {
await this.waitForProductsToLoad();
await this.waitForProductsToLoad( currentUI );
if ( currentUI === 'editor' ) {
await this.initializeLocatorsForEditor();
@ -417,11 +417,15 @@ class ProductCollectionPage {
this.pagination = this.page.locator( SELECTORS.pagination.onFrontend );
}
private async waitForProductsToLoad() {
private async waitForProductsToLoad( currentUI: 'editor' | 'frontend' ) {
// Wait for the product blocks to be loaded.
await this.page.waitForSelector( SELECTORS.product );
// Wait for the loading spinner to be detached.
await this.page.waitForSelector( '.is-loading', { state: 'detached' } );
if ( currentUI === 'editor' ) {
// Wait for the loading spinner to be detached.
await this.page.waitForSelector( '.is-loading', {
state: 'detached',
} );
}
}
}