Commit b75fdeac453 for woocommerce
commit b75fdeac4535423a8ca6a84f98eeca720e4f5fca
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Tue Sep 15 23:26:45 2026 +0300
[tests] Reduce Product Reviews block E2E tests from 13 to 4 (#68601)
* test(blocks): Reduce Product Reviews block E2E tests from 13 to 4
The All Reviews, Reviews by Category, and Reviews by Product blocks
ran thirteen browser titles. Most of them sorted or offset a review
list, or published a block with no category or product selected. That
exercises shared sort, serialization, hydration, and request code
that runs below the browser.
Add Jest contracts for the shared review code: the Store API sort
arguments, the saved data attributes and their hydration, the
container's empty-selection guard, the Offset control's validation,
and the HOC's request arguments. Add a PHPUnit matrix for the Store
API reviews route's ordering and offset; the test file keeps trunk's
two password-protection tests from #68375. Keep four browser titles:
All Reviews inserts the block and sorts in the frontend, each
filtered block inserts and renders a review in the editor and the
frontend, and the category offset persists into the frontend.
Consolidates the mega-branch slices:
- Slice 025: test(blocks): Move Product Reviews matrices below E2E
- Slice 086: test(blocks): Consolidate All Reviews coverage
- test(blocks): assert review offset default
- test(blocks): model hydrated review defaults
- test(e2e): Expose review category state assertion
- test(e2e): Expose review product state assertion
Refs TESTOPS-234
Refs #68046
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Pair the category filter with rating order in the review matrix
No route test combined `category_id` with a sort, so the two were only
ever proven apart. The matrix product now belongs to a category of its
own and the request scopes by either `product_id` or `category_id`,
which lets a fifth row ask for rating order within a category.
The category holds only this product, so the expected order matches the
product-scoped rows; what the row adds is the pairing, not new ordering
coverage. Making the route ignore `category_id` fails the new row and
the existing category test, and leaves the other four rows green.
Refs #68601
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Match the Load more button by its exact accessible name
The sort reset test found the button with `/load more/i`. The repo's
regex rule allows a pattern only when no built-in alternative exists,
and `findByRole` accepts an exact string.
The Reviews block passes "Load more reviews" as the screen reader label,
and `Label` hides the visible "Load more" with `aria-hidden`, so that is
the accessible name. Matching it exactly also fails the test when the
label changes, which the substring pattern did not.
Refs #68601
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Cover the product offset request in the review contracts
The Reviews by Product offset E2E title was removed on this branch, and
the Jest flow that replaced it built only a Reviews by Category element.
A container change that dropped the offset for product blocks would
still pass every test; the PR description listed that path as knowingly
dropped coverage.
Run the positive-offset flow test for both a category and a product
element. The product case expects the hydrated `product_id` alongside
the configured offset. Making the container pass a zero offset whenever
a product is set fails only the new case.
Refs #68601
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/testops-234-product-reviews-blocks b/plugins/woocommerce/changelog/testops-234-product-reviews-blocks
new file mode 100644
index 00000000000..96faf1e4ef3
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-product-reviews-blocks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce Product Reviews block E2E tests from 13 to 4; Jest and a Store API PHPUnit matrix own sorting, offset, and empty-selection behavior.
+
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx b/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
index e931f3aa09e..25df086197c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
@@ -69,6 +69,8 @@ describe( 'withReviews Component', () => {
} );
afterEach( () => {
+ renderResult?.unmount();
+ renderResult = undefined;
mockUtils.getReviews.mockReset();
CapturedComponent.mockClear();
lastProps = undefined;
@@ -121,6 +123,72 @@ describe( 'withReviews Component', () => {
} );
} );
+ it.each( [
+ [
+ 'category array',
+ { categoryIds: [ 4, 8 ], productId: undefined },
+ {
+ category_id: '4,8',
+ offset: 0,
+ order: 'desc',
+ orderby: 'date_gmt',
+ per_page: 2,
+ },
+ ],
+ [
+ 'product',
+ { productId: 42 },
+ {
+ offset: 0,
+ order: 'desc',
+ orderby: 'date_gmt',
+ per_page: 2,
+ product_id: 42,
+ },
+ ],
+ ] )( 'requests reviews for a %s filter', async ( _name, props, args ) => {
+ mockUtils.getReviews.mockResolvedValue( {
+ reviews: [],
+ totalReviews: 0,
+ } );
+
+ await renderComponent( props );
+
+ expect( mockUtils.getReviews ).toHaveBeenCalledWith( args );
+ } );
+
+ it( 'combines configured offset with appended review count', async () => {
+ mockUtils.getReviews
+ .mockResolvedValueOnce( {
+ reviews: mockReviews.slice( 0, 2 ),
+ totalReviews: 10,
+ } )
+ .mockResolvedValueOnce( {
+ reviews: mockReviews.slice( 2 ),
+ totalReviews: 10,
+ } );
+ await renderComponent( { offset: 5, reviewsToDisplay: 2 } );
+
+ await settle( () =>
+ renderResult.rerender(
+ <TestComponent
+ attributes={ {} }
+ offset={ 5 }
+ order="desc"
+ orderby="date_gmt"
+ productId={ 1 }
+ reviewsToDisplay={ 4 }
+ />
+ )
+ );
+
+ expect( mockUtils.getReviews ).toHaveBeenNthCalledWith( 2, {
+ ...defaultArgs,
+ offset: 7,
+ per_page: 2,
+ } );
+ } );
+
it( 'replaces reviews when the offset changes as the display count increases', async () => {
const { getReviews } = mockUtils;
getReviews.mockResolvedValue( {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/test/review-contracts.test.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/test/review-contracts.test.tsx
new file mode 100644
index 00000000000..050ed872760
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/test/review-contracts.test.tsx
@@ -0,0 +1,476 @@
+/**
+ * External dependencies
+ */
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { getSetting } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import type { ReviewBlockAttributes } from '../attributes';
+import FrontendContainerBlock from '../frontend-container-block';
+import { getSharedReviewListControls } from '../edit-utils';
+import { getDataAttrs, getReviews, getSortArgs } from '../utils';
+
+type FrontendRegistration = {
+ getProps: (
+ element: HTMLElement,
+ index: number
+ ) => { attributes: Record< string, unknown > };
+};
+
+let mockFrontendRegistration: FrontendRegistration | undefined;
+
+jest.mock( '@woocommerce/base-utils', () => ( {
+ ...jest.requireActual( '@woocommerce/base-utils' ),
+ renderFrontend: jest.fn( ( registration ) => {
+ mockFrontendRegistration = registration;
+ } ),
+} ) );
+
+jest.mock( '@woocommerce/settings', () => ( {
+ ...jest.requireActual( '@woocommerce/settings' ),
+ getSetting: jest
+ .fn()
+ .mockImplementation( ( setting, defaultValue ) => defaultValue ),
+} ) );
+
+jest.mock( '../utils', () => ( {
+ ...jest.requireActual( '../utils' ),
+ getReviews: jest.fn(),
+} ) );
+
+jest.mock( '@wordpress/components', () => {
+ const actual = jest.requireActual( '@wordpress/components' );
+ const React = jest.requireActual( 'react' );
+
+ return {
+ ...actual,
+ __experimentalInputControl: ( { label, onChange, value } ) =>
+ React.createElement( 'input', {
+ 'aria-label': label,
+ onChange: ( event ) => onChange( event.target.value ),
+ value,
+ } ),
+ __experimentalNumberControl: ( {
+ __unstableStateReducer,
+ label,
+ onChange,
+ value,
+ } ) =>
+ React.createElement( 'input', {
+ 'aria-label': label,
+ onChange: ( event ) => {
+ const state = __unstableStateReducer
+ ? __unstableStateReducer( {
+ value: event.target.value,
+ } )
+ : { value: event.target.value };
+ onChange( state.value );
+ },
+ type: 'number',
+ value,
+ } ),
+ __experimentalToolsPanelItem: ( { children, label } ) =>
+ React.createElement( 'section', { 'aria-label': label }, children ),
+ 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
+ )
+ )
+ ),
+ ToggleControl: ( { checked, label, onChange } ) =>
+ React.createElement( 'input', {
+ 'aria-label': label,
+ checked,
+ onChange,
+ type: 'checkbox',
+ } ),
+ };
+} );
+
+// Importing the frontend entrypoint registers its selector and hydration mapper.
+jest.requireActual( '../frontend' );
+
+const mockGetReviews = getReviews as jest.Mock;
+const mockGetSetting = getSetting as jest.Mock;
+
+const createAttributes = (
+ overrides: Partial< ReviewBlockAttributes > = {}
+): ReviewBlockAttributes => ( {
+ editMode: false,
+ imageType: 'reviewer',
+ orderby: 'most-recent',
+ previewReviews: null as never,
+ reviewsOnLoadMore: 2,
+ reviewsOnPageLoad: 2,
+ showLoadMore: true,
+ showOrderby: true,
+ showReviewContent: true,
+ showReviewDate: false,
+ showReviewerName: false,
+ showReviewImage: false,
+ showReviewRating: false,
+ ...overrides,
+} );
+
+const createFrontendElement = (
+ className: string,
+ dataAttributes: Record< string, unknown > = {}
+) => {
+ const element = document.createElement( 'div' );
+ element.className = className;
+ Object.entries( dataAttributes ).forEach( ( [ key, value ] ) => {
+ if ( value !== undefined ) {
+ element.setAttribute( key, String( value ) );
+ }
+ } );
+ return element;
+};
+
+const getHydratedAttributes = ( element: HTMLElement ) => {
+ if ( ! mockFrontendRegistration ) {
+ throw new Error( 'Reviews frontend was not registered.' );
+ }
+
+ return {
+ ...element.dataset,
+ ...mockFrontendRegistration.getProps( element, 0 ).attributes,
+ };
+};
+
+const reviews = [
+ { id: 1, review: 'First review' },
+ { id: 2, review: 'Second review' },
+ { id: 3, review: 'Third review' },
+ { id: 4, review: 'Fourth review' },
+];
+
+describe( 'Product Reviews contracts', () => {
+ afterEach( () => {
+ jest.clearAllMocks();
+ mockGetSetting.mockImplementation(
+ ( setting, defaultValue ) => defaultValue
+ );
+ } );
+
+ describe( 'sorting', () => {
+ it.each( [
+ [ 'most recent', 'most-recent', true, 'desc', 'date_gmt' ],
+ [ 'highest rating', 'highest-rating', true, 'desc', 'rating' ],
+ [ 'lowest rating', 'lowest-rating', true, 'asc', 'rating' ],
+ [
+ 'ratings disabled fallback',
+ 'lowest-rating',
+ false,
+ 'desc',
+ 'date_gmt',
+ ],
+ ] )(
+ 'maps %s to the Store API order',
+ ( _name, value, ratingsEnabled, order, orderby ) => {
+ mockGetSetting.mockImplementation( ( setting, defaultValue ) =>
+ setting === 'reviewRatingsEnabled'
+ ? ratingsEnabled
+ : defaultValue
+ );
+
+ expect( getSortArgs( value ) ).toEqual( { order, orderby } );
+ }
+ );
+
+ it( 'resets an appended list and requests the lowest ratings first', async () => {
+ const user = userEvent.setup();
+ mockGetReviews
+ .mockResolvedValueOnce( {
+ reviews: reviews.slice( 0, 2 ),
+ totalReviews: 4,
+ } )
+ .mockResolvedValueOnce( {
+ reviews: reviews.slice( 2 ),
+ totalReviews: 4,
+ } )
+ .mockResolvedValueOnce( {
+ reviews: reviews.slice( 0, 2 ),
+ totalReviews: 4,
+ } );
+
+ render(
+ <FrontendContainerBlock
+ attributes={ createAttributes( { categoryIds: [ 7 ] } ) }
+ />
+ );
+
+ await waitFor( () =>
+ expect( mockGetReviews ).toHaveBeenCalledTimes( 1 )
+ );
+ await user.click(
+ await screen.findByRole( 'button', {
+ name: 'Load more reviews',
+ } )
+ );
+ await waitFor( () =>
+ expect( mockGetReviews ).toHaveBeenCalledTimes( 2 )
+ );
+ expect( mockGetReviews ).toHaveBeenNthCalledWith( 2, {
+ category_id: '7',
+ offset: 2,
+ order: 'desc',
+ orderby: 'date_gmt',
+ per_page: 2,
+ } );
+
+ await user.selectOptions(
+ screen.getByRole( 'combobox', { name: 'Order reviews by' } ),
+ 'lowest-rating'
+ );
+ await waitFor( () =>
+ expect( mockGetReviews ).toHaveBeenCalledTimes( 3 )
+ );
+ expect( mockGetReviews ).toHaveBeenNthCalledWith( 3, {
+ category_id: '7',
+ offset: 0,
+ order: 'asc',
+ orderby: 'rating',
+ per_page: 2,
+ } );
+ } );
+ } );
+
+ describe( 'serialization and hydration', () => {
+ it.each( [
+ [
+ 'category IDs',
+ { categoryIds: [ 12, 34 ] },
+ { 'data-category-ids': '12,34' },
+ ],
+ [ 'product ID', { productId: 56 }, { 'data-product-id': 56 } ],
+ [
+ 'empty category IDs',
+ { categoryIds: [] },
+ { 'data-category-ids': '' },
+ ],
+ ] )( 'serializes %s', ( _name, selection, expected ) => {
+ expect(
+ getDataAttrs( createAttributes( selection ) )
+ ).toMatchObject( expected );
+ } );
+
+ it.each( [
+ [ 'positive integer', 4, 4 ],
+ [ 'numeric positive integer', '6', 6 ],
+ [ 'zero', 0, undefined ],
+ [ 'negative integer', -1, undefined ],
+ [ 'fraction', 1.5, undefined ],
+ [ 'non-numeric', 'invalid', undefined ],
+ ] )( 'serializes a %s offset', ( _name, offset, expected ) => {
+ expect(
+ getDataAttrs( createAttributes( { offset } ) )[ 'data-offset' ]
+ ).toBe( expected );
+ } );
+
+ it.each( [
+ [
+ 'category block',
+ 'wp-block-woocommerce-reviews-by-category has-content',
+ { 'data-category-ids': '7', 'data-offset': '3' },
+ { categoryIds: '7', isFilteredReviewsBlock: true, offset: 3 },
+ ],
+ [
+ 'product block',
+ 'wp-block-woocommerce-reviews-by-product',
+ { 'data-product-id': '9', 'data-offset': '-1' },
+ { productId: '9', isFilteredReviewsBlock: true, offset: 0 },
+ ],
+ [
+ 'all reviews block',
+ 'wp-block-woocommerce-all-reviews',
+ { 'data-offset': '1.5' },
+ { isFilteredReviewsBlock: false, offset: 0 },
+ ],
+ ] )(
+ 'hydrates the %s identity and offset',
+ ( _name, className, dataAttributes, expected ) => {
+ const element = createFrontendElement(
+ className,
+ dataAttributes
+ );
+
+ expect( getHydratedAttributes( element ) ).toMatchObject(
+ expected
+ );
+ }
+ );
+
+ it.each( [
+ 'wp-block-woocommerce-reviews-by-category',
+ 'wp-block-woocommerce-reviews-by-product',
+ ] )( 'does not request reviews for an empty %s', ( className ) => {
+ mockGetReviews.mockResolvedValue( {
+ reviews: [],
+ totalReviews: 0,
+ } );
+ const element = createFrontendElement( className );
+ const { container } = render(
+ <FrontendContainerBlock
+ attributes={ createAttributes(
+ getHydratedAttributes(
+ element
+ ) as Partial< ReviewBlockAttributes >
+ ) }
+ />
+ );
+
+ expect( container ).toBeEmptyDOMElement();
+ expect( mockGetReviews ).not.toHaveBeenCalled();
+ } );
+
+ it( 'does not request reviews for a saved empty category selection', () => {
+ mockGetReviews.mockResolvedValue( {
+ reviews: [],
+ totalReviews: 0,
+ } );
+ const attributes = createAttributes( { categoryIds: [] } );
+ const element = createFrontendElement(
+ 'wp-block-woocommerce-reviews-by-category',
+ getDataAttrs( attributes )
+ );
+ const { container } = render(
+ <FrontendContainerBlock
+ attributes={ createAttributes(
+ getHydratedAttributes(
+ element
+ ) as Partial< ReviewBlockAttributes >
+ ) }
+ />
+ );
+
+ expect( container ).toBeEmptyDOMElement();
+ expect( mockGetReviews ).not.toHaveBeenCalled();
+ } );
+ } );
+
+ describe( 'Offset control', () => {
+ it( 'renders a zero default offset', () => {
+ render(
+ <>
+ { getSharedReviewListControls(
+ createAttributes(),
+ jest.fn(),
+ { showOffset: true }
+ ) }
+ </>
+ );
+
+ expect(
+ screen.getByRole( 'spinbutton', { name: 'Offset' } )
+ ).toHaveValue( 0 );
+ } );
+
+ it.each( [
+ [ 'zero', '0', { offset: 0 } ],
+ [ 'a positive integer', '8', { offset: 8 } ],
+ [ 'negative zero', '-0', { offset: 0 } ],
+ [ 'an empty value', '', undefined ],
+ [ 'a fractional value', '1.5', undefined ],
+ [ 'a negative integer', '-2', undefined ],
+ ] )( 'accepts or rejects %s', ( _name, value, expected ) => {
+ const setAttributes = jest.fn();
+ render(
+ <>
+ { getSharedReviewListControls(
+ createAttributes( { offset: 1 } ),
+ setAttributes,
+ { showOffset: true }
+ ) }
+ </>
+ );
+
+ fireEvent.change(
+ screen.getByRole( 'spinbutton', { name: 'Offset' } ),
+ { target: { value } }
+ );
+
+ expect( setAttributes.mock.calls ).toEqual(
+ expected ? [ [ expected ] ] : []
+ );
+ } );
+
+ it.each( [
+ [
+ 'category',
+ 'wp-block-woocommerce-reviews-by-category',
+ { categoryIds: [ 9 ] },
+ { category_id: 9 },
+ ],
+ [
+ 'product',
+ 'wp-block-woocommerce-reviews-by-product',
+ { productId: 9 },
+ { product_id: '9' },
+ ],
+ ] )(
+ 'flows a positive value through serialization, hydration, and the %s review request',
+ async ( _name, className, selection, filterArgs ) => {
+ const setAttributes = jest.fn();
+ const attributes = createAttributes( selection );
+ const controls = render(
+ <>
+ { getSharedReviewListControls(
+ attributes,
+ setAttributes,
+ {
+ showOffset: true,
+ }
+ ) }
+ </>
+ );
+
+ fireEvent.change(
+ screen.getByRole( 'spinbutton', { name: 'Offset' } ),
+ { target: { value: '5' } }
+ );
+ expect( setAttributes ).toHaveBeenCalledWith( { offset: 5 } );
+ controls.unmount();
+
+ const serialized = getDataAttrs( { ...attributes, offset: 5 } );
+ const element = createFrontendElement( className, serialized );
+ mockGetReviews.mockResolvedValue( {
+ reviews: reviews.slice( 0, 2 ),
+ totalReviews: 4,
+ } );
+
+ render(
+ <FrontendContainerBlock
+ attributes={
+ getHydratedAttributes(
+ element
+ ) as ReviewBlockAttributes
+ }
+ />
+ );
+
+ await waitFor( () =>
+ expect( mockGetReviews ).toHaveBeenCalledWith( {
+ ...filterArgs,
+ offset: 5,
+ order: 'desc',
+ orderby: 'date_gmt',
+ per_page: 2,
+ } )
+ );
+ }
+ );
+ } );
+} );
diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-product-reviews-blocks b/plugins/woocommerce/client/blocks/changelog/testops-234-product-reviews-blocks
new file mode 100644
index 00000000000..96faf1e4ef3
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/testops-234-product-reviews-blocks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce Product Reviews block E2E tests from 13 to 4; Jest and a Store API PHPUnit matrix own sorting, offset, and empty-selection behavior.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/all-reviews/all-reviews.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/all-reviews/all-reviews.block_theme.spec.ts
index 8d76c2a5d3d..5538b8c3c68 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/all-reviews/all-reviews.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/all-reviews/all-reviews.block_theme.spec.ts
@@ -26,61 +26,33 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await editor.insertBlock( { name: BLOCK_NAME } );
} );
- test( 'block can be inserted and it sorts reviews by most recent by default', async ( {
+ test( 'block can be inserted and sorts reviews in the frontend', async ( {
frontendUtils,
editor,
} ) => {
await expect(
editor.canvas.getByText( allReviews[ 0 ].review )
).toBeVisible();
-
- await editor.publishAndVisitPost();
-
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
- } );
-
- test( 'can sort by highest rating in the frontend', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
- await editor.publishAndVisitPost();
-
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
-
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Highest rating' );
-
- await expect( reviews.first() ).toHaveText( highestRating.review );
- } );
-
- test( 'can sort by lowest rating in the frontend', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
await editor.publishAndVisitPost();
const block = await frontendUtils.getBlockByName( BLOCK_NAME );
const reviews = block.locator(
'.wc-block-components-review-list-item__text'
);
+ const select = block.getByLabel( 'Order by' );
- await expect( reviews.first() ).toHaveText( latestReview.review );
+ await test.step( 'Most recent by default', async () => {
+ await expect( reviews.first() ).toHaveText( latestReview.review );
+ } );
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Lowest rating' );
+ await test.step( 'Highest rating', async () => {
+ await select.selectOption( 'Highest rating' );
+ await expect( reviews.first() ).toHaveText( highestRating.review );
+ } );
- await expect( reviews.first() ).toHaveText( lowestRating.review );
+ await test.step( 'Lowest rating', async () => {
+ await select.selectOption( 'Lowest rating' );
+ await expect( reviews.first() ).toHaveText( lowestRating.review );
+ } );
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
index 09e686b50bb..77a39850442 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
@@ -6,112 +6,42 @@ import { expect, test } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
-import { hoodieReviews, allReviews } from '../../../test-data/blocks/data/data';
-
-const latestReview = hoodieReviews[ hoodieReviews.length - 1 ];
-
-const highestRating = [ ...hoodieReviews ].sort(
- ( a, b ) => b.rating - a.rating
-)[ 0 ];
-
-const lowestRating = [ ...hoodieReviews ].sort(
- ( a, b ) => a.rating - b.rating
-)[ 0 ];
+import { allReviews, hoodieReviews } from '../../../test-data/blocks/data/data';
const BLOCK_NAME = 'woocommerce/reviews-by-category';
test.describe( `${ BLOCK_NAME } Block`, () => {
- test.describe( 'with a category selected', () => {
- test.beforeEach( async ( { admin, editor } ) => {
- await admin.createNewPost();
- await editor.insertBlock( { name: BLOCK_NAME } );
-
- const blockLocator = await editor.getBlockByName( BLOCK_NAME );
- const categoryCheckbox = blockLocator.getByRole( 'checkbox', {
- name: 'Clothing',
- exact: true,
- } );
- await categoryCheckbox.check();
- await expect( categoryCheckbox ).toBeChecked();
-
- await blockLocator.getByRole( 'button', { name: 'Done' } ).click();
- } );
-
- test( 'block can be inserted and it successfully renders a review in the editor and the frontend', async ( {
- page,
- editor,
- } ) => {
- await expect(
- editor.canvas.getByText( hoodieReviews[ 0 ].review )
- ).toBeVisible();
-
- await editor.publishAndVisitPost();
-
- await expect(
- page.getByText( hoodieReviews[ 0 ].review )
- ).toBeVisible();
- } );
-
- test( 'sorts by most recent review by default and can sort by highest rating', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
- await editor.publishAndVisitPost();
-
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
-
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Highest rating' );
-
- await expect( reviews.first() ).toHaveText( highestRating.review );
- } );
-
- test( 'can sort by lowest rating', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
- await editor.publishAndVisitPost();
-
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
-
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Lowest rating' );
-
- await expect( reviews.first() ).toHaveText( lowestRating.review );
- } );
- } );
-
- test( 'renders no reviews on the frontend when published without a category selected', async ( {
+ test( 'block can be inserted and it successfully renders a review in the editor and the frontend', async ( {
+ page,
admin,
editor,
- frontendUtils,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: BLOCK_NAME } );
- await editor.publishAndVisitPost();
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
+ const blockLocator = await editor.getBlockByName( BLOCK_NAME );
+ const categoryCheckbox = blockLocator.getByRole( 'checkbox', {
+ name: 'Clothing',
+ exact: true,
+ } );
+ await categoryCheckbox.click();
+ await expect( categoryCheckbox ).toBeChecked();
+
+ await blockLocator.getByRole( 'button', { name: 'Done' } ).click();
+ await expect(
+ editor.canvas.getByText( hoodieReviews[ 0 ].review )
+ ).toBeVisible();
+
+ await editor.publishAndVisitPost();
await expect(
- block.locator( '.wc-block-components-review-list-item__text' )
- ).toHaveCount( 0 );
+ page
+ .locator( '.wp-block-woocommerce-reviews-by-category' )
+ .getByText( hoodieReviews[ 0 ].review )
+ ).toBeVisible();
} );
- test( 'can skip reviews with an offset and load subsequent reviews', async ( {
+ test( 'Category offset persists into frontend review results', async ( {
page,
admin,
frontendUtils,
@@ -129,11 +59,7 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await blockLocator
.getByRole( 'checkbox', { name: /Accessories/ } )
.check();
- await blockLocator
- .getByRole( 'button', {
- name: 'Done',
- } )
- .click();
+ await blockLocator.getByRole( 'button', { name: 'Done' } ).click();
await editor.openDocumentSettingsSidebar();
const sidebarSettings = page.getByRole( 'region', {
@@ -158,7 +84,6 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await expect(
editor.canvas.getByText( allReviews[ 3 ].review )
).toBeVisible();
-
await expect(
editor.canvas.getByText( allReviews[ 4 ].review )
).toBeHidden();
@@ -166,7 +91,6 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await editor.publishAndVisitPost();
const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
await expect( block.getByText( allReviews[ 0 ].review ) ).toBeVisible();
await expect( block.getByText( allReviews[ 1 ].review ) ).toBeVisible();
await expect( block.getByText( allReviews[ 2 ].review ) ).toBeVisible();
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
index 3973bcda605..0f5801cef39 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
@@ -10,152 +10,32 @@ import { hoodieReviews } from '../../../test-data/blocks/data/data';
const BLOCK_NAME = 'woocommerce/reviews-by-product';
-const latestReview = hoodieReviews[ hoodieReviews.length - 1 ];
-
-const highestRating = [ ...hoodieReviews ].sort(
- ( a, b ) => b.rating - a.rating
-)[ 0 ];
-
-const lowestRating = [ ...hoodieReviews ].sort(
- ( a, b ) => a.rating - b.rating
-)[ 0 ];
-
test.describe( `${ BLOCK_NAME } Block`, () => {
- test.describe( 'with a product selected', () => {
- test.beforeEach( async ( { admin, editor } ) => {
- await admin.createNewPost();
- await editor.insertBlock( { name: BLOCK_NAME } );
-
- const productCheckbox = editor.canvas.getByLabel(
- 'Hoodie, has 3 reviews'
- );
- await productCheckbox.check();
- await expect( productCheckbox ).toBeChecked();
-
- await editor.canvas.getByRole( 'button', { name: 'Done' } ).click();
- } );
-
- test( 'block can be inserted and it successfully renders a review in the editor and the frontend', async ( {
- page,
- editor,
- } ) => {
- await expect(
- editor.canvas.getByText( hoodieReviews[ 0 ].review )
- ).toBeVisible();
-
- await editor.publishAndVisitPost();
-
- await expect(
- page.getByText( hoodieReviews[ 0 ].review )
- ).toBeVisible();
- } );
-
- test( 'sorts by most recent by default and can sort by highest rating', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
- await editor.publishAndVisitPost();
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
-
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Highest rating' );
-
- await expect( reviews.first() ).toHaveText( highestRating.review );
- } );
-
- test( 'can sort by lowest rating', async ( {
- page,
- frontendUtils,
- editor,
- } ) => {
- await editor.publishAndVisitPost();
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
- const reviews = block.locator(
- '.wc-block-components-review-list-item__text'
- );
-
- await expect( reviews.first() ).toHaveText( latestReview.review );
-
- const select = page.getByLabel( 'Order by' );
- await select.selectOption( 'Lowest rating' );
-
- await expect( reviews.first() ).toHaveText( lowestRating.review );
- } );
- } );
-
- test( 'renders no reviews on the frontend when published without a product selected', async ( {
- admin,
- editor,
- frontendUtils,
- } ) => {
- await admin.createNewPost();
- await editor.insertBlock( { name: BLOCK_NAME } );
- await editor.publishAndVisitPost();
-
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
- await expect(
- block.locator( '.wc-block-components-review-list-item__text' )
- ).toHaveCount( 0 );
- } );
-
- test( 'can skip reviews with an offset in the editor and frontend', async ( {
+ test( 'block can be inserted and it successfully renders a review in the editor and the frontend', async ( {
page,
admin,
- frontendUtils,
editor,
} ) => {
await admin.createNewPost();
await editor.insertBlock( { name: BLOCK_NAME } );
+
const productCheckbox = editor.canvas.getByLabel(
'Hoodie, has 3 reviews'
);
- await productCheckbox.check();
- await editor.canvas
- .getByRole( 'button', {
- name: 'Done',
- } )
- .click();
-
- await editor.openDocumentSettingsSidebar();
- const sidebarSettings = page.getByRole( 'region', {
- name: 'Editor settings',
- } );
-
- await sidebarSettings
- .getByRole( 'spinbutton', { name: 'Offset' } )
- .fill( '1' );
+ await productCheckbox.click();
+ await expect( productCheckbox ).toBeChecked();
+ await editor.canvas.getByRole( 'button', { name: 'Done' } ).click();
await expect(
editor.canvas.getByText( hoodieReviews[ 0 ].review )
).toBeVisible();
- await expect(
- editor.canvas.getByText( hoodieReviews[ 1 ].review )
- ).toBeVisible();
- await expect(
- editor.canvas.getByText( hoodieReviews[ 2 ].review )
- ).toBeHidden();
await editor.publishAndVisitPost();
- const block = await frontendUtils.getBlockByName( BLOCK_NAME );
-
await expect(
- block.getByText( hoodieReviews[ 0 ].review )
+ page
+ .locator( '.wp-block-woocommerce-reviews-by-product' )
+ .getByText( hoodieReviews[ 0 ].review )
).toBeVisible();
- await expect(
- block.getByText( hoodieReviews[ 1 ].review )
- ).toBeVisible();
- await expect(
- block.getByText( hoodieReviews[ 2 ].review )
- ).toBeHidden();
} );
} );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductReviews.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductReviews.php
index b566d837ca9..d1e40e86afd 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductReviews.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductReviews.php
@@ -217,6 +217,124 @@ class ProductReviews extends ControllerTestCase {
$this->assertSame( 5, $data[0]['rating'] );
}
+ /**
+ * @testdox Reviews are returned in the requested sort order and respect offset.
+ * @dataProvider get_items_sort_and_offset_data
+ *
+ * @param string $orderby Review field used for sorting.
+ * @param string $order Sort direction.
+ * @param int $offset Number of reviews to skip.
+ * @param array $expected_contents Expected review contents in response order.
+ * @param string $filter_by Whether to scope the request by `product_id` or `category_id`.
+ */
+ public function test_get_items_sort_and_offset_matrix( string $orderby, string $order, int $offset, array $expected_contents, string $filter_by = 'product' ): void {
+ $fixtures = new FixtureData();
+ $category = $fixtures->get_product_category(
+ array(
+ 'name' => 'Review ordering category',
+ )
+ );
+ $product = $fixtures->get_simple_product(
+ array(
+ 'name' => 'Review ordering product',
+ 'regular_price' => 20,
+ 'category_ids' => array( $category['term_id'] ),
+ )
+ );
+
+ $fixtures->add_product_review(
+ $product->get_id(),
+ 1,
+ 'Oldest one-star review',
+ array(
+ 'comment_date' => '2024-01-01 09:00:00',
+ 'comment_date_gmt' => '2024-01-01 09:00:00',
+ )
+ );
+ $fixtures->add_product_review(
+ $product->get_id(),
+ 5,
+ 'Middle five-star review',
+ array(
+ 'comment_date' => '2024-01-02 09:00:00',
+ 'comment_date_gmt' => '2024-01-02 09:00:00',
+ )
+ );
+ $fixtures->add_product_review(
+ $product->get_id(),
+ 3,
+ 'Newest three-star review',
+ array(
+ 'comment_date' => '2024-01-03 09:00:00',
+ 'comment_date_gmt' => '2024-01-03 09:00:00',
+ )
+ );
+
+ $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products/reviews' );
+ if ( 'category' === $filter_by ) {
+ // The category holds only this product, so the expected order is the same
+ // as the product-scoped rows. Sorting and filtering are resolved by
+ // different parts of the query, and nothing else pairs them.
+ $request->set_param( 'category_id', (string) $category['term_id'] );
+ } else {
+ $request->set_param( 'product_id', (string) $product->get_id() );
+ }
+ $request->set_param( 'per_page', 10 );
+ $request->set_param( 'orderby', $orderby );
+ $request->set_param( 'order', $order );
+ $request->set_param( 'offset', $offset );
+ $response = rest_get_server()->dispatch( $request );
+
+ $this->assertSame( 200, $response->get_status(), 'Unexpected status code.' );
+ $this->assertSame(
+ array_map( 'wpautop', $expected_contents ),
+ wp_list_pluck( $response->get_data(), 'review' ),
+ 'Reviews were not returned in the requested order.'
+ );
+ $this->assertSame( 3, $response->get_headers()['X-WP-Total'], 'Unexpected total review count.' );
+ }
+
+ /**
+ * Data provider for review sort and offset requests.
+ *
+ * @return array<string, array{string, string, int, string[], 3?: string}>
+ */
+ public function get_items_sort_and_offset_data(): array {
+ return array(
+ 'recent' => array(
+ 'date_gmt',
+ 'desc',
+ 0,
+ array( 'Newest three-star review', 'Middle five-star review', 'Oldest one-star review' ),
+ ),
+ 'rating ascending' => array(
+ 'rating',
+ 'asc',
+ 0,
+ array( 'Oldest one-star review', 'Newest three-star review', 'Middle five-star review' ),
+ ),
+ 'rating descending' => array(
+ 'rating',
+ 'desc',
+ 0,
+ array( 'Middle five-star review', 'Newest three-star review', 'Oldest one-star review' ),
+ ),
+ 'recent with offset' => array(
+ 'date_gmt',
+ 'desc',
+ 1,
+ array( 'Middle five-star review', 'Oldest one-star review' ),
+ ),
+ 'rating descending, scoped by category' => array(
+ 'rating',
+ 'desc',
+ 0,
+ array( 'Middle five-star review', 'Newest three-star review', 'Oldest one-star review' ),
+ 'category',
+ ),
+ );
+ }
+
/**
* Test getting reviews from a specific product.
*/