Commit 38bea0a82c9 for woocommerce
commit 38bea0a82c92ee14fd4cb7f9754a42accfbb95eb
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Sep 14 17:22:15 2026 +0300
[tests] Reduce Product Filters E2E tests from 9 to 3 (#68606)
test(blocks): Reduce Product Filters E2E tests from 9 to 3
The Product Filters specs ran nine browser titles. Five of the
attribute filter's titles each checked one step of the same select,
deselect, and Clear journey from a fresh page, and a sixth checked
the rendered list. The editor spec's two titles inserted the same
block to check its canvas and its List View separately.
Add a PHPUnit test for the attribute filter's rendered options and
counts in its interactivity context, a Jest test for toggling an
attribute on and off in the store, and count fields in the checkbox
list's mirror test. Fold the five into one lifecycle title that checks
the URL, the product titles, the checkbox, and the Clear button at
each step, keep the counts title, which also renders the list, and
merge the two editor titles.
The attribute filter templates look up the Color attribute ID instead
of assuming it is 1.
Consolidates the mega-branch slices:
- Slice 066: test(blocks): Move attribute lifecycle below E2E
- Slice 114: test(blocks): Consolidate Product Filters structure
coverage
- test: Isolate final PHPUnit fixtures (this test file only)
- refactor(e2e): simplify migrated Blocks test contracts (this spec
only)
Refs TESTOPS-234
Refs #68046
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/testops-234-product-filters-block b/plugins/woocommerce/changelog/testops-234-product-filters-block
new file mode 100644
index 00000000000..135e1f86a9c
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-product-filters-block
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce Product Filters E2E tests from 9 to 3; PHPUnit owns the attribute filter's options and counts, and Jest owns the store's attribute toggle.
+
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
index b950e484098..691e68ca930 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
@@ -127,6 +127,50 @@ describe( 'product filters interactivity store', () => {
] );
} );
+ it( 'toggles an attribute and removes its query parameters when deselected', () => {
+ if ( ! mockRegisteredStore ) {
+ throw new Error( 'Product filters store was not registered.' );
+ }
+
+ const context = {
+ isOverlayOpened: false,
+ params: {},
+ activeFilters: [],
+ item: {
+ type: 'attribute/color',
+ label: 'Gray',
+ value: 'gray',
+ selected: false,
+ count: 2,
+ attributeQueryType: 'or' as const,
+ },
+ activeLabelTemplate: 'Color: {{label}}',
+ filterType: 'attribute/color',
+ };
+
+ mockGetContext.mockReturnValue( context );
+
+ mockRegisteredStore.actions.toggle();
+
+ expect( context.activeFilters ).toEqual( [
+ {
+ value: 'gray',
+ type: 'attribute/color',
+ activeLabel: 'Color: Gray',
+ attributeQueryType: 'or',
+ },
+ ] );
+ expect( mockRegisteredStore.state.params ).toEqual( {
+ filter_color: 'gray',
+ query_type_color: 'or',
+ } );
+
+ mockRegisteredStore.actions.toggle();
+
+ expect( context.activeFilters ).toEqual( [] );
+ expect( mockRegisteredStore.state.params ).toEqual( {} );
+ } );
+
it( 'returns no selectable items when server context items are not an array', () => {
if ( ! mockRegisteredStore ) {
throw new Error( 'Product filters store was not registered.' );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/__tests__/frontend.test.ts
index 3ddee197f15..18c89853119 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/__tests__/frontend.test.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/__tests__/frontend.test.ts
@@ -16,12 +16,14 @@ const mockParentStore = {
label: 'Blue',
value: 'blue',
selected: false,
+ count: 4,
},
{
id: 'attribute-red',
label: 'Red',
value: 'red',
selected: true,
+ count: 7,
},
],
},
@@ -72,6 +74,7 @@ describe( 'product filter checkbox list interactivity store', () => {
label: 'Blue',
value: 'blue',
selected: false,
+ count: 4,
index: 0,
hidden: false,
},
@@ -80,6 +83,7 @@ describe( 'product filter checkbox list interactivity store', () => {
label: 'Red',
value: 'red',
selected: true,
+ count: 7,
index: 1,
hidden: false,
},
diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-product-filters-block b/plugins/woocommerce/client/blocks/changelog/testops-234-product-filters-block
new file mode 100644
index 00000000000..135e1f86a9c
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/testops-234-product-filters-block
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce Product Filters E2E tests from 9 to 3; PHPUnit owns the attribute filter's options and counts, and Jest owns the store's attribute toggle.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/attribute-filter-frontend.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/attribute-filter-frontend.block_theme.spec.ts
index 8ff20b6caab..eea5b1310e5 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/attribute-filter-frontend.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/attribute-filter-frontend.block_theme.spec.ts
@@ -1,9 +1,14 @@
/**
* External dependencies
*/
-import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils';
-
-const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Red', 'Green', 'Gray', 'Yellow' ];
+import {
+ TemplateCompiler,
+ test as base,
+ expect,
+ wpCLI,
+} from '@woocommerce/e2e-utils';
+
+const GRAY_PRODUCT_TITLES = [ 'T-Shirt', 'T-Shirt with Logo' ];
const COLOR_ATTRIBUTES_WITH_COUNTS = [
'Blue (4)',
'Red (4)',
@@ -12,6 +17,34 @@ const COLOR_ATTRIBUTES_WITH_COUNTS = [
'Yellow (1)',
];
+const getColorAttributeId = async () => {
+ const { stdout } = await wpCLI(
+ 'wc product_attribute list --format=json --user=1'
+ );
+ const firstBracket = stdout.indexOf( '[' );
+ const lastBracket = stdout.lastIndexOf( ']' );
+
+ if ( firstBracket < 0 || lastBracket <= firstBracket ) {
+ throw new Error( 'Product attribute CLI output did not contain JSON.' );
+ }
+
+ const attributes = JSON.parse(
+ stdout.slice( firstBracket, lastBracket + 1 )
+ ) as Array< { id: number | string; name: string; slug: string } >;
+ const colorAttributes = attributes.filter(
+ ( attribute ) =>
+ attribute.name === 'Color' && attribute.slug === 'pa_color'
+ );
+
+ expect( colorAttributes ).toHaveLength( 1 );
+ const attributeId = Number( colorAttributes[ 0 ].id );
+ expect( Number.isSafeInteger( attributeId ) && attributeId > 0 ).toBe(
+ true
+ );
+
+ return attributeId;
+};
+
const test = base.extend< { templateCompiler: TemplateCompiler } >( {
templateCompiler: async ( { requestUtils }, use ) => {
const compiler = await requestUtils.createTemplateFromFile(
@@ -24,9 +57,10 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
test.describe( 'With default display style', () => {
test.beforeEach( async ( { templateCompiler, page } ) => {
+ const colorAttributeId = await getColorAttributeId();
await templateCompiler.compile( {
attributes: {
- attributeId: 1,
+ attributeId: colorAttributeId,
},
} );
@@ -44,132 +78,83 @@ test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
} );
} );
- test( 'clear button is not shown on initial page load', async ( {
+ test( 'selects and clears an attribute while keeping URL, products, and controls in sync', async ( {
page,
} ) => {
await page.goto( '/shop' );
- const button = page.getByRole( 'button', {
+ const grayCheckbox = page.getByRole( 'checkbox', { name: 'Gray' } );
+ const clearButton = page.getByRole( 'button', {
name: 'Clear filters',
} );
-
- await expect( button ).toBeHidden();
- } );
-
- test( 'renders a checkbox list with the available attribute filters', async ( {
- page,
- } ) => {
- await page.goto( '/shop' );
-
- const listItems = page
- .getByRole( 'heading', {
- name: 'Attribute',
- } )
- .locator( '..' )
- .locator( '..' )
- .locator( 'label' );
-
- await expect( listItems ).toHaveCount( 5 );
-
- for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) {
- await expect( listItems.nth( i ) ).toHaveText(
- COLOR_ATTRIBUTE_VALUES[ i ]
- );
- }
- } );
-
- test( 'filters the list of products by selecting an attribute', async ( {
- page,
- } ) => {
- await page.goto( '/shop' );
-
- const grayCheckbox = page.getByText( 'Gray' );
- await grayCheckbox.click();
-
- // wait for navigation
- await page.waitForURL( /.*filter_color=gray.*/ );
-
- const products = page.locator( '.wc-block-product' );
-
- await expect( products ).toHaveCount( 2 );
- } );
-
- test( 'clear button appears after a filter is applied', async ( {
- page,
- } ) => {
- await page.goto( '/shop' );
-
- const grayCheckbox = page.getByText( 'Gray' );
- await grayCheckbox.click();
-
- // wait for navigation
- await page.waitForURL( /.*filter_color=gray.*/ );
-
- const button = page.getByRole( 'button', {
+ const clearButtonIncludingHidden = page.getByRole( 'button', {
name: 'Clear filters',
+ includeHidden: true,
} );
+ const productTitles = page.locator(
+ '.wp-block-woocommerce-product-template .wp-block-post-title'
+ );
+ const expectFilterParams = async (
+ expectedValue: string | null
+ ) => {
+ await expect
+ .poll( () => {
+ const params = new URL( page.url() ).searchParams;
+ return {
+ filterColor: params.get( 'filter_color' ),
+ queryTypeColor: params.get( 'query_type_color' ),
+ };
+ } )
+ .toEqual( {
+ filterColor: expectedValue,
+ queryTypeColor: expectedValue ? 'or' : null,
+ } );
+ };
+ await expect( productTitles.first() ).toBeVisible();
+ const initialProductTitles = (
+ await productTitles.allTextContents()
+ ).map( ( title ) => title.trim() );
+ expect( initialProductTitles ).not.toHaveLength( 0 );
+
+ await expect( clearButton ).toBeHidden();
+ await expect( grayCheckbox ).not.toBeChecked();
- await expect( button ).toBeVisible();
- } );
-
- test( 'clear button hides after deselecting all filters', async ( {
- page,
- } ) => {
- await page.goto( '/shop' );
-
- const grayCheckbox = page.getByText( 'Gray' );
await grayCheckbox.click();
-
- // wait for navigation
- await page.waitForURL( /.*filter_color=gray.*/ );
+ await expectFilterParams( 'gray' );
+ await expect( productTitles ).toHaveText( GRAY_PRODUCT_TITLES );
+ await expect( grayCheckbox ).toBeChecked();
+ await expect( clearButton ).toBeVisible();
await grayCheckbox.click();
+ await expectFilterParams( null );
+ await expect( productTitles ).toHaveText( initialProductTitles );
+ await expect( grayCheckbox ).not.toBeChecked();
+ await expect( clearButtonIncludingHidden ).toHaveCount( 0 );
- const button = page.getByRole( 'button', {
- name: 'Clear filters',
- } );
-
- await expect( button ).toBeHidden();
- } );
-
- test( 'filters are cleared after clear button is clicked', async ( {
- page,
- } ) => {
- await page.goto( '/shop' );
-
- const grayCheckbox = page.getByText( 'Gray' );
await grayCheckbox.click();
-
- // wait for navigation
- await page.waitForURL( /.*filter_color=gray.*/ );
-
- const button = page.getByRole( 'button', {
- name: 'Clear filters',
- } );
-
- await button.click();
-
- COLOR_ATTRIBUTE_VALUES.map( async ( color ) => {
- const element = page.locator(
- `input[value="${ color.toLowerCase() }"]`
- );
-
- await expect( element ).not.toBeChecked();
- } );
+ await expectFilterParams( 'gray' );
+ await expect( productTitles ).toHaveText( GRAY_PRODUCT_TITLES );
+ await expect( clearButton ).toBeVisible();
+ await clearButton.click();
+ await expectFilterParams( null );
+ await expect( productTitles ).toHaveText( initialProductTitles );
+ await expect( grayCheckbox ).not.toBeChecked();
+ await expect( clearButtonIncludingHidden ).toHaveCount( 0 );
} );
} );
test.describe( 'With show counts enabled', () => {
test.beforeEach( async ( { templateCompiler } ) => {
+ const colorAttributeId = await getColorAttributeId();
await templateCompiler.compile( {
attributes: {
- attributeId: 1,
+ attributeId: colorAttributeId,
showCounts: true,
},
} );
} );
- test( 'Renders checkboxes with associated product counts', async ( {
+ test( 'renders current product counts for every attribute', async ( {
page,
} ) => {
await page.goto( '/shop' );
@@ -182,13 +167,9 @@ test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
.locator( '..' )
.locator( 'label' );
- await expect( listItems ).toHaveCount( 5 );
-
- for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) {
- await expect( listItems.nth( i ) ).toHaveText(
- COLOR_ATTRIBUTES_WITH_COUNTS[ i ]
- );
- }
+ await expect( listItems ).toHaveText(
+ COLOR_ATTRIBUTES_WITH_COUNTS
+ );
} );
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters.block_theme.spec.ts
index d9897bad86f..09ab9547c6d 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-filters/product-filters.block_theme.spec.ts
@@ -54,7 +54,7 @@ test.describe( `${ blockData.name }`, () => {
} );
} );
- test( 'should be visible and contain correct inner blocks', async ( {
+ test( 'shows the Product Filters inner-block structure and List View names', async ( {
editor,
pageObject,
} ) => {
@@ -84,18 +84,6 @@ test.describe( `${ blockData.name }`, () => {
for ( const option of expectedColorFilterOptions ) {
await expect( colorFilterBlock ).toContainText( option );
}
- } );
-
- test( 'should contain the correct inner block names in the list view', async ( {
- editor,
- pageObject,
- } ) => {
- await pageObject.addProductFiltersBlock( { cleanContent: true } );
-
- const block = editor.canvas.getByLabel(
- blockData.selectors.editor.blocks.filters.label
- );
- await expect( block ).toBeVisible();
await pageObject.page.getByLabel( 'Document Overview' ).click();
const listView = pageObject.page.getByLabel( 'List View' );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterAttributeTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterAttributeTest.php
index 2545149ac90..466f06c8a82 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterAttributeTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterAttributeTest.php
@@ -4,6 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
use WC_Unit_Test_Case;
+use WP_HTML_Tag_Processor;
/**
* Tests for the ProductFilterAttribute block type.
@@ -24,6 +25,20 @@ class ProductFilterAttributeTest extends WC_Unit_Test_Case {
*/
private $attribute_ids = array();
+ /**
+ * Attribute terms created during tests, keyed by taxonomy.
+ *
+ * @var array<string, int[]>
+ */
+ private $attribute_term_ids = array();
+
+ /**
+ * Attribute taxonomies registered by this test class.
+ *
+ * @var string[]
+ */
+ private $registered_taxonomies = array();
+
/**
* Set up test fixtures.
*/
@@ -39,12 +54,20 @@ class ProductFilterAttributeTest extends WC_Unit_Test_Case {
public function tearDown(): void {
remove_filter( 'woocommerce_pre_product_filter_data', array( $this, 'filter_product_filter_data' ), 10 );
+ foreach ( $this->attribute_term_ids as $taxonomy => $term_ids ) {
+ foreach ( $term_ids as $term_id ) {
+ wp_delete_term( $term_id, $taxonomy );
+ }
+ }
+
foreach ( $this->attribute_ids as $attribute_id ) {
wc_delete_attribute( $attribute_id );
}
- if ( taxonomy_exists( 'pa_material' ) ) {
- unregister_taxonomy( 'pa_material' );
+ foreach ( $this->registered_taxonomies as $taxonomy ) {
+ if ( taxonomy_exists( $taxonomy ) ) {
+ unregister_taxonomy( $taxonomy );
+ }
}
parent::tearDown();
@@ -88,19 +111,21 @@ class ProductFilterAttributeTest extends WC_Unit_Test_Case {
if ( ! taxonomy_exists( 'pa_material' ) ) {
register_taxonomy( 'pa_material', array( 'product' ), array( 'labels' => array( 'name' => 'Material' ) ) );
+ $this->registered_taxonomies[] = 'pa_material';
}
$term = wp_insert_term( 'Cotton & Linen', 'pa_material' );
$this->assertNotWPError( $term );
- $term_id = (int) $term['term_id'];
- $this->attribute_counts = array( $term_id => 1 );
- $stored_term = get_term( $term_id, 'pa_material' );
- $expected_serialized_label = 'Cotton & Linen';
- $expected_context_label = 'Cotton \\u0026 Linen';
- $double_encoded_serialization = 'Cotton &amp; Linen';
- $double_encoded_context_entity = '\\u0026amp;';
+ $term_id = (int) $term['term_id'];
+ $this->attribute_term_ids['pa_material'][] = $term_id;
+ $this->attribute_counts = array( $term_id => 1 );
+ $stored_term = get_term( $term_id, 'pa_material' );
+ $expected_serialized_label = 'Cotton & Linen';
+ $expected_context_label = 'Cotton \\u0026 Linen';
+ $double_encoded_serialization = 'Cotton &amp; Linen';
+ $double_encoded_context_entity = '\\u0026amp;';
$this->assertSame( 'Cotton & Linen', $stored_term->name, 'Term fixture should use WordPress encoded storage.' );
@@ -112,19 +137,123 @@ class ProductFilterAttributeTest extends WC_Unit_Test_Case {
$this->assertStringNotContainsString( $double_encoded_context_entity, $output, 'Interactivity context should not contain an encoded entity label.' );
}
+ /**
+ * @testdox Should render ordered attribute options and counts in the interactivity context.
+ */
+ public function test_renders_ordered_attribute_options_and_counts_in_interactivity_context(): void {
+ $attribute_slug = 'filter-count-color';
+ $attribute_taxonomy = 'pa_' . $attribute_slug;
+ $filter_type = 'attribute/' . $attribute_slug;
+ $attribute_id = wc_create_attribute(
+ array(
+ 'name' => 'Color',
+ 'slug' => $attribute_slug,
+ )
+ );
+
+ $this->assertIsInt( $attribute_id, 'Attribute should be created.' );
+ $this->attribute_ids[] = $attribute_id;
+
+ if ( ! taxonomy_exists( $attribute_taxonomy ) ) {
+ register_taxonomy( $attribute_taxonomy, array( 'product' ), array( 'labels' => array( 'name' => 'Color' ) ) );
+ $this->registered_taxonomies[] = $attribute_taxonomy;
+ }
+
+ $blue = wp_insert_term( 'Blue', $attribute_taxonomy );
+ $this->assertNotWPError( $blue );
+ $blue_id = (int) $blue['term_id'];
+ $this->attribute_term_ids[ $attribute_taxonomy ][] = $blue_id;
+
+ $red = wp_insert_term( 'Red', $attribute_taxonomy );
+ $this->assertNotWPError( $red );
+ $red_id = (int) $red['term_id'];
+ $this->attribute_term_ids[ $attribute_taxonomy ][] = $red_id;
+ $this->attribute_counts = array(
+ $blue_id => 7,
+ $red_id => 3,
+ );
+
+ $output_without_counts = $this->render_attribute_filter_with_checkbox_list( $attribute_id );
+ $context_without_counts = $this->get_attribute_filter_context( $output_without_counts, $filter_type );
+
+ $this->assertIsArray( $context_without_counts, 'The attribute filter should expose an interactivity context without counts.' );
+ foreach ( $context_without_counts['items'] as $item ) {
+ $this->assertArrayNotHasKey( 'count', $item );
+ }
+
+ $output = $this->render_attribute_filter_with_checkbox_list( $attribute_id, true );
+ $context = $this->get_attribute_filter_context( $output, $filter_type );
+
+ $this->assertIsArray( $context, 'The attribute filter should expose an interactivity context.' );
+ $this->assertSame( 'Color: {{label}}', $context['activeLabelTemplate'] );
+ $this->assertSame( $filter_type, $context['filterType'] );
+ $this->assertSame(
+ array(
+ array(
+ 'id' => $filter_type . '-blue',
+ 'label' => 'Blue',
+ 'ariaLabel' => 'Blue',
+ 'value' => 'blue',
+ 'selected' => false,
+ 'type' => $filter_type,
+ 'attributeQueryType' => 'or',
+ 'count' => 7,
+ ),
+ array(
+ 'id' => $filter_type . '-red',
+ 'label' => 'Red',
+ 'ariaLabel' => 'Red',
+ 'value' => 'red',
+ 'selected' => false,
+ 'type' => $filter_type,
+ 'attributeQueryType' => 'or',
+ 'count' => 3,
+ ),
+ ),
+ $context['items']
+ );
+ }
+
+ /**
+ * Get a matching interactivity context from rendered block markup.
+ *
+ * @param string $output Rendered block markup.
+ * @param string $filter_type Expected filter type.
+ * @return array|null
+ */
+ private function get_attribute_filter_context( string $output, string $filter_type ): ?array {
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ while ( $processor->next_tag() ) {
+ $encoded_context = $processor->get_attribute( 'data-wp-context' );
+ if ( ! is_string( $encoded_context ) ) {
+ continue;
+ }
+
+ $candidate = json_decode( $encoded_context, true );
+ if ( is_array( $candidate ) && ( $candidate['filterType'] ?? null ) === $filter_type ) {
+ return $candidate;
+ }
+ }
+
+ return null;
+ }
+
/**
* Render an attribute filter block with a checkbox list inner block.
*
- * @param int $attribute_id Attribute ID.
+ * @param int $attribute_id Attribute ID.
+ * @param bool $show_counts Whether to include product counts.
* @return string
*/
- private function render_attribute_filter_with_checkbox_list( int $attribute_id ): string {
+ private function render_attribute_filter_with_checkbox_list( int $attribute_id, bool $show_counts = false ): string {
$attribute_block = array(
'blockName' => 'woocommerce/product-filter-attribute',
'attrs' => array(
'attributeId' => $attribute_id,
'hideEmpty' => false,
'queryType' => 'or',
+ 'showCounts' => $show_counts,
'sortOrder' => 'name-asc',
),
'innerBlocks' => array(