Commit 065e18d741 for woocommerce
commit 065e18d741861ac58a9bae6ed94966fe2a7a3397
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date: Fri Jan 30 18:55:50 2026 +0100
Products by Tag/Category: journey starts with taxonomy picker (#62993)
* Foundation for product picker
* Updated handpicked product picker
* Move the collection specific controls to the top of filters
* Add changelog
* Update docs etc
* Bring back the comment
* Prefix editor side CSS classes with .wc-block-editor-
* Rename product picker files to be less confusing
* Make Done button always visible but only disabled when no products is chosen
* Update selector in E2E tests
* Rename files to follow kebab-case convention
* Introduce taxonomy pickers
* Introduce taxonomy picker
* Introduce Product brand picker and use it in Product Collection
* Add changelog
* Make Brand picker more aliogned with Categories than Tags
* Revert By Brands related code so it can be implemented in separate PR
* Update changelog
* Simplify picker state handling
* Change the file name to fol,low kebab-case
* Add by brand filter on the top of inspector controls
* Get rid of addFilter injecting collection specific filter since we moved this to the top anyway
* Refactor colelction specific controls
* Minor refactor
* Remove unused references
* Improve the styles of search list controlk
* Add E2E tests
* Adopt E2E tests to real life
diff --git a/plugins/woocommerce/changelog/add-58403-with-taxonomies b/plugins/woocommerce/changelog/add-58403-with-taxonomies
new file mode 100644
index 0000000000..0325ac332f
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-58403-with-taxonomies
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Products by Tag/Category: journey starts with terms picker
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
index 115bed117d..875af68892 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/index.tsx
@@ -22,6 +22,9 @@ import CollectionSelectionModal from './collection-selection-modal';
import { useProductCollectionUIState } from '../utils';
import SingleProductPicker from './single-product-picker';
import MultiProductPicker from './multi-product-picker';
+import TaxonomyPicker, {
+ getTaxonomySlugForCollection,
+} from './taxonomy-picker';
import { useTracksLocation } from '../tracks-utils';
import { useRegisterEmailCollections } from '../hooks/use-register-email-collections';
@@ -35,22 +38,40 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
- // Track if the hand-picked products picker is active.
+ // Track if the collection-specific picker is active (Hand-Picked or Taxonomy).
// This allows multi-select before clicking "Done".
- const [ isHandPickedPickerActive, setIsHandPickedPickerActive ] =
- useState( false );
+ // Only one picker can be active at a time since collections are mutually exclusive.
+ const [ isPickerActive, setIsPickerActive ] = useState( false );
const isHandPickedCollection =
attributes.collection === CoreCollectionNames.HAND_PICKED;
const hasHandPickedProducts =
( attributes.query?.woocommerceHandPickedProducts?.length ?? 0 ) > 0;
- // Activate the picker when Hand-Picked collection is selected with no products
+ const isTaxonomyCollection =
+ attributes.collection === CoreCollectionNames.BY_CATEGORY ||
+ attributes.collection === CoreCollectionNames.BY_TAG;
+
+ const taxonomySlug = getTaxonomySlugForCollection( attributes.collection );
+ const hasSelectedTerms = taxonomySlug
+ ? ( attributes.query?.taxQuery?.[ taxonomySlug ]?.length ?? 0 ) > 0
+ : false;
+
+ // Activate the picker when a collection needs initial selection
useEffect( () => {
if ( isHandPickedCollection && ! hasHandPickedProducts ) {
- setIsHandPickedPickerActive( true );
+ setIsPickerActive( true );
+ } else if ( isTaxonomyCollection && ! hasSelectedTerms ) {
+ setIsPickerActive( true );
}
- }, [ isHandPickedCollection, hasHandPickedProducts ] );
+ }, [
+ isHandPickedCollection,
+ hasHandPickedProducts,
+ isTaxonomyCollection,
+ hasSelectedTerms,
+ ] );
+
+ const dismissPicker = () => setIsPickerActive( false );
const hasInnerBlocks = useSelect(
( select ) =>
@@ -85,18 +106,19 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
};
const renderComponent = () => {
- // Show the hand-picked products picker if it's active (local state).
+ // Show the collection-specific picker if it's active (local state).
// This allows multi-select before clicking "Done".
- // The inspector controls (HandPickedProductsControlField) are inside
- // ProductCollectionContent, so they're automatically hidden while
- // the picker is shown.
- if ( isHandPickedCollection && isHandPickedPickerActive ) {
- return (
- <MultiProductPicker
- { ...props }
- onDone={ () => setIsHandPickedPickerActive( false ) }
- />
- );
+ // The inspector controls are inside ProductCollectionContent,
+ // so they're automatically hidden while the picker is shown.
+ if ( isPickerActive ) {
+ if ( isHandPickedCollection ) {
+ return (
+ <MultiProductPicker { ...props } onDone={ dismissPicker } />
+ );
+ }
+ if ( isTaxonomyCollection ) {
+ return <TaxonomyPicker { ...props } onDone={ dismissPicker } />;
+ }
}
switch ( productCollectionUIStateInEditor ) {
@@ -125,11 +147,12 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
// This case is hit when no products are selected
// and the picker was previously dismissed but products were removed
return (
- <MultiProductPicker
- { ...props }
- onDone={ () => setIsHandPickedPickerActive( false ) }
- />
+ <MultiProductPicker { ...props } onDone={ dismissPicker } />
);
+ case ProductCollectionUIStatesInEditor.TAXONOMY_PICKER:
+ // This case is hit when no taxonomy terms are selected
+ // and the picker was previously dismissed but terms were removed
+ return <TaxonomyPicker { ...props } onDone={ dismissPicker } />;
case ProductCollectionUIStatesInEditor.VALID:
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
return (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
index 8b7f55829f..f2c8e8f4b3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
@@ -4,8 +4,6 @@
import { InspectorControls } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
-import { EditorBlock } from '@woocommerce/types';
-import { addFilter } from '@wordpress/hooks';
import { useIsEmailEditor } from '@woocommerce/email-editor';
import { recordEvent } from '@woocommerce/tracks';
import { CesFeedbackButton } from '@woocommerce/editor-components/ces-feedback-button';
@@ -21,7 +19,6 @@ import {
import metadata from '../../block.json';
import { useTracksLocation } from '../../tracks-utils';
import {
- ProductCollectionEditComponentProps,
ProductCollectionContentProps,
CoreFilterNames,
FilterName,
@@ -150,6 +147,39 @@ const ProductCollectionInspectorControls = (
query,
};
+ /**
+ * Renders the collection-specific control based on the collection type.
+ * These controls are placed at the top for easy access when editing.
+ */
+ const renderCollectionSpecificControl = () => {
+ switch ( collection ) {
+ case CoreCollectionNames.HAND_PICKED:
+ return (
+ <PanelBody>
+ <HandPickedProductsControlField
+ { ...queryControlProps }
+ />
+ </PanelBody>
+ );
+ case CoreCollectionNames.BY_CATEGORY:
+ case CoreCollectionNames.BY_TAG:
+ case CoreCollectionNames.BY_BRAND:
+ return (
+ <PanelBody>
+ <TaxonomyControls
+ { ...queryControlProps }
+ collection={ collection }
+ renderMode="standalone"
+ />
+ </PanelBody>
+ );
+ case CoreCollectionNames.RELATED:
+ return <RelatedByControl { ...queryControlProps } />;
+ default:
+ return null;
+ }
+ };
+
return (
<InspectorControls>
<LinkedProductControl
@@ -159,20 +189,7 @@ const ProductCollectionInspectorControls = (
location={ props.location }
/>
- {
- /**
- * "Hand-Picked" collection-specific control.
- * Placed at the top for easy access when editing product selection.
- * Only rendered when ProductCollectionContent is shown (not during picker).
- */
- collection === CoreCollectionNames.HAND_PICKED && (
- <PanelBody>
- <HandPickedProductsControlField
- { ...queryControlProps }
- />
- </PanelBody>
- )
- }
+ { renderCollectionSpecificControl() }
<ToolsPanel
label={ __( 'Settings', 'woocommerce' ) }
@@ -273,83 +290,3 @@ const ProductCollectionInspectorControls = (
};
export default ProductCollectionInspectorControls;
-
-const isProductCollection = ( blockName: string ) =>
- blockName === metadata.name;
-
-const CollectionSpecificControls = (
- props: ProductCollectionEditComponentProps
-) => {
- const { attributes, context } = props;
- const { collection } = attributes;
-
- const setQueryAttributeBind = useMemo(
- () => setQueryAttribute.bind( null, props ),
- [ props ]
- );
- const tracksLocation = useTracksLocation( context.templateSlug );
- const trackInteraction = ( filter: FilterName ) => {
- return recordEvent(
- 'blocks_product_collection_inspector_control_clicked',
- {
- collection,
- location: tracksLocation,
- filter,
- }
- );
- };
- const queryControlProps = {
- setQueryAttribute: setQueryAttributeBind,
- trackInteraction,
- query: attributes.query,
- };
-
- const isByTaxonomy =
- collection === CoreCollectionNames.BY_CATEGORY ||
- collection === CoreCollectionNames.BY_TAG ||
- collection === CoreCollectionNames.BY_BRAND;
-
- return (
- <InspectorControls>
- {
- /**
- * "Related Products" collection-specific controls.
- */
- collection === CoreCollectionNames.RELATED && (
- <RelatedByControl { ...queryControlProps } />
- )
- }
- {
- /**
- * "By Taxonomy" collection-specific controls.
- */
- isByTaxonomy && (
- <PanelBody>
- <TaxonomyControls
- { ...queryControlProps }
- collection={ collection }
- renderMode="standalone"
- />
- </PanelBody>
- )
- }
- </InspectorControls>
- );
-};
-
-const withCollectionSpecificControls =
- < T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
- ( props: ProductCollectionEditComponentProps ) => {
- if ( ! isProductCollection( props.name ) ) {
- return <BlockEdit { ...props } />;
- }
-
- return (
- <>
- <CollectionSpecificControls { ...props } />
- <BlockEdit { ...props } />
- </>
- );
- };
-
-addFilter( 'editor.BlockEdit', metadata.name, withCollectionSpecificControls );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/taxonomy-picker.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/taxonomy-picker.tsx
new file mode 100644
index 0000000000..7694942df9
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/taxonomy-picker.tsx
@@ -0,0 +1,166 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Icon, category, tag } from '@wordpress/icons';
+import { Placeholder, Button } from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+import ProductTagControl from '@woocommerce/editor-components/product-tag-control';
+import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductCollectionEditComponentProps } from '../types';
+import { CoreCollectionNames } from '../types';
+import { getCollectionByName } from '../collections';
+import { setQueryAttribute } from '../utils';
+
+interface TaxonomyPickerProps extends ProductCollectionEditComponentProps {
+ onDone: () => void;
+}
+
+/**
+ * Get the taxonomy slug for a given collection.
+ */
+export const getTaxonomySlugForCollection = (
+ collection: string | undefined
+): string | null => {
+ switch ( collection ) {
+ case CoreCollectionNames.BY_CATEGORY:
+ return 'product_cat';
+ case CoreCollectionNames.BY_TAG:
+ return 'product_tag';
+ default:
+ return null;
+ }
+};
+
+/**
+ * Get the description text for a given collection.
+ */
+const getDescriptionForCollection = (
+ collection: string | undefined
+): string => {
+ switch ( collection ) {
+ case CoreCollectionNames.BY_CATEGORY:
+ return __(
+ 'Display a grid of products from your selected categories.',
+ 'woocommerce'
+ );
+ case CoreCollectionNames.BY_TAG:
+ return __(
+ 'Display a grid of products from your selected tags.',
+ 'woocommerce'
+ );
+ default:
+ return __(
+ 'Select taxonomy terms to display products from.',
+ 'woocommerce'
+ );
+ }
+};
+
+/**
+ * Get the icon for a given collection.
+ */
+const getIconForCollection = ( collection: string | undefined ) => {
+ switch ( collection ) {
+ case CoreCollectionNames.BY_CATEGORY:
+ return category;
+ case CoreCollectionNames.BY_TAG:
+ return tag;
+ default:
+ // For brands and others, use category icon as fallback
+ return category;
+ }
+};
+
+const TaxonomyPicker = ( props: TaxonomyPickerProps ) => {
+ const { attributes, onDone } = props;
+ const blockProps = useBlockProps();
+
+ const collectionData = getCollectionByName( attributes.collection );
+ const taxonomySlug = getTaxonomySlugForCollection( attributes.collection );
+
+ // Get selected term IDs for the relevant taxonomy
+ const selectedTermIds: number[] = taxonomySlug
+ ? attributes.query?.taxQuery?.[ taxonomySlug ] || []
+ : [];
+
+ const hasSelectedTerms = selectedTermIds.length > 0;
+
+ if ( ! collectionData || ! taxonomySlug ) {
+ return null;
+ }
+
+ const handleTermChange = ( termIds: number[] ) => {
+ setQueryAttribute( props, {
+ taxQuery: {
+ ...attributes.query?.taxQuery,
+ [ taxonomySlug ]: termIds,
+ },
+ } );
+ };
+
+ const renderTaxonomyControl = () => {
+ switch ( attributes.collection ) {
+ case CoreCollectionNames.BY_CATEGORY:
+ return (
+ <ProductCategoryControl
+ selected={ selectedTermIds }
+ onChange={ ( value = [] ) => {
+ const ids = value.map(
+ ( { id }: { id: number } ) => id
+ );
+ handleTermChange( ids );
+ } }
+ />
+ );
+ case CoreCollectionNames.BY_TAG:
+ return (
+ <ProductTagControl
+ selected={ selectedTermIds }
+ onChange={ ( value = [] ) => {
+ const ids = value.map(
+ ( { id }: { id: number | string } ) =>
+ Number( id )
+ );
+ handleTermChange( ids );
+ } }
+ />
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+ <div { ...blockProps }>
+ <Placeholder
+ icon={
+ // @ts-expect-error Icon types are incomplete
+ <Icon
+ icon={ getIconForCollection( attributes.collection ) }
+ className="block-editor-block-icon"
+ />
+ }
+ label={ collectionData.title }
+ >
+ { getDescriptionForCollection( attributes.collection ) }
+ <div className="wc-block-editor-product-collection__taxonomy-picker-selection">
+ { renderTaxonomyControl() }
+ <Button
+ variant="primary"
+ onClick={ onDone }
+ disabled={ ! hasSelectedTerms }
+ >
+ { __( 'Done', 'woocommerce' ) }
+ </Button>
+ </div>
+ </Placeholder>
+ </div>
+ );
+};
+
+export default TaxonomyPicker;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
index e8fab2ec71..92a041f685 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
@@ -13,6 +13,7 @@ export enum ProductCollectionUIStatesInEditor {
COLLECTION_PICKER = 'collection_chooser',
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
HAND_PICKED_PRODUCTS_PICKER = 'hand_picked_products_picker',
+ TAXONOMY_PICKER = 'taxonomy_picker',
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
VALID = 'valid',
DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
index 6c270f3462..988b73f885 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
@@ -285,7 +285,30 @@ export const useProductCollectionUIState = ( {
}
/**
- * Case 4: Preview mode - based on `usesReference` value
+ * Case 4: Taxonomy picker for BY_CATEGORY, BY_TAG collections
+ * Show the picker when no taxonomy terms are selected.
+ */
+ const isTaxonomyCollection =
+ attributes.collection === CoreCollectionNames.BY_CATEGORY ||
+ attributes.collection === CoreCollectionNames.BY_TAG;
+
+ if ( isCollectionSelected && isTaxonomyCollection ) {
+ const taxonomySlug =
+ attributes.collection === CoreCollectionNames.BY_CATEGORY
+ ? 'product_cat'
+ : 'product_tag';
+
+ const selectedTermIds =
+ attributes.query?.taxQuery?.[ taxonomySlug ] || [];
+ const hasSelectedTerms = selectedTermIds.length > 0;
+
+ if ( ! hasSelectedTerms ) {
+ return ProductCollectionUIStatesInEditor.TAXONOMY_PICKER;
+ }
+ }
+
+ /**
+ * Case 5: Preview mode - based on `usesReference` value
*/
if ( isInRequiredLocation ) {
/**
@@ -331,6 +354,7 @@ export const useProductCollectionUIState = ( {
hasInnerBlocks,
attributes.query?.productReference,
attributes.query?.woocommerceHandPickedProducts,
+ attributes.query?.taxQuery,
] );
return { productCollectionUIStateInEditor, isLoading: ! hasResolved };
diff --git a/plugins/woocommerce/client/blocks/assets/js/editor-components/search-list-control/style.scss b/plugins/woocommerce/client/blocks/assets/js/editor-components/search-list-control/style.scss
index b9476d2543..bf342caa1c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/editor-components/search-list-control/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/editor-components/search-list-control/style.scss
@@ -80,13 +80,12 @@
}
ul {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $gap-smallest;
list-style: none;
margin: 0;
padding: 0;
-
- li {
- float: left;
- }
}
}
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/collection-pickers.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/collection-pickers.block_theme.spec.ts
new file mode 100644
index 0000000000..3a5ce43c5b
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/collection-pickers.block_theme.spec.ts
@@ -0,0 +1,281 @@
+/**
+ * External dependencies
+ */
+import { test as base, expect } from '@woocommerce/e2e-utils';
+
+/**
+ * Internal dependencies
+ */
+import ProductCollectionPage, {
+ Collections,
+ SELECTORS,
+} from './product-collection.page';
+
+const test = base.extend< { pageObject: ProductCollectionPage } >( {
+ pageObject: async ( { page, admin, editor }, use ) => {
+ const pageObject = new ProductCollectionPage( {
+ page,
+ admin,
+ editor,
+ } );
+ await use( pageObject );
+ },
+} );
+
+/**
+ * Taxonomy-based collections configuration for parameterized tests.
+ */
+const taxonomyCollections: {
+ slug: Collections;
+ name: string;
+ termName: string;
+ termLabel: string;
+ expectedProductCount: number;
+}[] = [
+ {
+ slug: 'productsByCategory',
+ name: 'Products by Category',
+ termName: 'categories',
+ termLabel: 'Accessories',
+ expectedProductCount: 5,
+ },
+ {
+ slug: 'productsByTag',
+ name: 'Products by Tag',
+ termName: 'tags',
+ termLabel: 'Recommended',
+ expectedProductCount: 2,
+ },
+];
+
+test.describe( 'Product Collection: Collection Pickers', () => {
+ test.describe( 'Hand-Picked Products', () => {
+ test( 'Can select multiple products and Done button becomes enabled', async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'handPicked' );
+
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ const doneButton = productPicker.locator(
+ SELECTORS.pickerDoneButton
+ );
+
+ // Initially disabled
+ await expect( doneButton ).toBeDisabled();
+
+ // Select first product
+ await productPicker.getByText( 'Album (woo-album)' ).click();
+
+ // Done button should now be enabled
+ await expect( doneButton ).toBeEnabled();
+
+ // Select second product
+ await productPicker.getByText( 'Beanie (woo-beanie)' ).click();
+
+ // Click Done
+ await doneButton.click();
+
+ // Picker should be hidden and products should be displayed
+ await expect( productPicker ).toBeHidden();
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount( 2 );
+ } );
+
+ test( 'Picker is not shown after save and refresh', async ( {
+ pageObject,
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'handPicked' );
+
+ // Select a product and click Done
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await productPicker.getByText( 'Album (woo-album)' ).click();
+ await productPicker.locator( SELECTORS.pickerDoneButton ).click();
+
+ // Save and refresh
+ await editor.saveDraft();
+ await page.reload();
+ await editor.canvas.locator( 'body' ).waitFor();
+
+ // Click on the block to select it
+ await editor.canvas
+ .locator( '[data-type="woocommerce/product-collection"]' )
+ .first()
+ .click();
+
+ // Picker should not be shown
+ const pickerAfterRefresh = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await expect( pickerAfterRefresh ).toBeHidden();
+
+ // Products should be visible
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount( 1 );
+ } );
+
+ test( 'Products are displayed on frontend', async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'handPicked' );
+
+ // Select products and click Done
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await productPicker.getByText( 'Album (woo-album)' ).click();
+ await productPicker.getByText( 'Beanie (woo-beanie)' ).click();
+ await productPicker.locator( SELECTORS.pickerDoneButton ).click();
+
+ await pageObject.refreshLocators( 'editor' );
+ await pageObject.publishAndGoToFrontend();
+ await expect( pageObject.products ).toHaveCount( 2 );
+ } );
+ } );
+
+ for ( const collection of taxonomyCollections ) {
+ test.describe( `${ collection.name }`, () => {
+ test( `Can select ${ collection.termName } and Done button becomes enabled`, async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( collection.slug );
+
+ const taxonomyPicker = editor.canvas.locator(
+ SELECTORS.taxonomyPicker
+ );
+ const doneButton = taxonomyPicker.locator(
+ SELECTORS.pickerDoneButton
+ );
+
+ // Initially disabled
+ await expect( doneButton ).toBeDisabled();
+
+ // Select a term
+ await taxonomyPicker
+ .getByRole( 'checkbox', { name: collection.termLabel } )
+ .click();
+
+ // Done button should now be enabled
+ await expect( doneButton ).toBeEnabled();
+
+ // Click Done
+ await doneButton.click();
+
+ // Picker should be hidden and products should be displayed
+ await expect( taxonomyPicker ).toBeHidden();
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount(
+ collection.expectedProductCount
+ );
+ } );
+
+ test( `Products from selected ${ collection.termName } are displayed on frontend`, async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( collection.slug );
+
+ // Select term and click Done
+ const taxonomyPicker = editor.canvas.locator(
+ SELECTORS.taxonomyPicker
+ );
+ await taxonomyPicker
+ .getByRole( 'checkbox', { name: collection.termLabel } )
+ .click();
+ await taxonomyPicker
+ .locator( SELECTORS.pickerDoneButton )
+ .click();
+
+ await pageObject.refreshLocators( 'editor' );
+ await pageObject.publishAndGoToFrontend();
+ await expect( pageObject.products ).toHaveCount(
+ collection.expectedProductCount
+ );
+ } );
+ } );
+ }
+
+ test.describe( 'Collection switching', () => {
+ test( 'Switching from Hand-Picked to Products by Category shows taxonomy picker', async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'handPicked' );
+
+ // Select a product and click Done
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await productPicker.getByText( 'Album (woo-album)' ).click();
+ await productPicker.locator( SELECTORS.pickerDoneButton ).click();
+
+ // Switch to Products by Category using toolbar
+ await pageObject.changeCollectionUsingToolbar(
+ 'productsByCategory'
+ );
+
+ // Taxonomy picker should now be shown
+ const taxonomyPicker = editor.canvas.locator(
+ SELECTORS.taxonomyPicker
+ );
+ await expect( taxonomyPicker ).toBeVisible();
+ } );
+
+ test( 'Switching to a non-picker collection displays products immediately', async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'handPicked' );
+
+ // Select a product and click Done
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await productPicker.getByText( 'Album (woo-album)' ).click();
+ await productPicker.locator( SELECTORS.pickerDoneButton ).click();
+
+ // Switch to Featured Products (no picker needed)
+ await pageObject.changeCollectionUsingToolbar( 'featured' );
+
+ // No picker should be shown
+ await expect( productPicker ).toBeHidden();
+ const taxonomyPicker = editor.canvas.locator(
+ SELECTORS.taxonomyPicker
+ );
+ await expect( taxonomyPicker ).toBeHidden();
+
+ // Products should be displayed
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount( 4 );
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
index 1d16de918f..9698f8d52d 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/product-collection/product-collection.page.ts
@@ -57,6 +57,9 @@ export const SELECTORS = {
collectionPlaceholder:
'[data-type="woocommerce/product-collection"] .components-placeholder',
productPicker: '.wc-block-editor-product-collection__product-picker',
+ taxonomyPicker:
+ '.wc-block-editor-product-collection__taxonomy-picker-selection',
+ pickerDoneButton: '.components-button.is-primary',
linkedProductControl: {
button: '.wc-block-product-collection-linked-product-control__button',
popoverContent:
@@ -71,6 +74,9 @@ export type Collections =
| 'onSale'
| 'featured'
| 'relatedProducts'
+ | 'handPicked'
+ | 'productsByCategory'
+ | 'productsByTag'
| 'productCatalog'
| 'myCustomCollection'
| 'myCustomCollectionWithPreview'
@@ -90,6 +96,9 @@ const collectionToButtonNameMap = {
onSale: 'On Sale Products',
featured: 'Featured Products',
relatedProducts: 'Related Products',
+ handPicked: 'Hand-Picked Products',
+ productsByCategory: 'Products by Category',
+ productsByTag: 'Products by Tag',
productCatalog: 'create your own',
myCustomCollection: 'My Custom Collection',
myCustomCollectionWithPreview: 'My Custom Collection with Preview',