Commit 961242a7201 for woocommerce
commit 961242a72010c3993a56e821c0e757cead9cc95f
Author: Tung Du <dinhtungdu@gmail.com>
Date: Fri Sep 25 13:22:07 2026 +0700
Restore the Product Collection editor E2E tests removed in #68617 (#69090)
Partially reverts #68617: restores the four editor E2E specs and the
page object, and removes the three Jest contract suites that mocked the
inspector and picker controls. Keeps the PHPUnit query tests.
Claude-Session: https://claude.ai/code/session_014r6PoAedUkWFLNpyerW69N
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/test/collection-contracts.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/test/collection-contracts.tsx
deleted file mode 100644
index ef0a43c8839..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/test/collection-contracts.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-/**
- * External dependencies
- */
-import { getBlockVariations, type BlockVariation } from '@wordpress/blocks';
-
-/**
- * Internal dependencies
- */
-import bestSellers from '../best-sellers';
-import byBrand from '../by-brand';
-import byCategory from '../by-category';
-import byTag from '../by-tag';
-import cartContents from '../cart-contents';
-import crossSells from '../cross-sells';
-import featured from '../featured';
-import handPicked from '../hand-picked';
-import newArrivals from '../new-arrivals';
-import onSale from '../on-sale';
-import productCatalog from '../product-collection';
-import related from '../related';
-import topRated from '../top-rated';
-import upsells from '../upsells';
-import {
- DEFAULT_ATTRIBUTES,
- DEFAULT_QUERY,
- INNER_BLOCKS_NO_RESULTS_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_TEMPLATE,
- PRODUCT_COLLECTION_BLOCK_NAME,
-} from '../../constants';
-import { registerCoreCollections } from '../../test/utils/contract-setup';
-import { CoreCollectionNames, CoreFilterNames } from '../../types';
-
-const productGrid = {
- type: 'flex',
- columns: 5,
- shrinkColumns: true,
-};
-
-const linkedProductGrid = {
- type: 'flex',
- columns: 4,
- shrinkColumns: true,
-};
-
-const chooserScope = [ 'inserter', 'block' ];
-
-const heading = ( content: string, textAlign = 'center' ) => [
- 'core/heading',
- {
- textAlign,
- level: 2,
- content,
- style: { spacing: { margin: { bottom: '1rem' } } },
- },
-];
-
-const collectionRows = [
- {
- caseName: 'Product Catalog',
- definition: productCatalog,
- name: CoreCollectionNames.PRODUCT_CATALOG,
- scope: [],
- query: DEFAULT_QUERY,
- displayLayout: DEFAULT_ATTRIBUTES.displayLayout,
- hideControls: [ CoreFilterNames.INHERIT ],
- usesReference: undefined,
- innerBlocks: INNER_BLOCKS_TEMPLATE,
- },
- {
- caseName: 'Featured Products',
- definition: featured,
- name: CoreCollectionNames.FEATURED,
- scope: chooserScope,
- query: { ...DEFAULT_QUERY, featured: true, perPage: 5, pages: 1 },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.FEATURED,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Featured products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'New Arrivals',
- definition: newArrivals,
- name: CoreCollectionNames.NEW_ARRIVALS,
- scope: chooserScope,
- query: {
- ...DEFAULT_QUERY,
- orderBy: 'date',
- order: 'desc',
- perPage: 5,
- pages: 1,
- timeFrame: { operator: 'in', value: '-7 days' },
- },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.ORDER,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'New arrivals' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'On Sale Products',
- definition: onSale,
- name: CoreCollectionNames.ON_SALE,
- scope: chooserScope,
- query: {
- ...DEFAULT_QUERY,
- woocommerceOnSale: true,
- perPage: 5,
- pages: 1,
- },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.ON_SALE,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'On sale products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'Best Sellers',
- definition: bestSellers,
- name: CoreCollectionNames.BEST_SELLERS,
- scope: chooserScope,
- query: {
- ...DEFAULT_QUERY,
- orderBy: 'popularity',
- order: 'desc',
- perPage: 5,
- pages: 1,
- },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.ORDER,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Best selling products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'Top Rated Products',
- definition: topRated,
- name: CoreCollectionNames.TOP_RATED,
- scope: chooserScope,
- query: {
- ...DEFAULT_QUERY,
- orderBy: 'rating',
- order: 'desc',
- perPage: 5,
- pages: 1,
- },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.ORDER,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Top rated products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'Hand-Picked Products',
- definition: handPicked,
- name: CoreCollectionNames.HAND_PICKED,
- scope: chooserScope,
- query: { ...DEFAULT_QUERY, orderBy: 'post__in' },
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.HAND_PICKED,
- CoreFilterNames.FILTERABLE,
- CoreFilterNames.ORDER,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Recommended products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- ],
- },
- {
- caseName: 'Products by Category',
- definition: byCategory,
- name: CoreCollectionNames.BY_CATEGORY,
- scope: chooserScope,
- query: DEFAULT_QUERY,
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.HAND_PICKED,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Products by Category' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- ],
- },
- {
- caseName: 'Products by Tag',
- definition: byTag,
- name: CoreCollectionNames.BY_TAG,
- scope: chooserScope,
- query: DEFAULT_QUERY,
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.HAND_PICKED,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Products by Tag' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- ],
- },
- {
- caseName: 'Products by Brand',
- definition: byBrand,
- name: CoreCollectionNames.BY_BRAND,
- scope: chooserScope,
- query: DEFAULT_QUERY,
- displayLayout: productGrid,
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.HAND_PICKED,
- CoreFilterNames.FILTERABLE,
- ],
- usesReference: undefined,
- innerBlocks: [
- heading( 'Products by Brand' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- ],
- },
- {
- caseName: 'Related Products',
- definition: related,
- name: CoreCollectionNames.RELATED,
- scope: chooserScope,
- query: { ...DEFAULT_QUERY, perPage: 4, pages: 1 },
- displayLayout: linkedProductGrid,
- hideControls: [ CoreFilterNames.INHERIT ],
- usesReference: [ 'product' ],
- innerBlocks: [
- heading( 'Related Products' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'Upsells',
- definition: upsells,
- name: CoreCollectionNames.UPSELLS,
- scope: chooserScope,
- query: { ...DEFAULT_QUERY, perPage: 8, pages: 1 },
- displayLayout: linkedProductGrid,
- hideControls: [ CoreFilterNames.INHERIT, CoreFilterNames.FILTERABLE ],
- usesReference: [ 'product', 'cart', 'order' ],
- innerBlocks: [
- heading( 'You may also like', 'left' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
- {
- caseName: 'Cross-Sells',
- definition: crossSells,
- name: CoreCollectionNames.CROSS_SELLS,
- scope: chooserScope,
- query: { ...DEFAULT_QUERY, perPage: 8, pages: 1 },
- displayLayout: linkedProductGrid,
- hideControls: [ CoreFilterNames.INHERIT, CoreFilterNames.FILTERABLE ],
- usesReference: [ 'product', 'cart', 'order' ],
- innerBlocks: [
- heading( 'You may be interested in…', 'left' ),
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- ],
- },
-] as const;
-
-const emailRows = [
- {
- caseName: 'Cart Contents',
- definition: cartContents,
- name: CoreCollectionNames.CART_CONTENTS,
- scope: chooserScope,
- query: {
- ...DEFAULT_QUERY,
- inherit: false,
- perPage: 10,
- pages: 1,
- },
- displayLayout: {
- type: 'flex',
- columns: 1,
- shrinkColumns: true,
- },
- hideControls: [
- CoreFilterNames.INHERIT,
- CoreFilterNames.ATTRIBUTES,
- CoreFilterNames.KEYWORD,
- CoreFilterNames.ORDER,
- CoreFilterNames.DEFAULT_ORDER,
- CoreFilterNames.FEATURED,
- CoreFilterNames.ON_SALE,
- CoreFilterNames.STOCK_STATUS,
- CoreFilterNames.HAND_PICKED,
- CoreFilterNames.TAXONOMY,
- CoreFilterNames.FILTERABLE,
- CoreFilterNames.CREATED,
- CoreFilterNames.PRICE_RANGE,
- ],
- usesReference: undefined,
- innerBlocks: [ heading( 'Your Cart' ), INNER_BLOCKS_PRODUCT_TEMPLATE ],
- },
-] as const;
-
-const allRows = [ ...collectionRows, ...emailRows ];
-
-const getRegisteredCollection = ( name: string ): BlockVariation => {
- const variation = getBlockVariations( PRODUCT_COLLECTION_BLOCK_NAME )?.find(
- ( candidate ) => candidate.name === name
- );
-
- if ( ! variation ) {
- throw new Error( `Expected registered collection "${ name }".` );
- }
-
- return variation;
-};
-
-let unregisterCoreCollections: () => void;
-
-beforeAll( () => {
- unregisterCoreCollections = registerCoreCollections();
-} );
-
-afterAll( () => {
- unregisterCoreCollections();
-} );
-
-describe( 'Product Collection built-in collection contracts', () => {
- it( 'registers chooser and email collections in their declared order', () => {
- expect(
- getBlockVariations( PRODUCT_COLLECTION_BLOCK_NAME )?.map(
- ( variation ) => variation.name
- )
- ).toEqual( allRows.map( ( row ) => row.name ) );
- } );
-
- it.each( allRows )(
- 'declares $caseName collection contract',
- ( {
- caseName,
- definition,
- name,
- scope,
- query,
- displayLayout,
- hideControls,
- usesReference,
- innerBlocks,
- } ) => {
- const variation = getRegisteredCollection( name );
-
- expect( variation.title ).toBe( caseName );
- expect( variation.scope ).toEqual( scope );
- expect( variation.attributes?.collection ).toBe( name );
- expect( variation.attributes?.query ).toEqual( query );
- expect( variation.attributes?.displayLayout ).toEqual(
- displayLayout
- );
- expect( variation.attributes?.hideControls ).toEqual(
- hideControls
- );
- expect( variation.innerBlocks ).toEqual( innerBlocks );
- expect(
- 'usesReference' in definition
- ? definition.usesReference
- : undefined
- ).toEqual( usesReference );
- }
- );
-
- // Every case above builds its expected query by spreading DEFAULT_QUERY, so the
- // shared defaults are only ever compared against themselves: changing perPage
- // from 9 leaves all of them green. The deleted "Items per page, offset & max
- // page to show" and "sort products by title" E2E titles were the only thing
- // holding these values, so pin them against literals here.
- it( 'pins the query defaults every collection inherits', () => {
- expect( DEFAULT_QUERY ).toMatchObject( {
- perPage: 9,
- pages: 0,
- offset: 0,
- postType: 'product',
- order: 'asc',
- orderBy: 'title',
- inherit: false,
- } );
- } );
-
- it( 'keeps the default no-results block in the catalog template', () => {
- expect(
- getRegisteredCollection( CoreCollectionNames.PRODUCT_CATALOG )
- ).toHaveProperty( 'innerBlocks', [
- INNER_BLOCKS_PRODUCT_TEMPLATE,
- INNER_BLOCKS_PAGINATION_TEMPLATE,
- INNER_BLOCKS_NO_RESULTS_TEMPLATE,
- ] );
- } );
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/test/control-contracts.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/test/control-contracts.tsx
deleted file mode 100644
index a64dc3bb23b..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/test/control-contracts.tsx
+++ /dev/null
@@ -1,708 +0,0 @@
-/**
- * External dependencies
- */
-import {
- act,
- fireEvent,
- render,
- screen,
- waitFor,
-} from '@testing-library/react';
-
-/**
- * Internal dependencies
- */
-import ProductCollectionInspectorControls from '..';
-import { DEFAULT_ATTRIBUTES, DEFAULT_QUERY } from '../../../constants';
-import { mockProductTaxonomies } from '../../../test/utils/contract-setup';
-import type {
- ProductCollectionContentProps,
- ProductCollectionAttributes,
-} from '../../../types';
-import { CoreFilterNames } from '../../../types';
-import { LocationType } from '../../../../product-template/utils';
-
-let mockIsEmailEditor = false;
-
-jest.mock( '@wordpress/block-editor', () => {
- const actual = jest.requireActual( '@wordpress/block-editor' );
- const React = jest.requireActual( 'react' );
-
- return {
- ...actual,
- InspectorControls: ( { children } ) =>
- React.createElement( 'div', null, children ),
- };
-} );
-
-jest.mock( '@woocommerce/email-editor', () => ( {
- useIsEmailEditor: () => mockIsEmailEditor,
-} ) );
-
-jest.mock( '@woocommerce/settings', () => ( {
- ...jest.requireActual( '@woocommerce/settings' ),
- ADMIN_URL: 'https://example.test/wp-admin/',
- getSetting: jest.fn( ( setting, defaultValue ) =>
- setting === 'stockStatusOptions'
- ? {
- instock: 'In stock',
- outofstock: 'Out of stock',
- }
- : defaultValue
- ),
-} ) );
-
-// Native inputs stand in for the real controls, so this suite proves the
-// setAttributes payloads, not the controls themselves. The retained E2E journeys
-// still drive the real inspector.
-jest.mock( '@wordpress/components', () => {
- const actual = jest.requireActual( '@wordpress/components' );
- const React = jest.requireActual( 'react' );
- const passThrough = ( { children } ) =>
- React.createElement( React.Fragment, null, children );
- const BaseControl = ( { children, label } ) =>
- React.createElement( 'fieldset', { 'aria-label': label }, children );
- BaseControl.VisualLabel = passThrough;
-
- return {
- ...actual,
- BaseControl,
- ExternalLink: ( { children, href } ) =>
- React.createElement( 'a', { href }, children ),
- Flex: passThrough,
- FlexItem: passThrough,
- FormTokenField: ( { label, onChange, value = [] } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- onChange: ( event ) =>
- onChange(
- event.target.value
- ? event.target.value
- .split( ',' )
- .map( ( token ) => token.trim() )
- : []
- ),
- value: value.join( ', ' ),
- } ),
- Notice: passThrough,
- PanelBody: passThrough,
- RadioControl: ( { onChange, options, selected } ) =>
- React.createElement(
- 'select',
- {
- 'aria-label': 'Created period',
- onChange: ( event ) => onChange( event.target.value ),
- value: selected || '',
- },
- React.createElement( 'option', { value: '' }, 'Select' ),
- options.map( ( option ) =>
- React.createElement(
- 'option',
- { key: option.value, value: option.value },
- option.label
- )
- )
- ),
- RangeControl: ( { label, max, min, onChange, value } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- max,
- min,
- onChange: ( event ) => onChange( Number( event.target.value ) ),
- type: 'range',
- value,
- } ),
- SelectControl: ( { label, onChange, options, value } ) =>
- React.createElement(
- 'select',
- {
- 'aria-label': label,
- onChange: ( event ) => onChange( event.target.value ),
- value,
- },
- options.map( ( option ) =>
- React.createElement(
- 'option',
- { key: option.value, value: option.value },
- option.label
- )
- )
- ),
- TextControl: ( { label, onChange, value } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- onChange: ( event ) => onChange( event.target.value ),
- value,
- } ),
- ToggleControl: ( { checked, label, onChange } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- checked,
- onChange: ( event ) => onChange( event.target.checked ),
- type: 'checkbox',
- } ),
- __experimentalHStack: passThrough,
- __experimentalInputControl: ( {
- label,
- onBlur,
- onChange,
- onKeyDown,
- value,
- } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- onBlur,
- onChange: ( event ) => onChange( event.target.value ),
- onKeyDown,
- value: value || '',
- } ),
- __experimentalInputControlPrefixWrapper: passThrough,
- __experimentalNumberControl: ( { label, min, onChange, value } ) =>
- React.createElement( 'input', {
- 'aria-label': label,
- min,
- onChange: ( event ) => onChange( Number( event.target.value ) ),
- type: 'number',
- value,
- } ),
- __experimentalToggleGroupControl: ( {
- children,
- label,
- onChange,
- value,
- } ) =>
- React.createElement(
- 'select',
- {
- 'aria-label': label,
- onChange: ( event ) => onChange( event.target.value ),
- value,
- },
- children
- ),
- __experimentalToggleGroupControlOption: ( { label, value } ) =>
- React.createElement( 'option', { value }, label ),
- __experimentalToolsPanel: ( { children, label } ) =>
- React.createElement( 'section', { 'aria-label': label }, children ),
- __experimentalToolsPanelItem: ( { children, label, onDeselect } ) =>
- React.createElement(
- 'section',
- { 'aria-label': label },
- children,
- onDeselect &&
- React.createElement(
- 'button',
- { onClick: onDeselect, type: 'button' },
- `Reset ${ label }`
- )
- ),
- __experimentalUnitControl: ( { onChange, value } ) =>
- React.createElement( 'input', {
- 'aria-label': 'Fixed width',
- onChange: ( event ) => onChange( event.target.value ),
- value,
- } ),
- };
-} );
-
-jest.mock(
- '@woocommerce/editor-components/product-attribute-term-control',
- () => ( props ) => {
- const React = jest.requireActual( 'react' );
- return React.createElement(
- 'button',
- {
- onClick: () =>
- props.onChange( [ { id: 41, value: 'pa_color' } ] ),
- type: 'button',
- },
- 'Choose product attribute'
- );
- }
-);
-
-jest.mock(
- '@woocommerce/editor-components/product-category-control',
- () => ( props ) => {
- const React = jest.requireActual( 'react' );
- return React.createElement(
- 'button',
- {
- onClick: () => props.onChange( [ { id: 31 } ] ),
- type: 'button',
- },
- 'Choose product category'
- );
- }
-);
-
-jest.mock(
- '@woocommerce/editor-components/product-tag-control',
- () => ( props ) => {
- const React = jest.requireActual( 'react' );
- return React.createElement(
- 'button',
- {
- onClick: () => props.onChange( [ { id: 32 } ] ),
- type: 'button',
- },
- 'Choose product tag'
- );
- }
-);
-
-jest.mock(
- '@woocommerce/editor-components/product-brand-control',
- () => ( props ) => {
- const React = jest.requireActual( 'react' );
- return React.createElement(
- 'button',
- {
- onClick: () => props.onChange( [ { id: 33 } ] ),
- type: 'button',
- },
- 'Choose product brand'
- );
- }
-);
-
-jest.mock( '@woocommerce/editor-components/utils', () => ( {
- ...jest.requireActual( '@woocommerce/editor-components/utils' ),
- getProducts: jest.fn().mockResolvedValue( [
- { id: 71, name: 'Beanie' },
- { id: 72, name: 'Cap' },
- ] ),
-} ) );
-
-jest.mock( '@woocommerce/tracks', () => ( {
- recordEvent: jest.fn(),
-} ) );
-
-const createAttributes = (
- overrides: Partial< ProductCollectionAttributes > = {}
-): ProductCollectionAttributes => ( {
- ...DEFAULT_ATTRIBUTES,
- collection: undefined,
- convertedFromProducts: false,
- hideControls: [ CoreFilterNames.HAND_PICKED ],
- query: { ...DEFAULT_QUERY },
- queryContext: [ { page: 1 } ],
- queryId: 1,
- templateSlug: '',
- ...overrides,
-} );
-
-const renderInspector = ( {
- attributes = createAttributes(),
- templateSlug = 'single-product',
-}: {
- attributes?: ProductCollectionAttributes;
- templateSlug?: string;
-} = {} ) => {
- const setAttributes = jest.fn();
- const props = {
- attributes,
- clientId: 'product-collection-test',
- context: { templateSlug },
- insertBlocksAfter: jest.fn(),
- isSelected: true,
- isUsingReferencePreviewMode: false,
- location: { type: LocationType.Site },
- name: 'woocommerce/product-collection',
- onReplace: jest.fn(),
- openCollectionSelectionModal: jest.fn(),
- setAttributes,
- tracksLocation: 'single-product',
- } as unknown as ProductCollectionContentProps;
-
- const renderResult = render(
- <ProductCollectionInspectorControls { ...props } />
- );
-
- return { ...renderResult, setAttributes };
-};
-
-describe( 'Product Collection inspector control contracts', () => {
- beforeEach( () => {
- mockIsEmailEditor = false;
- mockProductTaxonomies();
- } );
-
- afterEach( () => {
- jest.restoreAllMocks();
- } );
-
- it( 'writes the complete nested query from the products-per-page control', () => {
- const { setAttributes } = renderInspector();
-
- fireEvent.change(
- screen.getByRole( 'slider', { name: 'Products per page' } ),
- { target: { value: '12' } }
- );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- perPage: 12,
- },
- } );
- } );
-
- it.each( [
- [ 'order', 'Order by', 'combobox' ],
- [ 'keyword', 'Keyword', 'textbox' ],
- [ 'offset', 'Offset', 'spinbutton' ],
- [ 'max-pages-to-show', 'Max pages to show', 'spinbutton' ],
- [ 'products-per-page', 'Products per page', 'slider' ],
- [ 'on-sale', 'Show only products on sale', 'checkbox' ],
- [ 'featured', 'Show only featured products', 'checkbox' ],
- ] as const )(
- 'honors the %s hidden-control contract',
- ( control, label, role ) => {
- renderInspector( {
- attributes: createAttributes( {
- hideControls: [ CoreFilterNames.HAND_PICKED, control ],
- } ),
- } );
-
- expect(
- screen.queryByRole( role, { name: label } )
- ).not.toBeInTheDocument();
- }
- );
-
- it( 'writes the debounced keyword to the complete nested query', () => {
- jest.useFakeTimers();
- const { setAttributes, unmount } = renderInspector();
-
- try {
- fireEvent.change(
- screen.getByRole( 'textbox', { name: 'Keyword' } ),
- { target: { value: 'beanie' } }
- );
- expect( setAttributes ).not.toHaveBeenCalled();
-
- act( () => {
- jest.advanceTimersByTime( 250 );
- } );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- search: 'beanie',
- },
- } );
- } finally {
- unmount();
- jest.useRealTimers();
- }
- } );
-
- it.each( [
- [ 'category', 'product_cat', 31 ],
- [ 'tag', 'product_tag', 32 ],
- [ 'brand', 'product_brand', 33 ],
- ] as const )(
- 'writes the %s taxonomy selection to the complete nested query',
- ( control, taxonomy, termId ) => {
- const { setAttributes } = renderInspector();
-
- fireEvent.click(
- screen.getByRole( 'button', {
- name: `Choose product ${ control }`,
- } )
- );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- taxQuery: { [ taxonomy ]: [ termId ] },
- },
- } );
- }
- );
-
- it( 'switches the visible controls for inherited, carousel, and email contexts', () => {
- const inherited = createAttributes( {
- query: { ...DEFAULT_QUERY, inherit: true },
- } );
- const { rerender } = renderInspector( {
- attributes: inherited,
- templateSlug: 'archive-product',
- } );
-
- expect(
- screen.getByRole( 'combobox', { name: 'Query type' } )
- ).toBeInTheDocument();
- expect(
- screen.queryByRole( 'region', { name: 'Filters' } )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole( 'spinbutton', { name: 'Offset' } )
- ).not.toBeInTheDocument();
-
- const carousel = {
- ...createAttributes(),
- displayLayout: {
- ...DEFAULT_ATTRIBUTES.displayLayout,
- type: 'carousel',
- },
- } as ProductCollectionAttributes;
- rerender(
- <ProductCollectionInspectorControls
- { ...( {
- attributes: carousel,
- clientId: 'product-collection-test',
- context: { templateSlug: 'single-product' },
- location: { type: LocationType.Site },
- setAttributes: jest.fn(),
- } as unknown as ProductCollectionContentProps ) }
- />
- );
- expect(
- screen.getByRole( 'slider', { name: 'Products in carousel' } )
- ).toBeInTheDocument();
- expect(
- screen.queryByRole( 'slider', { name: 'Columns' } )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole( 'spinbutton', {
- name: 'Max pages to show',
- } )
- ).not.toBeInTheDocument();
-
- mockIsEmailEditor = true;
- rerender(
- <ProductCollectionInspectorControls
- { ...( {
- attributes: createAttributes(),
- clientId: 'product-collection-test',
- context: { templateSlug: 'single-product' },
- location: { type: LocationType.Site },
- setAttributes: jest.fn(),
- } as unknown as ProductCollectionContentProps ) }
- />
- );
- expect(
- screen.getByRole( 'slider', { name: 'Number of products' } )
- ).toBeInTheDocument();
- expect(
- screen.queryByRole( 'combobox', { name: 'Layout' } )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole( 'combobox', { name: 'Width' } )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole( 'spinbutton', { name: 'Offset' } )
- ).not.toBeInTheDocument();
- } );
-
- it.each( [
- [
- 'Order by',
- 'combobox',
- 'title/desc',
- { orderBy: 'title', order: 'desc' },
- ],
- [ 'Offset', 'spinbutton', '4', { offset: 4 } ],
- [ 'Max pages to show', 'spinbutton', '2', { pages: 2 } ],
- ] as const )(
- 'writes the complete nested query from %s',
- ( label, role, value, update ) => {
- const { setAttributes } = renderInspector();
- fireEvent.change( screen.getByRole( role, { name: label } ), {
- target: { value },
- } );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: { ...DEFAULT_QUERY, ...update },
- } );
- }
- );
-
- it.each( [
- [ 'Show only products on sale', { woocommerceOnSale: true } ],
- [ 'Show only featured products', { featured: true } ],
- ] as const )(
- 'writes the complete nested query from %s',
- ( label, update ) => {
- const { setAttributes } = renderInspector();
- fireEvent.click( screen.getByRole( 'checkbox', { name: label } ) );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: { ...DEFAULT_QUERY, ...update },
- } );
- }
- );
-
- it( 'writes stock and attribute filters', () => {
- const { setAttributes } = renderInspector();
-
- fireEvent.change(
- screen.getByRole( 'textbox', { name: 'Stock Status' } ),
- {
- target: { value: 'In stock' },
- }
- );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- woocommerceStockStatus: [ 'instock' ],
- },
- } );
-
- fireEvent.click( screen.getByText( 'Choose product attribute' ) );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- woocommerceAttributes: [ { taxonomy: 'pa_color', termId: 41 } ],
- },
- } );
- } );
-
- it( 'writes the created period with its default within operator', () => {
- const { setAttributes } = renderInspector();
-
- fireEvent.change(
- screen.getByRole( 'combobox', {
- name: 'Created period',
- } ),
- {
- target: { value: '-7 days' },
- }
- );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- timeFrame: { operator: 'in', value: '-7 days' },
- },
- } );
- } );
-
- it.each( [
- [ 'in', 'not-in' ],
- [ 'not-in', 'in' ],
- ] as const )(
- 'writes the created operator from %s to %s',
- ( initialOperator, nextOperator ) => {
- const attributes = createAttributes( {
- query: {
- ...DEFAULT_QUERY,
- timeFrame: {
- operator: initialOperator,
- value: '-7 days',
- },
- },
- } );
- const { setAttributes } = renderInspector( { attributes } );
-
- fireEvent.change(
- screen.getByRole( 'combobox', { name: 'Created' } ),
- { target: { value: nextOperator } }
- );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...attributes.query,
- timeFrame: {
- operator: nextOperator,
- value: '-7 days',
- },
- },
- } );
- }
- );
-
- it.each( [
- [ 'MIN', '10', { max: 20, min: 10 } ],
- [ 'MAX', '30', { max: 30, min: 5 } ],
- ] as const )(
- 'writes the %s price boundary',
- ( label, value, expectedPriceRange ) => {
- const attributes = createAttributes( {
- query: {
- ...DEFAULT_QUERY,
- priceRange: { max: 20, min: 5 },
- },
- } );
- const { setAttributes } = renderInspector( { attributes } );
-
- fireEvent.change( screen.getByRole( 'textbox', { name: label } ), {
- target: { value },
- } );
- fireEvent.blur( screen.getByRole( 'textbox', { name: label } ) );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...attributes.query,
- priceRange: expectedPriceRange,
- },
- } );
- }
- );
-
- it( 'resets the price range', () => {
- const attributes = createAttributes( {
- query: {
- ...DEFAULT_QUERY,
- priceRange: { max: 20, min: 5 },
- },
- } );
- const { setAttributes } = renderInspector( { attributes } );
-
- fireEvent.click(
- screen.getByRole( 'button', { name: 'Reset Price Range' } )
- );
-
- expect( setAttributes ).toHaveBeenCalledWith( {
- query: {
- ...attributes.query,
- priceRange: undefined,
- },
- } );
- } );
-
- it( 'writes the hand-picked product filter after remote data resolves', async () => {
- const { setAttributes } = renderInspector( {
- attributes: createAttributes( { hideControls: [] } ),
- } );
- const field = screen.getByRole( 'textbox', { name: 'Hand-Picked' } );
- await waitFor( () => expect( field ).toHaveValue( '' ) );
-
- fireEvent.change( field, {
- target: { value: 'Cap, Beanie' },
- } );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...DEFAULT_QUERY,
- woocommerceHandPickedProducts: [ '72', '71' ],
- },
- } );
- } );
-
- it( 'writes display settings and resets the products-per-page query', () => {
- const attributes = createAttributes( {
- query: { ...DEFAULT_QUERY, perPage: 12 },
- } );
- const { setAttributes } = renderInspector( { attributes } );
-
- fireEvent.change( screen.getByRole( 'slider', { name: 'Columns' } ), {
- target: { value: '4' },
- } );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- displayLayout: {
- ...DEFAULT_ATTRIBUTES.displayLayout,
- columns: 4,
- },
- } );
-
- fireEvent.click(
- screen.getByRole( 'button', {
- name: 'Reset Products per page',
- } )
- );
- expect( setAttributes ).toHaveBeenLastCalledWith( {
- query: { ...attributes.query, perPage: DEFAULT_QUERY.perPage },
- } );
- } );
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/test/picker-contracts.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/test/picker-contracts.tsx
deleted file mode 100644
index 3d47af928bf..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/test/picker-contracts.tsx
+++ /dev/null
@@ -1,648 +0,0 @@
-/**
- * External dependencies
- */
-import { act, render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import {
- BlockEditorProvider,
- store as blockEditorStore,
-} from '@wordpress/block-editor';
-import { type BlockInstance } from '@wordpress/blocks';
-import { dispatch } from '@wordpress/data';
-import { useState, type ReactNode } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import Edit from '..';
-import LinkedProductControl from '../inspector-controls/linked-product-control';
-import MultiProductPicker from '../multi-product-picker';
-import SingleProductPicker from '../single-product-picker';
-import TaxonomyPicker from '../taxonomy-picker';
-import {
- DEFAULT_ATTRIBUTES,
- DEFAULT_QUERY,
- PRODUCT_COLLECTION_BLOCK_NAME,
-} from '../../constants';
-import {
- mockProductTaxonomies,
- registerCoreCollections,
-} from '../../test/utils/contract-setup';
-import {
- CoreCollectionNames,
- type ProductCollectionAttributes,
- type ProductCollectionEditComponentProps,
- type ProductCollectionQuery,
- type ProductCollectionSetAttributes,
-} from '../../types';
-import {
- LocationType,
- type WooCommerceBlockLocation,
-} from '../../../product-template/utils';
-
-jest.mock( '@woocommerce/editor-components/products-control', () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange, selected } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose hand-picked products',
- 'data-selected': selected.join( ',' ),
- onClick: () => onChange( [ { id: 71 }, { id: 72 } ] ),
- type: 'button',
- },
- 'Choose hand-picked products'
- );
-} );
-
-jest.mock( '@woocommerce/editor-components/product-category-control', () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange, selected } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose product category',
- 'data-selected': selected.join( ',' ),
- onClick: () => onChange( [ { id: 31 } ] ),
- type: 'button',
- },
- 'Choose product category'
- );
-} );
-
-jest.mock( '@woocommerce/editor-components/product-tag-control', () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange, selected } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose product tag',
- 'data-selected': selected.join( ',' ),
- onClick: () => onChange( [ { id: '32' } ] ),
- type: 'button',
- },
- 'Choose product tag'
- );
-} );
-
-jest.mock( '@woocommerce/editor-components/product-brand-control', () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange, selected } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose product brand',
- 'data-selected': selected.join( ',' ),
- onClick: () => onChange( [ { id: 33 } ] ),
- type: 'button',
- },
- 'Choose product brand'
- );
-} );
-
-jest.mock( '@woocommerce/editor-components/product-control', () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange, selected } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose product reference',
- 'data-selected': String( selected ?? '' ),
- onClick: () => onChange( [ { id: 73 } ] ),
- type: 'button',
- },
- 'Choose product reference'
- );
-} );
-
-jest.mock(
- '@woocommerce/editor-components/product-attribute-term-control',
- () => {
- const React = jest.requireActual( 'react' );
-
- return ( { onChange } ) =>
- React.createElement(
- 'button',
- {
- 'aria-label': 'Choose product attribute',
- onClick: () =>
- onChange( [ { id: 41, value: 'pa_color' } ] ),
- type: 'button',
- },
- 'Choose product attribute'
- );
- }
-);
-
-jest.mock( '@woocommerce/editor-components/utils', () => ( {
- ...jest.requireActual( '@woocommerce/editor-components/utils' ),
- getProduct: jest.fn( ( id: number ) =>
- Promise.resolve( {
- id,
- images: [],
- name: id === 71 ? 'Beanie' : `Product ${ id }`,
- sku: `sku-${ id }`,
- } )
- ),
- getProducts: jest.fn().mockResolvedValue( [
- { id: 71, name: 'Beanie' },
- { id: 72, name: 'Cap' },
- ] ),
-} ) );
-
-const createAttributes = (
- collection: string,
- query: Partial< ProductCollectionQuery > = {}
-): ProductCollectionAttributes => ( {
- ...DEFAULT_ATTRIBUTES,
- collection,
- convertedFromProducts: false,
- filterable: false,
- hideControls: [],
- query: { ...DEFAULT_QUERY, ...query },
- queryContext: [ { page: 1 } ],
- queryId: 1,
- templateSlug: '',
-} );
-
-const makeEditProps = (
- attributes: ProductCollectionAttributes,
- setAttributes: ProductCollectionSetAttributes,
- usesReference?: string[]
-): ProductCollectionEditComponentProps =>
- ( {
- attributes,
- clientId: 'product-collection-picker-contract',
- context: { templateSlug: '' },
- insertBlocksAfter: jest.fn(),
- isSelected: true,
- name: PRODUCT_COLLECTION_BLOCK_NAME,
- onReplace: jest.fn(),
- setAttributes,
- tracksLocation: 'other',
- usesReference,
- } ) as unknown as ProductCollectionEditComponentProps;
-
-const GutenbergProvider = ( { children }: { children: ReactNode } ) => {
- const [ blocks, setBlocks ] = useState< BlockInstance[] >( [] );
-
- return (
- <BlockEditorProvider
- value={ blocks }
- onInput={ setBlocks }
- onChange={ setBlocks }
- settings={ {} }
- >
- { children }
- </BlockEditorProvider>
- );
-};
-
-const StatefulAttributes = ( {
- children,
- initialAttributes,
- onSetAttributes = jest.fn(),
-}: {
- children: (
- attributes: ProductCollectionAttributes,
- setAttributes: ProductCollectionSetAttributes
- ) => ReactNode;
- initialAttributes: ProductCollectionAttributes;
- onSetAttributes?: jest.Mock;
-} ) => {
- const [ attributes, setCurrentAttributes ] = useState( initialAttributes );
- const setAttributes: ProductCollectionSetAttributes = ( updates ) => {
- onSetAttributes( updates );
- setCurrentAttributes( ( current ) => ( {
- ...current,
- ...updates,
- } ) );
- };
-
- return (
- <>
- { children( attributes, setAttributes ) }
- <output data-testid="current-query">
- { JSON.stringify( attributes.query ) }
- </output>
- </>
- );
-};
-
-const renderInEditor = ( children: ReactNode ) =>
- render( <GutenbergProvider>{ children }</GutenbergProvider> );
-
-const click = async (
- user: ReturnType< typeof userEvent.setup >,
- element: HTMLElement
-) => {
- await act( async () => {
- await user.click( element );
- } );
-};
-
-let unregisterCoreCollections: () => void;
-
-beforeAll( () => {
- unregisterCoreCollections = registerCoreCollections();
-} );
-
-beforeEach( () => {
- dispatch( blockEditorStore ).resetBlocks( [] );
- mockProductTaxonomies();
-} );
-
-afterEach( () => {
- jest.restoreAllMocks();
-} );
-
-afterAll( () => {
- unregisterCoreCollections();
-} );
-
-describe( 'collection-specific picker contracts', () => {
- it( 'writes hand-picked products and enables Done before dismissal', async () => {
- const user = userEvent.setup();
- const onDone = jest.fn();
- const onSetAttributes = jest.fn();
- const attributes = createAttributes( CoreCollectionNames.HAND_PICKED, {
- search: 'shirts',
- } );
-
- renderInEditor(
- <StatefulAttributes
- initialAttributes={ attributes }
- onSetAttributes={ onSetAttributes }
- >
- { ( currentAttributes, setAttributes ) => (
- <MultiProductPicker
- { ...makeEditProps( currentAttributes, setAttributes ) }
- onDone={ onDone }
- />
- ) }
- </StatefulAttributes>
- );
-
- const done = screen.getByRole( 'button', { name: 'Done' } );
- expect( done ).toBeDisabled();
-
- await click(
- user,
- screen.getByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- );
-
- expect( onSetAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...attributes.query,
- woocommerceHandPickedProducts: [ '71', '72' ],
- },
- } );
- expect( done ).toBeEnabled();
- await click( user, done );
- expect( onDone ).toHaveBeenCalledTimes( 1 );
- } );
-
- it( 'writes Products by Category taxonomy selection', async () => {
- const user = userEvent.setup();
- const onDone = jest.fn();
- const onSetAttributes = jest.fn();
- const attributes = createAttributes( CoreCollectionNames.BY_CATEGORY, {
- search: 'hoodie',
- taxQuery: { product_tag: [ 17 ] },
- } );
-
- renderInEditor(
- <StatefulAttributes
- initialAttributes={ attributes }
- onSetAttributes={ onSetAttributes }
- >
- { ( currentAttributes, setAttributes ) => (
- <TaxonomyPicker
- { ...makeEditProps( currentAttributes, setAttributes ) }
- onDone={ onDone }
- />
- ) }
- </StatefulAttributes>
- );
-
- const done = screen.getByRole( 'button', { name: 'Done' } );
- expect( done ).toBeDisabled();
- await click(
- user,
- screen.getByRole( 'button', {
- name: 'Choose product category',
- } )
- );
-
- expect( onSetAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...attributes.query,
- taxQuery: {
- product_tag: [ 17 ],
- product_cat: [ 31 ],
- },
- },
- } );
- expect( done ).toBeEnabled();
- await click( user, done );
- expect( onDone ).toHaveBeenCalledTimes( 1 );
- } );
-
- it.each( [
- {
- caseName: 'Products by Tag',
- collection: CoreCollectionNames.BY_TAG,
- controlName: 'Choose product tag',
- taxonomy: 'product_tag',
- termId: 32,
- },
- {
- caseName: 'Products by Brand',
- collection: CoreCollectionNames.BY_BRAND,
- controlName: 'Choose product brand',
- taxonomy: 'product_brand',
- termId: 33,
- },
- ] )( 'writes $caseName taxonomy selection', async ( row ) => {
- const user = userEvent.setup();
- const onSetAttributes = jest.fn();
- const attributes = createAttributes( row.collection );
-
- renderInEditor(
- <StatefulAttributes
- initialAttributes={ attributes }
- onSetAttributes={ onSetAttributes }
- >
- { ( currentAttributes, setAttributes ) => (
- <TaxonomyPicker
- { ...makeEditProps( currentAttributes, setAttributes ) }
- onDone={ jest.fn() }
- />
- ) }
- </StatefulAttributes>
- );
-
- await click(
- user,
- screen.getByRole( 'button', { name: row.controlName } )
- );
-
- expect( onSetAttributes ).toHaveBeenLastCalledWith( {
- query: {
- ...attributes.query,
- taxQuery: { [ row.taxonomy ]: [ row.termId ] },
- },
- } );
- expect( screen.getByRole( 'button', { name: 'Done' } ) ).toBeEnabled();
- } );
-
- it( 'writes an exact single-product reference', async () => {
- const user = userEvent.setup();
- const onSetAttributes = jest.fn();
- const attributes = createAttributes( CoreCollectionNames.RELATED, {
- search: 'hoodie',
- } );
-
- renderInEditor(
- <StatefulAttributes
- initialAttributes={ attributes }
- onSetAttributes={ onSetAttributes }
- >
- { ( currentAttributes, setAttributes ) => (
- <SingleProductPicker
- { ...makeEditProps( currentAttributes, setAttributes, [
- 'product',
- ] ) }
- isDeletedProductReference={ false }
- />
- ) }
- </StatefulAttributes>
- );
-
- await click(
- user,
- screen.getByRole( 'button', {
- name: 'Choose product reference',
- } )
- );
- expect( onSetAttributes ).toHaveBeenLastCalledWith( {
- query: { ...attributes.query, productReference: 73 },
- } );
- } );
-} );
-
-describe( 'Edit picker-state contracts', () => {
- it( 'dismisses and reactivates the hand-picked picker', async () => {
- const user = userEvent.setup();
- const setAttributes = jest.fn();
- const empty = createAttributes( CoreCollectionNames.HAND_PICKED );
- const selected = createAttributes( CoreCollectionNames.HAND_PICKED, {
- woocommerceHandPickedProducts: [ '71', '72' ],
- } );
- const { rerender } = renderInEditor(
- <Edit { ...makeEditProps( empty, setAttributes ) } />
- );
-
- expect(
- await screen.findByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).toBeInTheDocument();
-
- rerender(
- <GutenbergProvider>
- <Edit { ...makeEditProps( selected, setAttributes ) } />
- </GutenbergProvider>
- );
- const done = screen.getByRole( 'button', { name: 'Done' } );
- expect( done ).toBeEnabled();
- await click( user, done );
- await waitFor( () =>
- expect(
- screen.queryByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).not.toBeInTheDocument()
- );
-
- rerender(
- <GutenbergProvider>
- <Edit { ...makeEditProps( empty, setAttributes ) } />
- </GutenbergProvider>
- );
- expect(
- await screen.findByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).toBeInTheDocument();
- } );
-
- it( 'drops stale pickers when collections switch', async () => {
- const setAttributes = jest.fn();
- const { rerender } = renderInEditor(
- <Edit
- { ...makeEditProps(
- createAttributes( CoreCollectionNames.HAND_PICKED ),
- setAttributes
- ) }
- />
- );
-
- expect(
- await screen.findByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).toBeInTheDocument();
-
- rerender(
- <GutenbergProvider>
- <Edit
- { ...makeEditProps(
- createAttributes( CoreCollectionNames.BY_CATEGORY ),
- setAttributes
- ) }
- />
- </GutenbergProvider>
- );
- expect(
- await screen.findByRole( 'button', {
- name: 'Choose product category',
- } )
- ).toBeInTheDocument();
- expect(
- screen.queryByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).not.toBeInTheDocument();
-
- setAttributes.mockClear();
- rerender(
- <GutenbergProvider>
- <Edit
- { ...makeEditProps(
- createAttributes( CoreCollectionNames.FEATURED ),
- setAttributes
- ) }
- />
- </GutenbergProvider>
- );
- await waitFor( () =>
- expect( setAttributes ).toHaveBeenCalledWith(
- expect.objectContaining( {
- collection: CoreCollectionNames.FEATURED,
- } )
- )
- );
- expect(
- screen.queryByRole( 'button', {
- name: 'Choose hand-picked products',
- } )
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole( 'button', {
- name: 'Choose product category',
- } )
- ).not.toBeInTheDocument();
- } );
-} );
-
-describe( 'linked-product reference contracts', () => {
- it.each( [
- {
- caseName: 'product',
- label: 'From the current product',
- location: LocationType.Product,
- usesReference: [ 'product' ],
- },
- {
- caseName: 'cart',
- label: 'From products in the cart',
- location: LocationType.Cart,
- usesReference: [ 'cart' ],
- },
- {
- caseName: 'order',
- label: 'From products in the order',
- location: LocationType.Order,
- usesReference: [ 'order' ],
- },
- {
- caseName: 'multiple',
- label: 'From the current product',
- location: LocationType.Product,
- usesReference: [ 'product', 'cart', 'order' ],
- },
- ] )( 'defaults $caseName context to its current reference', ( row ) => {
- renderInEditor(
- <LinkedProductControl
- query={ { ...DEFAULT_QUERY } }
- setAttributes={ jest.fn() }
- location={ { type: row.location } as WooCommerceBlockLocation }
- usesReference={ row.usesReference }
- />
- );
-
- expect(
- screen.getByRole( 'radio', { name: row.label } )
- ).toBeChecked();
- expect(
- screen.getByRole( 'radio', {
- name: 'From a specific product',
- } )
- ).not.toBeChecked();
- } );
-
- it( 'replaces the exact linked product and preserves the query', async () => {
- const user = userEvent.setup();
- const onSetAttributes = jest.fn();
- const initialQuery = {
- ...DEFAULT_QUERY,
- productReference: 71,
- search: 'hoodie',
- };
-
- renderInEditor(
- <StatefulAttributes
- initialAttributes={ createAttributes(
- CoreCollectionNames.RELATED,
- initialQuery
- ) }
- onSetAttributes={ onSetAttributes }
- >
- { ( attributes, setAttributes ) => (
- <LinkedProductControl
- query={ attributes.query }
- setAttributes={ setAttributes }
- location={ { type: LocationType.Site } }
- usesReference={ [ 'product' ] }
- />
- ) }
- </StatefulAttributes>
- );
-
- await act( async () => {
- await Promise.resolve();
- } );
- await click(
- user,
- await screen.findByRole( 'button', { name: /Beanie/ } )
- );
- await click(
- user,
- screen.getByRole( 'button', {
- name: 'Choose product reference',
- } )
- );
-
- expect( onSetAttributes ).toHaveBeenLastCalledWith( {
- query: { ...initialQuery, productReference: 73 },
- } );
- } );
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/utils/contract-setup.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/utils/contract-setup.ts
deleted file mode 100644
index 5d52db86c85..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/utils/contract-setup.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * External dependencies
- */
-import {
- getBlockVariations,
- registerBlockVariation,
- unregisterBlockVariation,
-} from '@wordpress/blocks';
-import { store as coreStore } from '@wordpress/core-data';
-import { select } from '@wordpress/data';
-import { removeFilter } from '@wordpress/hooks';
-
-/**
- * Internal dependencies
- */
-import {
- registerCollections,
- registerEmailCollections,
-} from '../../collections';
-import { PRODUCT_COLLECTION_BLOCK_NAME } from '../../constants';
-
-type Taxonomy = {
- name: string;
- slug: string;
- visibility: { publicly_queryable: boolean };
-};
-
-const getCollectionNames = () =>
- ( getBlockVariations( PRODUCT_COLLECTION_BLOCK_NAME ) ?? [] ).map(
- ( variation ) => variation.name
- );
-
-/**
- * Registers the chooser and email collections the way the editor does.
- *
- * Returns a function that unregisters the collections it added and restores `window.wp`.
- */
-export const registerCoreCollections = (): ( () => void ) => {
- const originalWpDescriptor = Object.getOwnPropertyDescriptor(
- window,
- 'wp'
- );
- const existingNames = new Set( getCollectionNames() );
-
- // registerProductCollection() calls the global wp.blocks.registerBlockVariation.
- Object.defineProperty( window, 'wp', {
- configurable: true,
- writable: true,
- value: {
- ...window.wp,
- blocks: {
- ...window.wp?.blocks,
- registerBlockVariation,
- },
- },
- } );
-
- registerCollections();
- registerEmailCollections();
-
- const registeredNames = getCollectionNames().filter(
- ( name ) => ! existingNames.has( name )
- );
-
- return () => {
- for ( const name of registeredNames ) {
- unregisterBlockVariation( PRODUCT_COLLECTION_BLOCK_NAME, name );
- removeFilter( 'editor.BlockEdit', name );
- }
-
- if ( originalWpDescriptor ) {
- Object.defineProperty( window, 'wp', originalWpDescriptor );
- } else {
- Reflect.deleteProperty( window, 'wp' );
- }
- };
-};
-
-/**
- * Makes the core data store report the product category, tag and brand taxonomies.
- */
-export const mockProductTaxonomies = () =>
- jest
- .spyOn(
- select( coreStore ) as unknown as {
- getTaxonomies: () => Taxonomy[];
- },
- 'getTaxonomies'
- )
- .mockReturnValue( [
- {
- name: 'product categories',
- slug: 'product_cat',
- visibility: { publicly_queryable: true },
- },
- {
- name: 'product tags',
- slug: 'product_tag',
- visibility: { publicly_queryable: true },
- },
- {
- name: 'product brands',
- slug: 'product_brand',
- visibility: { publicly_queryable: true },
- },
- ] );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collection-pickers.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collection-pickers.block_theme.spec.ts
index 69509cc1ccb..dec7e2c0127 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collection-pickers.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collection-pickers.block_theme.spec.ts
@@ -6,7 +6,10 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
-import ProductCollectionPage, { SELECTORS } from './product-collection.page';
+import ProductCollectionPage, {
+ Collections,
+ SELECTORS,
+} from './product-collection.page';
const test = base.extend< { pageObject: ProductCollectionPage } >( {
pageObject: async ( { page, admin, editor }, use ) => {
@@ -19,13 +22,45 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
},
} );
+/**
+ * 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,
+ },
+ {
+ slug: 'productsByBrand',
+ name: 'Products by Brand',
+ termName: 'brands',
+ termLabel: 'WooCommerce',
+ expectedProductCount: 3,
+ },
+];
+
test.describe( 'Product Collection: Collection Pickers', () => {
test.describe( 'Hand-Picked Products', () => {
- test( 'Products are displayed on frontend', async ( {
+ test( 'Can select multiple products and Done button becomes enabled', async ( {
pageObject,
admin,
editor,
- page,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
@@ -37,119 +72,171 @@ test.describe( 'Product Collection: Collection Pickers', () => {
const doneButton = productPicker.locator(
SELECTORS.pickerDoneButton
);
+
+ // Initially disabled
await expect( doneButton ).toBeDisabled();
+ // Select first product
await productPicker
.getByRole( 'checkbox', { name: 'Album (woo-album)' } )
.click();
+
+ // Done button should now be enabled
await expect( doneButton ).toBeEnabled();
- const beanieCheckbox = productPicker.getByRole( 'checkbox', {
- name: 'Beanie (woo-beanie)',
- } );
- await beanieCheckbox.click();
- await expect( beanieCheckbox ).toBeChecked();
-
- const selectedQuery = await pageObject.getProductCollectionQuery();
- const selectedIds = selectedQuery.woocommerceHandPickedProducts;
- expect( selectedIds ).toHaveLength( 2 );
- expect(
- selectedIds.every( ( id: string | number ) =>
- /^[1-9]\d*$/.test( String( id ) )
- )
- ).toBe( true );
+ // Select second product
+ await productPicker
+ .getByRole( 'checkbox', { name: '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.productTitles ).toHaveText( [
- 'Album',
- 'Beanie',
- ] );
+ 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
+ .getByRole( 'checkbox', { name: '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();
- await expect(
- editor.canvas.locator( SELECTORS.productPicker )
- ).toBeHidden();
- expect(
- ( await pageObject.getProductCollectionQuery() )
- .woocommerceHandPickedProducts
- ).toEqual( selectedIds );
+ // 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.productTitles ).toHaveText( [
- 'Album',
- 'Beanie',
- ] );
- await pageObject.publishAndGoToFrontend();
- await expect( pageObject.productTitles ).toHaveText( [
- 'Album',
- 'Beanie',
- ] );
+ await expect( pageObject.products ).toHaveCount( 1 );
} );
- } );
- test.describe( 'Products by Category', () => {
- test( 'Products from selected categories are displayed on frontend', async ( {
+ test( 'Products are displayed on frontend', async ( {
pageObject,
admin,
editor,
} ) => {
await admin.createNewPost();
await pageObject.insertProductCollection();
- await pageObject.chooseCollectionInPost( 'productsByCategory' );
+ await pageObject.chooseCollectionInPost( 'handPicked' );
- const taxonomyPicker = editor.canvas.locator(
- SELECTORS.taxonomyPicker
- );
- const doneButton = taxonomyPicker.locator(
- SELECTORS.pickerDoneButton
+ // Select products and click Done
+ const productPicker = editor.canvas.locator(
+ SELECTORS.productPicker
);
- await expect( doneButton ).toBeDisabled();
- await taxonomyPicker
- .getByRole( 'checkbox', { name: 'Accessories' } )
+ await productPicker
+ .getByRole( 'checkbox', { name: 'Album (woo-album)' } )
.click();
- await expect( doneButton ).toBeEnabled();
-
- const taxQuery = ( await pageObject.getProductCollectionQuery() )
- .taxQuery;
- expect( Object.keys( taxQuery ) ).toEqual( [ 'product_cat' ] );
- expect( taxQuery.product_cat ).toHaveLength( 1 );
- expect( String( taxQuery.product_cat[ 0 ] ) ).toMatch(
- /^[1-9]\d*$/
- );
+ await productPicker
+ .getByRole( 'checkbox', { name: 'Beanie (woo-beanie)' } )
+ .click();
+ await productPicker.locator( SELECTORS.pickerDoneButton ).click();
- await doneButton.click();
- await expect( taxonomyPicker ).toBeHidden();
await pageObject.refreshLocators( 'editor' );
- await expect( pageObject.products ).toHaveCount( 5 );
await pageObject.publishAndGoToFrontend();
- await expect( pageObject.products ).toHaveCount( 5 );
- await expect
- .poll( async () =>
- (
- await pageObject.productTitles.allTextContents()
- ).toSorted()
- )
- .toEqual(
- [
- 'Beanie',
- 'Beanie with Logo',
- 'Belt',
- 'Cap',
- 'Protected: Sunglasses',
- ].toSorted()
- );
+ 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 to a non-picker collection displays products immediately', async ( {
+ test( 'Switching from Hand-Picked to Products by Category shows taxonomy picker', async ( {
pageObject,
admin,
editor,
@@ -158,6 +245,7 @@ test.describe( 'Product Collection: Collection Pickers', () => {
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost( 'handPicked' );
+ // Select a product and click Done
const productPicker = editor.canvas.locator(
SELECTORS.productPicker
);
@@ -165,32 +253,50 @@ test.describe( 'Product Collection: Collection Pickers', () => {
.getByRole( 'checkbox', { name: 'Album (woo-album)' } )
.click();
await productPicker.locator( SELECTORS.pickerDoneButton ).click();
- await expect( productPicker ).toBeHidden();
+ // 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( productPicker ).toBeHidden();
await expect( taxonomyPicker ).toBeVisible();
- await taxonomyPicker
- .getByRole( 'checkbox', { name: 'Accessories' } )
+ } );
+
+ 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
+ .getByRole( 'checkbox', { name: 'Album (woo-album)' } )
.click();
- await taxonomyPicker.locator( SELECTORS.pickerDoneButton ).click();
- await expect( taxonomyPicker ).toBeHidden();
+ 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.productTitles ).toHaveText( [
- 'Cap',
- 'Hoodie with Zipper',
- 'Sunglasses',
- 'V-Neck T-Shirt',
- ] );
+ await expect( pageObject.products ).toHaveCount( 4 );
} );
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
index 3a6f5f9d935..d69486af313 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
@@ -20,9 +20,25 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
} );
test.describe( 'Product Collection: Collections', () => {
+ test( 'New Arrivals collection can be added and displays proper products', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'newArrivals' );
+
+ // New Arrivals are by default filtered to display products from last 7 days.
+ // Products in our test env have creation date set to much older, hence
+ // no products are expected to be displayed by default.
+ await expect( pageObject.products ).toHaveCount( 0 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 0 );
+ } );
+
test( 'Top Rated Products collection can be added and displays proper products', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock( 'topRated' );
const topRatedProducts = [
'V-Neck T-Shirt',
'Hoodie',
@@ -31,13 +47,97 @@ test.describe( 'Product Collection: Collections', () => {
'Beanie',
];
- await pageObject.createNewPostAndInsertBlock( 'topRated' );
await expect( pageObject.productTitles ).toHaveText( topRatedProducts );
await pageObject.publishAndGoToFrontend();
+
await expect( pageObject.productTitles ).toHaveText( topRatedProducts );
} );
+ test( 'Best Sellers collection can be added and displays proper products', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
+ const bestSellersProducts = [
+ 'Album',
+ 'Hoodie',
+ 'Single',
+ 'Hoodie with Logo',
+ 'T-Shirt with Logo',
+ ];
+
+ await expect( pageObject.productTitles ).toHaveText(
+ bestSellersProducts
+ );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.productTitles ).toHaveText(
+ bestSellersProducts
+ );
+ } );
+
+ test( 'On Sale Products collection can be added and displays proper products', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'onSale' );
+
+ const onSaleProducts = [
+ 'Beanie',
+ 'Beanie with Logo',
+ 'Belt',
+ 'Cap',
+ 'Hoodie',
+ ];
+
+ await expect( pageObject.products ).toHaveCount( 5 );
+ await expect( pageObject.productTitles ).toHaveText( onSaleProducts );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 5 );
+ } );
+
+ test( 'Featured Products collection can be added and displays proper products', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'featured' );
+
+ const featuredProducts = [
+ 'Cap',
+ 'Hoodie with Zipper',
+ 'Sunglasses',
+ 'V-Neck T-Shirt',
+ ];
+
+ await expect( pageObject.products ).toHaveCount( 4 );
+ await expect( pageObject.productTitles ).toHaveText( featuredProducts );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 4 );
+ } );
+
+ test( 'Product Catalog collection can be added in post and syncs query with template', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
+
+ const queryTypeLocator = pageObject
+ .locateSidebarSettings()
+ .getByLabel( SELECTORS.usePageContextControl );
+
+ await expect( queryTypeLocator.getByLabel( 'Default' ) ).toBeChecked();
+ await expect(
+ queryTypeLocator.getByLabel( 'Custom' )
+ ).not.toBeChecked();
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+ } );
+
test( 'Product Catalog Collection can be added in product archive and syncs query with template', async ( {
pageObject,
editor,
@@ -48,36 +148,64 @@ test.describe( 'Product Collection: Collections', () => {
postType: 'wp_template',
canvas: 'edit',
} );
+
await editor.setContent( '' );
+
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await editor.openDocumentSettingsSidebar();
- const queryType = pageObject
+ const queryTypeLocator = pageObject
.locateSidebarSettings()
.getByLabel( SELECTORS.usePageContextControl );
- await expect( queryType.getByLabel( 'Default' ) ).toBeChecked();
- await expect( queryType.getByLabel( 'Custom' ) ).not.toBeChecked();
- await pageObject.refreshLocators( 'editor' );
- await expect( pageObject.products.first() ).toBeVisible();
+ await expect( queryTypeLocator.getByLabel( 'Default' ) ).toBeChecked();
await expect(
- pageObject.productTitles.getByRole( 'link' ).first()
- ).toBeVisible();
- const editorProductPaths = await pageObject.getProductPermalinkPaths();
- expect( editorProductPaths ).not.toHaveLength( 0 );
+ queryTypeLocator.getByLabel( 'Custom' )
+ ).not.toBeChecked();
+ } );
+
+ test.describe( 'Have hidden implementation in UI', () => {
+ test( 'New Arrivals', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'newArrivals' );
+ const input = await pageObject.getOrderByElement();
- await editor.saveSiteEditorEntities( {
- isOnlyCurrentEntityDirty: true,
+ await expect( input ).toBeHidden();
+ } );
+
+ test( 'Top Rated Products', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'topRated' );
+ const input = await pageObject.getOrderByElement();
+
+ await expect( input ).toBeHidden();
+ } );
+
+ test( 'Best Sellers', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
+ const input = await pageObject.getOrderByElement();
+
+ await expect( input ).toBeHidden();
+ } );
+
+ test( 'On Sale Products', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'onSale' );
+ const sidebarSettings = pageObject.locateSidebarSettings();
+ const input = sidebarSettings.getByLabel(
+ SELECTORS.onSaleControlLabel
+ );
+
+ await expect( input ).toBeHidden();
+ } );
+
+ test( 'Featured Products', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock( 'featured' );
+ const sidebarSettings = pageObject.locateSidebarSettings();
+ const input = sidebarSettings.getByLabel(
+ SELECTORS.featuredControlLabel
+ );
+
+ await expect( input ).toBeHidden();
} );
- await pageObject.goToProductCatalogFrontend();
- await expect( pageObject.products.first() ).toBeVisible();
- await expect(
- pageObject.productTitles.getByRole( 'link' ).first()
- ).toBeVisible();
- await expect
- .poll( async () => pageObject.getProductPermalinkPaths() )
- .toEqual( editorProductPaths );
} );
test.describe( 'Related Products collection', () => {
@@ -85,19 +213,24 @@ test.describe( 'Product Collection: Collections', () => {
pageObject,
editor,
} ) => {
+ // "Related by" control shouldn't be visible for other collections
await pageObject.createNewPostAndInsertBlock( 'bestSellers' );
const sidebarSettings = pageObject.locateSidebarSettings();
+
const relatedByControl = sidebarSettings.locator(
'.wc-block-editor-product-collection-inspector-controls__relate-by'
);
await expect( relatedByControl ).toBeHidden();
+ // Change collection type to "Related Products"
+ // And verify that "Related by" control is visible
await pageObject.changeCollectionUsingToolbar( 'relatedProducts' );
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
await expect( relatedByControl ).toBeVisible();
+ // Verify that both checkboxes (Categories and Tags) are checked by default
const categoriesCheckbox =
sidebarSettings.getByLabel( 'Categories' );
const tagsCheckbox = sidebarSettings.getByLabel( 'Tags' );
@@ -105,6 +238,7 @@ test.describe( 'Product Collection: Collections', () => {
await expect( tagsCheckbox ).toBeChecked();
await expect( pageObject.productTitles ).toHaveText( [ 'Single' ] );
+ // Uncheck "Categories" checkbox
await categoriesCheckbox.uncheck();
await expect(
editor.canvas.getByText(
@@ -112,8 +246,8 @@ test.describe( 'Product Collection: Collections', () => {
)
).toBeVisible();
+ // Verify on frontend
await categoriesCheckbox.check();
- await expect( pageObject.productTitles ).toHaveText( [ 'Single' ] );
await pageObject.publishAndGoToFrontend();
await expect( pageObject.productTitles ).toHaveText( [ 'Single' ] );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
index e17194b2c35..a192d7e60f9 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
@@ -20,240 +20,395 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
} );
test.describe( 'Product Collection: Inspector Controls', () => {
- test( 'persists display controls and renders their frontend output', async ( {
+ test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject,
- page,
- editor,
} ) => {
await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.setNumberOfColumns( 2 );
+ await expect( pageObject.productTemplate ).toHaveClass( /columns-2/ );
+
await pageObject.setNumberOfColumns( 4 );
- await expect( pageObject.productTemplate ).toContainClass(
- 'columns-4'
- );
+ await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ );
- await page.getByRole( 'button', { name: 'Settings options' } ).click();
- await page.getByRole( 'menuitemcheckbox', { name: 'Offset' } ).click();
- await page
- .getByRole( 'menuitemcheckbox', { name: 'Max pages to show' } )
- .click();
- await page.getByRole( 'button', { name: 'Settings options' } ).click();
+ await pageObject.publishAndGoToFrontend();
- const settingsPanel = page.locator(
- '.wc-block-editor-product-collection__settings_panel'
- );
- await settingsPanel
- .getByRole( 'spinbutton', { name: 'Products per page' } )
- .fill( '3' );
- await pageObject.refreshLocators( 'editor' );
- await expect( pageObject.products ).toHaveCount( 3 );
+ await expect( pageObject.productTemplate ).toHaveClass( /columns-4/ );
+ } );
- await settingsPanel
- .getByRole( 'spinbutton', { name: 'Offset' } )
- .fill( '1' );
- await expect(
- editor.canvas.locator( '.wc-block-product-template__spinner' )
- ).toBeHidden();
- await pageObject.refreshLocators( 'editor' );
- await expect( pageObject.productTitles.first() ).toHaveText( 'Beanie' );
+ test( 'Order By - sort products by title in descending order correctly', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
- await settingsPanel
- .getByRole( 'spinbutton', { name: 'Max pages to show' } )
- .fill( '2' );
+ const sortedTitles = [
+ 'WordPress Pennant',
+ 'V-Neck T-Shirt',
+ 'T-Shirt with Logo',
+ 'T-Shirt',
+ /Sunglasses/, // In the frontend it's "Protected: Sunglasses"
+ 'Single',
+ 'Polo',
+ 'Long Sleeve Tee',
+ 'Logo Collection',
+ ];
+
+ await pageObject.setOrderBy( 'title/desc' );
+ await expect( pageObject.productTitles ).toHaveText( sortedTitles );
await pageObject.publishAndGoToFrontend();
- await expect( pageObject.productTemplate ).toContainClass(
- 'columns-4'
- );
- await expect( pageObject.products ).toHaveCount( 3 );
- await expect( pageObject.productTitles.first() ).toHaveText( 'Beanie' );
- await expect(
- page
- .locator( SELECTORS.pagination.onFrontend )
- .locator( '.page-numbers' )
- ).toHaveCount( 2 );
+ await expect( pageObject.productTitles ).toHaveText( sortedTitles );
} );
- test( 'inherits the Product Catalog query and preserves custom criteria', async ( {
+ test( 'Products can be filtered based on "on sale" status', async ( {
pageObject,
- editor,
} ) => {
- await pageObject.goToEditorTemplate();
- await pageObject.focusProductCollection();
- await editor.openDocumentSettingsSidebar();
+ await pageObject.createNewPostAndInsertBlock();
- const sidebarSettings = pageObject.locateSidebarSettings();
- const queryType = sidebarSettings.getByLabel(
- SELECTORS.usePageContextControl
- );
- const defaultQueryType = queryType.getByLabel( 'Default' );
- const customQueryType = queryType.getByLabel( 'Custom' );
- const onSaleControl = sidebarSettings.getByLabel(
- SELECTORS.onSaleControlLabel
- );
+ let allProducts = pageObject.products;
+ let saleProducts = pageObject.products.filter( {
+ hasText: 'Product on sale',
+ } );
+
+ await expect( allProducts ).toHaveCount( 9 );
+ await expect( saleProducts ).toHaveCount( 6 );
- await expect( defaultQueryType ).toBeChecked();
- await expect(
- pageObject.productTitles.getByRole( 'link' ).first()
- ).toBeVisible();
- const inheritedProductPaths =
- await pageObject.getProductPermalinkPaths();
- expect( inheritedProductPaths ).not.toHaveLength( 0 );
- await expect( onSaleControl ).toBeHidden();
-
- await customQueryType.click();
- await expect( onSaleControl ).toBeVisible();
await pageObject.setShowOnlyProductsOnSale( {
onSale: true,
- isLocatorsRefreshNeeded: false,
} );
- await expect
- .poll( async () => {
- const productPaths =
- await pageObject.getProductPermalinkPaths();
- return (
- productPaths.length > 0 &&
- JSON.stringify( productPaths ) !==
- JSON.stringify( inheritedProductPaths )
- );
- } )
- .toBeTruthy();
- const customProductPaths = await pageObject.getProductPermalinkPaths();
- expect( customProductPaths ).not.toHaveLength( 0 );
- expect( customProductPaths ).not.toEqual( inheritedProductPaths );
-
- await defaultQueryType.click();
- await expect( onSaleControl ).toBeHidden();
- await expect
- .poll( async () => pageObject.getProductPermalinkPaths() )
- .toEqual( inheritedProductPaths );
- await customQueryType.click();
- await expect( onSaleControl ).toBeChecked();
- await expect
- .poll( async () => pageObject.getProductPermalinkPaths() )
- .toEqual( customProductPaths );
-
- await editor.saveSiteEditorEntities( {
- isOnlyCurrentEntityDirty: true,
+
+ await expect( allProducts ).toHaveCount( 6 );
+ await expect( saleProducts ).toHaveCount( 6 );
+
+ await pageObject.publishAndGoToFrontend();
+ await pageObject.refreshLocators( 'frontend' );
+ allProducts = pageObject.products;
+ saleProducts = pageObject.products.filter( {
+ hasText: 'Product on sale',
} );
- await pageObject.goToProductCatalogFrontend();
- await expect(
- pageObject.productTitles.getByRole( 'link' ).first()
- ).toBeVisible();
- await expect
- .poll( async () => pageObject.getProductPermalinkPaths() )
- .toEqual( customProductPaths );
+
+ await expect( allProducts ).toHaveCount( 6 );
+ await expect( saleProducts ).toHaveCount( 6 );
} );
- test( 'correctly combines editor and front-end filters', async ( {
+ test( 'Products can be filtered based on selection in handpicked products option', async ( {
pageObject,
- editor,
- page,
- requestUtils,
} ) => {
- await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await pageObject.createNewPostAndInsertBlock();
- await pageObject.addFilter( 'Show product categories' );
- await pageObject.checkTaxonomyTerm( 'categories', 'Music' );
+ await pageObject.addFilter( 'Show Hand-picked' );
- const productCollectionBlock = await editor.getBlockByName(
- 'woocommerce/product-collection'
- );
- const productCollectionClientId =
- ( await productCollectionBlock
- .last()
- .getAttribute( 'data-block' ) ) ?? '';
- await editor.insertBlock(
- { name: 'woocommerce/product-filters' },
- { clientId: productCollectionClientId }
- );
+ const filterName = 'Hand-picked';
+ await pageObject.setFilterComboboxValue( filterName, [ 'Album' ] );
+ await expect( pageObject.products ).toHaveCount( 1 );
+ const productNames = [ 'Album', 'Cap' ];
+ await pageObject.setFilterComboboxValue( filterName, productNames );
await expect( pageObject.products ).toHaveCount( 2 );
- const postId = await editor.publishPost();
- await page.goto( `/?p=${ postId }` );
- await pageObject.refreshLocators( 'frontend' );
+ await expect( pageObject.productTitles ).toHaveText( productNames );
+
+ await pageObject.publishAndGoToFrontend();
await expect( pageObject.products ).toHaveCount( 2 );
+ await expect( pageObject.productTitles ).toHaveText( productNames );
+ } );
- await page
- .getByRole( 'textbox', {
- name: 'Filter products by maximum price',
- } )
- .dblclick();
- await page.keyboard.type( '5' );
- await page.keyboard.press( 'Tab' );
- await expect( pageObject.products ).toHaveCount( 1 );
+ test( 'Products can be filtered based on keyword.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.addFilter( 'Keyword' );
+
+ await pageObject.setKeyword( 'Album' );
+ await expect( pageObject.productTitles ).toHaveText( [ 'Album' ] );
+
+ await pageObject.setKeyword( 'Cap' );
+ await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
+
+ await pageObject.publishAndGoToFrontend();
+ await expect( pageObject.productTitles ).toHaveText( [ 'Cap' ] );
} );
- test( 'scopes Query Type and Product Filters to the first eligible collection', async ( {
+ test( 'Products can be filtered based on category.', async ( {
pageObject,
- editor,
- page,
- requestUtils,
} ) => {
- await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await pageObject.createNewPostAndInsertBlock();
- const productCollection = editor.canvas.getByLabel(
- 'Block: Product Collection',
- { exact: true }
- );
- const queryType = pageObject
- .locateSidebarSettings()
- .getByLabel( SELECTORS.usePageContextControl );
- const defaultQueryType = queryType.getByLabel( 'Default' );
- const customQueryType = queryType.getByLabel( 'Custom' );
-
- await editor.selectBlocks( productCollection.first() );
- await expect( defaultQueryType ).toBeChecked();
-
- await pageObject.insertProductCollection();
- await pageObject.chooseCollectionInPost( 'productCatalog' );
- await editor.selectBlocks( productCollection.last() );
- await expect( customQueryType ).toBeChecked();
- await expect( pageObject.products ).toHaveCount( 18 );
-
- const productCollectionBlock = await editor.getBlockByName(
- 'woocommerce/product-collection'
- );
- const secondCollectionClientId =
- ( await productCollectionBlock
- .last()
- .getAttribute( 'data-block' ) ) ?? '';
- await editor.insertBlock(
- { name: 'woocommerce/product-filters' },
- { clientId: secondCollectionClientId }
+ await pageObject.addFilter( 'Show product categories' );
+ await pageObject.checkTaxonomyTerm( 'categories', 'Clothing' );
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ // Switch to Accessories
+ await pageObject.uncheckTaxonomyTerm( 'categories', 'Clothing' );
+ await pageObject.checkTaxonomyTerm( 'categories', 'Accessories' );
+ const accessoriesProductNames = [
+ 'Beanie',
+ 'Beanie with Logo',
+ 'Belt',
+ 'Cap',
+ 'Sunglasses',
+ ];
+ await expect( pageObject.productTitles ).toHaveText(
+ accessoriesProductNames
);
- const postId = await editor.publishPost();
- await page.goto( `/?p=${ postId }` );
- const collections = page.locator(
- '.wp-block-woocommerce-product-collection'
+ await pageObject.publishAndGoToFrontend();
+
+ const frontendAccessoriesProductNames = [
+ 'Beanie',
+ 'Beanie with Logo',
+ 'Belt',
+ 'Cap',
+ 'Protected: Sunglasses',
+ ];
+ await expect( pageObject.productTitles ).toHaveText(
+ frontendAccessoriesProductNames
);
+ } );
+
+ test( 'Products can be filtered based on tags.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.addFilter( 'Show product tags' );
+ await pageObject.checkTaxonomyTerm( 'tags', 'Recommended' );
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'Beanie',
+ 'Hoodie',
+ ] );
+
+ await pageObject.publishAndGoToFrontend();
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'Beanie',
+ 'Hoodie',
+ ] );
+ } );
+
+ test( 'Products can be filtered based on brands.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.addFilter( 'Show Product Brands' );
+ await pageObject.checkTaxonomyTerm( 'brands', 'WooCommerce' );
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'Album',
+ 'Beanie',
+ 'Hoodie',
+ ] );
+
+ await pageObject.publishAndGoToFrontend();
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'Album',
+ 'Beanie',
+ 'Hoodie',
+ ] );
+ } );
+
+ test( 'Products can be filtered based on product attributes like color, size etc.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.addFilter( 'Show Product Attributes' );
+ await pageObject.setProductAttribute( 'Color', 'Green' );
+
+ await expect( pageObject.products ).toHaveCount( 3 );
+
+ await pageObject.setProductAttribute( 'Size', 'Large' );
+
+ await expect( pageObject.products ).toHaveCount( 1 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 1 );
+ } );
+
+ test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await pageObject.setFilterComboboxValue( 'Stock status', [
+ 'Out of stock',
+ ] );
+
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'T-Shirt with Logo',
+ ] );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.productTitles ).toHaveText( [
+ 'T-Shirt with Logo',
+ ] );
+ } );
+
+ test( 'Products can be filtered based on featured status.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Featured' );
+ await pageObject.setShowOnlyFeaturedProducts( {
+ featured: true,
+ } );
+
+ // In test data we have only 4 featured products.
+ await expect( pageObject.products ).toHaveCount( 4 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 4 );
+ } );
+
+ test( 'Products can be filtered based on created date.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Created' );
+ await pageObject.setCreatedFilter( {
+ operator: 'within',
+ range: 'last3months',
+ } );
+
+ // Products are created with the fixed publish date back in 2019
+ // so there's no products published in last 3 months.
+ await expect( pageObject.products ).toHaveCount( 0 );
+
+ await pageObject.setCreatedFilter( {
+ operator: 'before',
+ range: 'last3months',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+ } );
+
+ test( 'Products can be filtered based on price range.', async ( {
+ pageObject,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Price Range' );
+ await pageObject.setPriceRange( {
+ min: '25',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 7 );
+
+ await pageObject.setPriceRange( {
+ min: '15.28',
+ max: '17.21',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ await pageObject.setPriceRange( {
+ max: '17.29',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 5 );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 5 );
+ } );
+
+ // See https://github.com/woocommerce/woocommerce/pull/49917
+ test( 'Price range is inclusive in both editor and frontend.', async ( {
+ page,
+ pageObject,
+ editor,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Price Range' );
+ await pageObject.setPriceRange( {
+ min: '45',
+ max: '55',
+ } );
+
+ // Wait for the products to be filtered.
+ await expect( pageObject.products ).not.toHaveCount( 9 );
+
await expect(
- collections.first().locator( SELECTORS.product )
- ).toHaveCount( 9 );
+ pageObject.products.filter( { hasText: '$45.00' } )
+ ).not.toHaveCount( 0 );
await expect(
- collections.last().locator( SELECTORS.product )
- ).toHaveCount( 9 );
+ pageObject.products.filter( { hasText: '$55.00' } )
+ ).not.toHaveCount( 0 );
+
+ // Reset the price range.
+ await pageObject.setPriceRange( {
+ min: '0',
+ max: '0',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await editor.insertBlock( {
+ name: 'woocommerce/filter-wrapper',
+ attributes: { filterType: 'price-filter' },
+ } );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
await page
.getByRole( 'textbox', {
- name: 'Filter products by maximum price',
+ name: 'Filter products by minimum',
} )
.dblclick();
- await page.keyboard.type( '10' );
+ await page.keyboard.type( '45' );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum',
+ } )
+ .dblclick();
+ await page.keyboard.type( '55' );
+
await page.keyboard.press( 'Tab' );
+ // Wait for the products to be filtered.
+ await expect( pageObject.products ).not.toHaveCount( 9 );
+
await expect(
- collections.first().locator( SELECTORS.product )
- ).toHaveCount( 1 );
+ pageObject.products.filter( { hasText: '$45.00' } )
+ ).not.toHaveCount( 0 );
await expect(
- collections.last().locator( SELECTORS.product )
- ).toHaveCount( 9 );
+ pageObject.products.filter( { hasText: '$55.00' } )
+ ).not.toHaveCount( 0 );
} );
- test.describe( '"Query Type" control fallback', () => {
+ test.describe( '"Query Type" control', () => {
+ test( 'should be visible on posts', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect(
+ pageObject
+ .locateSidebarSettings()
+ .getByLabel( SELECTORS.usePageContextControl )
+ ).toBeVisible();
+ } );
+
[
+ `${ BLOCK_THEME_SLUG }//archive-product`,
`${ BLOCK_THEME_SLUG }//taxonomy-product_attribute`,
`${ BLOCK_THEME_SLUG }//product-search-results`,
].forEach( ( slug ) => {
@@ -336,6 +491,57 @@ test.describe( 'Product Collection: Inspector Controls', () => {
} );
} );
+ test( 'should work as expected in Product Catalog template', async ( {
+ pageObject,
+ editor,
+ } ) => {
+ await pageObject.goToEditorTemplate();
+ await pageObject.focusProductCollection();
+ await editor.openDocumentSettingsSidebar();
+
+ const sidebarSettings = pageObject.locateSidebarSettings();
+ const queryTypeLocator = sidebarSettings.getByLabel(
+ SELECTORS.usePageContextControl
+ );
+
+ const defaultQueryType = queryTypeLocator.getByLabel( 'Default' );
+ const customQueryType = queryTypeLocator.getByLabel( 'Custom' );
+
+ // Inherit query from template should be visible & enabled by default
+ await expect( defaultQueryType ).toBeChecked();
+
+ // "On sale control" should be hidden when inherit query from template is enabled
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeHidden();
+
+ // "On sale control" should be visible when inherit query from template is disabled
+ await customQueryType.click();
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeVisible();
+
+ // "On sale control" should retain its state when inherit query from template is enabled again
+ await pageObject.setShowOnlyProductsOnSale( {
+ onSale: true,
+ isLocatorsRefreshNeeded: false,
+ } );
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeChecked();
+ await defaultQueryType.click();
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeHidden();
+ await customQueryType.click();
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeVisible();
+ await expect(
+ sidebarSettings.getByLabel( SELECTORS.onSaleControlLabel )
+ ).toBeChecked();
+ } );
+
test( 'is enabled by default unless already enabled elsewhere', async ( {
pageObject,
editor,
@@ -384,5 +590,175 @@ test.describe( 'Product Collection: Inspector Controls', () => {
await expect( defaultQueryType ).toBeChecked();
await expect( customQueryType ).not.toBeChecked();
} );
+
+ test( 'allows filtering in non-archive context', async ( {
+ pageObject,
+ editor,
+ page,
+ requestUtils,
+ } ) => {
+ await requestUtils.setFeatureFlag( 'experimental-blocks', true );
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'productCatalog' );
+
+ await expect( pageObject.products ).toHaveCount( 18 );
+
+ const productCollectionBlock = await editor.getBlockByName(
+ 'woocommerce/product-collection'
+ );
+ const productCollectionClientId =
+ ( await productCollectionBlock
+ .last()
+ .getAttribute( 'data-block' ) ) ?? '';
+ await editor.insertBlock(
+ { name: 'woocommerce/product-filters' },
+ { clientId: productCollectionClientId }
+ );
+
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+
+ const productCollection = page.locator(
+ '.wp-block-woocommerce-product-collection'
+ );
+
+ await expect(
+ productCollection.first().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
+ await expect(
+ productCollection.last().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum price',
+ } )
+ .dblclick();
+ await page.keyboard.type( '10' );
+ await page.keyboard.press( 'Tab' );
+
+ await expect(
+ productCollection.first().locator( SELECTORS.product )
+ ).toHaveCount( 1 );
+ await expect(
+ productCollection.last().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
+ } );
+
+ test( 'correctly combines editor and front-end filters', async ( {
+ pageObject,
+ editor,
+ page,
+ requestUtils,
+ } ) => {
+ await requestUtils.setFeatureFlag( 'experimental-blocks', true );
+
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Show product categories' );
+ await pageObject.checkTaxonomyTerm( 'categories', 'Music' );
+
+ const productCollectionBlock = await editor.getBlockByName(
+ 'woocommerce/product-collection'
+ );
+ const productCollectionClientId =
+ ( await productCollectionBlock
+ .last()
+ .getAttribute( 'data-block' ) ) ?? '';
+ await editor.insertBlock(
+ { name: 'woocommerce/product-filters' },
+ { clientId: productCollectionClientId }
+ );
+
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+ await pageObject.refreshLocators( 'frontend' );
+
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum price',
+ } )
+ .dblclick();
+ await page.keyboard.type( '5' );
+ await page.keyboard.press( 'Tab' );
+
+ await expect( pageObject.products ).toHaveCount( 1 );
+ } );
+ } );
+
+ test( 'Display -> Items per page, offset & max page to show', async ( {
+ pageObject,
+ page,
+ editor,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ // Add all display controls
+ await page.getByRole( 'button', { name: 'Settings options' } ).click();
+ await page.getByRole( 'menuitemcheckbox', { name: 'Offset' } ).click();
+ await page
+ .getByRole( 'menuitemcheckbox', { name: 'Max pages to show' } )
+ .click();
+ await page.getByRole( 'button', { name: 'Settings options' } ).click();
+
+ // Get the products per page input
+ const settingsPanel = page.locator(
+ '.wc-block-editor-product-collection__settings_panel'
+ );
+ const productsPerPageInput = settingsPanel.getByRole( 'spinbutton', {
+ name: 'Products per page',
+ } );
+
+ // Test setting products per page to 2
+ await productsPerPageInput.fill( '2' );
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ // Test setting products per page to 3
+ await productsPerPageInput.fill( '3' );
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.products ).toHaveCount( 3 );
+
+ // Set offset to 1 and verify it skips the first product
+ const offsetInput = settingsPanel.getByRole( 'spinbutton', {
+ name: 'Offset',
+ } );
+ await offsetInput.fill( '1' );
+ const loadingSpinner = editor.canvas.locator(
+ '.wc-block-product-template__spinner'
+ );
+ await expect( loadingSpinner ).toBeHidden();
+ await pageObject.refreshLocators( 'editor' );
+ await expect( pageObject.productTitles.first() ).toHaveText( 'Beanie' );
+
+ // Set max pages to show
+ const maxPagesInput = settingsPanel.getByRole( 'spinbutton', {
+ name: 'Max pages to show',
+ } );
+ await maxPagesInput.fill( '2' );
+
+ await pageObject.publishAndGoToFrontend();
+
+ // Frontend: Verify products are displayed correctly
+ await expect( pageObject.products ).toHaveCount( 3 );
+ await expect( pageObject.productTitles.first() ).toHaveText( 'Beanie' );
+
+ // Frontend: Verify pagination is limited to 2 pages
+ const paginationContainer = page.locator(
+ SELECTORS.pagination.onFrontend
+ );
+ const paginationNumbers =
+ paginationContainer.locator( '.page-numbers' );
+ await expect( paginationNumbers ).toHaveCount( 2 );
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-collection.page.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-collection.page.ts
index 759cd0eaaa0..b0e68158d70 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-collection.page.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-collection.page.ts
@@ -832,31 +832,6 @@ class ProductCollectionPage {
}
}
- async getProductPermalinkPaths(): Promise< string[] > {
- return this.productTitles
- .getByRole( 'link' )
- .evaluateAll( ( links ) =>
- links.map(
- ( link ) =>
- new URL( ( link as HTMLAnchorElement ).href ).pathname
- )
- );
- }
-
- async getProductCollectionQuery() {
- return this.page.evaluate( () => {
- const block = window.wp.data
- .select( 'core/block-editor' )
- .getBlocks()
- .find(
- ( candidate: { name: string } ) =>
- candidate.name === 'woocommerce/product-collection'
- );
-
- return block?.attributes.query ?? {};
- } );
- }
-
private async initializeLocatorsForEditor() {
this.productTemplate = this.editor.canvas.locator(
SELECTORS.productTemplate
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-picker.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-picker.block_theme.spec.ts
index 4b087aad7db..9b2bf5d9f59 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-picker.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/product-picker.block_theme.spec.ts
@@ -6,7 +6,10 @@ import { test as base, expect, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
-import ProductCollectionPage, { SELECTORS } from './product-collection.page';
+import ProductCollectionPage, {
+ Collections,
+ SELECTORS,
+} from './product-collection.page';
const test = base.extend< { pageObject: ProductCollectionPage } >( {
pageObject: async ( { page, admin, editor }, use ) => {
@@ -20,62 +23,168 @@ const test = base.extend< { pageObject: ProductCollectionPage } >( {
} );
test.describe( 'Product Collection: Product Picker', () => {
+ const CUSTOM_COLLECTIONS = [
+ {
+ id: 'myCustomCollectionWithProductContext',
+ name: 'My Custom Collection - Product Context',
+ label: 'Block: My Custom Collection - Product Context',
+ collection:
+ 'woocommerce/product-collection/my-custom-collection-product-context',
+ },
+ {
+ id: 'myCustomCollectionMultipleContexts',
+ name: 'My Custom Collection - Multiple Contexts',
+ label: 'Block: My Custom Collection - Multiple Contexts',
+ collection:
+ 'woocommerce/product-collection/my-custom-collection-multiple-contexts',
+ },
+ ];
+
+ // Activate plugin which registers custom product collections
test.beforeEach( async ( { requestUtils } ) => {
await requestUtils.activatePlugin(
'woocommerce-blocks-test-register-product-collection'
);
} );
- test( 'For collection "My Custom Collection - Product Context" - manually selected product reference should be available on Frontend in a post', async ( {
+ CUSTOM_COLLECTIONS.forEach( ( collection ) => {
+ test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
+ pageObject,
+ admin,
+ page,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost(
+ collection.id as Collections
+ );
+
+ // Verify that product picker is shown in Editor
+ const editorProductPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await expect( editorProductPicker ).toBeVisible();
+
+ // Once a product is selected, the product picker should be hidden
+ await pageObject.chooseProductInEditorProductPickerIfAvailable(
+ editor.canvas
+ );
+ await expect( editorProductPicker ).toBeHidden();
+
+ // On Frontend, verify that product reference is a number
+ await pageObject.publishAndGoToFrontend();
+ const collectionWithProductContext = page.locator(
+ `[data-collection="${ collection.collection }"]`
+ );
+ const queryAttribute = JSON.parse(
+ ( await collectionWithProductContext.getAttribute(
+ 'data-query'
+ ) ) || '{}'
+ );
+ expect( typeof queryAttribute?.productReference ).toBe( 'number' );
+ } );
+
+ test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
+ pageObject,
+ admin,
+ page,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost(
+ collection.id as Collections
+ );
+
+ // Verify that product picker is shown in Editor
+ const editorProductPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await expect( editorProductPicker ).toBeVisible();
+
+ // Once a product is selected, the product picker should be hidden
+ await pageObject.chooseProductInEditorProductPickerIfAvailable(
+ editor.canvas
+ );
+ await expect( editorProductPicker ).toBeHidden();
+
+ // Verify that Album is selected
+ await expect(
+ admin.page.locator( SELECTORS.linkedProductControl.button )
+ ).toContainText( 'Album' );
+
+ // Change product using inspector control to Beanie
+ await admin.page
+ .locator( SELECTORS.linkedProductControl.button )
+ .click();
+ await admin.page
+ .locator( SELECTORS.linkedProductControl.popoverContent )
+ .getByLabel( 'Beanie', { exact: true } )
+ .click();
+ await expect(
+ admin.page.locator( SELECTORS.linkedProductControl.button )
+ ).toContainText( 'Beanie' );
+
+ // On Frontend, verify that product reference is a number
+ await pageObject.publishAndGoToFrontend();
+ const collectionWithProductContext = page.locator(
+ `[data-collection="${ collection.collection }"]`
+ );
+ const queryAttribute = JSON.parse(
+ ( await collectionWithProductContext.getAttribute(
+ 'data-query'
+ ) ) || '{}'
+ );
+ expect( typeof queryAttribute?.productReference ).toBe( 'number' );
+ } );
+
+ test( `For collection "${ collection.name }" - "From current product" is chosen by default`, async ( {
+ pageObject,
+ admin,
+ editor,
+ } ) => {
+ await admin.visitSiteEditor( {
+ postId: `${ BLOCK_THEME_SLUG }//single-product`,
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+ await editor.canvas.locator( 'body' ).click();
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInTemplate(
+ collection.id as Collections
+ );
+
+ const productToShowControl = admin.page.getByText(
+ 'From the current product'
+ );
+ await expect( productToShowControl ).toBeChecked();
+ } );
+ } );
+
+ test( `For collection "Block: My Custom Collection - Cart Context" - "From products in the cart" is chosen by default in Cart Template`, async ( {
pageObject,
admin,
- page,
editor,
- requestUtils,
} ) => {
- const matchingProducts = await requestUtils.rest<
- Array< { id: number; name: string } >
- >( {
- method: 'GET',
- path: 'wc/v3/products',
- params: { search: 'Album' },
+ await admin.visitSiteEditor( {
+ postId: `${ BLOCK_THEME_SLUG }//page-cart`,
+ postType: 'wp_template',
+ canvas: 'edit',
} );
- const exactAlbums = matchingProducts.filter(
- ( product ) => product.name === 'Album'
- );
- expect( exactAlbums ).toHaveLength( 1 );
- const albumId = exactAlbums[ 0 ].id;
-
- await admin.createNewPost();
+ await editor.canvas.locator( 'body' ).click();
await pageObject.insertProductCollection();
- await pageObject.chooseCollectionInPost(
- 'myCustomCollectionWithProductContext'
- );
-
- const productPicker = editor.canvas.locator( SELECTORS.productPicker );
- await expect( productPicker ).toBeVisible();
- await pageObject.chooseProductInEditorProductPickerIfAvailable(
- editor.canvas,
- 'Album'
+ await pageObject.chooseCollectionInTemplate(
+ 'myCustomCollectionWithCartContext'
);
- await expect( productPicker ).toBeHidden();
- const selectedProductId = (
- await pageObject.getProductCollectionQuery()
- ).productReference;
- expect( selectedProductId ).toBe( albumId );
-
- await pageObject.publishAndGoToFrontend();
- const collection = page.locator(
- '[data-collection="woocommerce/product-collection/my-custom-collection-product-context"]'
- );
- const query = JSON.parse(
- ( await collection.getAttribute( 'data-query' ) ) || '{}'
+ const fromProductsInCartRadioButton = admin.page.getByText(
+ 'From products in the cart'
);
- expect( query.productReference ).toBe( selectedProductId );
+ await expect( fromProductsInCartRadioButton ).toBeChecked();
} );
- test( 'For collection "Block: My Custom Collection - Order Context" - "From products in the order" is chosen by default in Order Confirmation Template', async ( {
+ test( `For collection "Block: My Custom Collection - Order Context" - "From products in the order" is chosen by default in Order Confirmation Template`, async ( {
pageObject,
admin,
editor,
@@ -91,74 +200,49 @@ test.describe( 'Product Collection: Product Picker', () => {
'myCustomCollectionWithOrderContext'
);
- await expect(
- admin.page.getByText( 'From products in the order' )
- ).toBeChecked();
+ const fromProductsInOrderRadioButton = admin.page.getByText(
+ 'From products in the order'
+ );
+ await expect( fromProductsInOrderRadioButton ).toBeChecked();
} );
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
pageObject,
admin,
editor,
- requestUtils,
} ) => {
- const matchingProducts = await requestUtils.rest<
- Array< { id: number; name: string } >
- >( {
- method: 'GET',
- path: 'wc/v3/products',
- params: { search: 'Album' },
- } );
- const exactAlbums = matchingProducts.filter(
- ( product ) => product.name === 'Album'
- );
- expect( exactAlbums ).toHaveLength( 1 );
- const albumId = exactAlbums[ 0 ].id;
-
await admin.createNewPost();
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInPost(
'myCustomCollectionWithProductContext'
);
- const productPicker = editor.canvas.locator( SELECTORS.productPicker );
- await expect( productPicker ).toBeVisible();
+ // Verify that product picker is shown in Editor
+ const editorProductPicker = editor.canvas.locator(
+ SELECTORS.productPicker
+ );
+ await expect( editorProductPicker ).toBeVisible();
+
+ // Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
- editor.canvas,
- 'Album'
+ editor.canvas
);
- await expect( productPicker ).toBeHidden();
- const firstProductId = ( await pageObject.getProductCollectionQuery() )
- .productReference;
- expect( firstProductId ).toBe( albumId );
+ await expect( editorProductPicker ).toBeHidden();
+ // Change collection using Toolbar
await pageObject.changeCollectionUsingToolbar(
'myCustomCollectionMultipleContexts'
);
- await expect( productPicker ).toBeVisible();
- expect(
- ( await pageObject.getProductCollectionQuery() ).productReference
- ).toBeUndefined();
+ await expect( editorProductPicker ).toBeVisible();
+ // Once a product is selected, the product picker should be hidden
await pageObject.chooseProductInEditorProductPickerIfAvailable(
editor.canvas
);
- await expect( productPicker ).toBeHidden();
- const secondProductId = ( await pageObject.getProductCollectionQuery() )
- .productReference;
- expect( secondProductId ).toBe( albumId );
+ await expect( editorProductPicker ).toBeHidden();
+ // Product picker should be hidden for collections that don't need product
await pageObject.changeCollectionUsingToolbar( 'featured' );
- await expect( productPicker ).toBeHidden();
- expect(
- ( await pageObject.getProductCollectionQuery() ).productReference
- ).toBeUndefined();
- await pageObject.refreshLocators( 'editor' );
- await expect( pageObject.productTitles ).toHaveText( [
- 'Cap',
- 'Hoodie with Zipper',
- 'Sunglasses',
- 'V-Neck T-Shirt',
- ] );
+ await expect( editorProductPicker ).toBeHidden();
} );
} );