Commit cb31ea3dd06 for woocommerce
commit cb31ea3dd0602ac6bef36401d90ae7dbc049b084
Author: Cem Ünalan <raicem@users.noreply.github.com>
Date: Wed Sep 2 10:59:45 2026 +0300
Hide empty star ratings on Discover page product cards (#67882)
* Hide marketplace rating block when a product has no rating
The in-app Marketplace product card guarded its rating block with
`product.averageRating !== null`, which lets `undefined` through:
`undefined !== null` is true. When it does, the block renders a filled
star with no number beside it, announced to screen readers as
"0.0 stars" via the `?? 0` fallback — a zero rating for a product that
has none.
`undefined` is reachable because the two paths that build Product
objects normalize differently. The search path maps the response field
by field with `averageRating: product.rating ?? null`, so a missing
rating becomes null. The Discover path casts the featured response to
ProductGroup[] with no mapping, so whatever keys the API sends land on
the object verbatim, and an absent key reads as `undefined`. The type
allows it: `averageRating?: number | null`.
Guard on `typeof === 'number'` instead, which excludes null and
undefined alike, and drop the now-unreachable `?? 0` fallback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Hide marketplace rating block when the rating is zero
* Reject non-numeric ratings and assert the star icon is absent in tests
* Inline no-rating test assertions to satisfy jest/expect-expect
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-marketplace-missing-rating-guard b/plugins/woocommerce/changelog/fix-marketplace-missing-rating-guard
new file mode 100644
index 00000000000..b558949705f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-marketplace-missing-rating-guard
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Hide the rating block on in-app Marketplace product cards when a product has no rating
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
index da5dbc676e2..3612d3475ca 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card-footer.tsx
@@ -199,6 +199,9 @@ function ProductCardFooter( props: { product: Product } ) {
);
}
+ // Ratings run 1-5, so 0 -- like a missing value -- means "no rating".
+ const averageRating = product.averageRating ?? 0;
+
return (
<>
<div className="woocommerce-marketplace__product-card__price">
@@ -230,18 +233,18 @@ function ProductCardFooter( props: { product: Product } ) {
</span>
</div>
<div className="woocommerce-marketplace__product-card__rating">
- { product.averageRating !== null && (
+ { Number.isFinite( averageRating ) && averageRating > 0 && (
<>
<span className="woocommerce-marketplace__product-card__rating-icon">
<Icon icon={ 'star-filled' } size={ 16 } />
</span>
<span className="woocommerce-marketplace__product-card__rating-average">
- <span aria-hidden>{ product.averageRating }</span>
+ <span aria-hidden>{ averageRating }</span>
<span className="screen-reader-text">
{ sprintf(
// translators: %.1f: average rating
__( '%.1f stars', 'woocommerce' ),
- product.averageRating ?? 0
+ averageRating
) }
</span>
</span>
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx
new file mode 100644
index 00000000000..966f719cda1
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card-footer.test.tsx
@@ -0,0 +1,113 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+import React from 'react';
+
+jest.mock( '@woocommerce/navigation', () => ( {
+ getNewPath: jest.fn( () => '/new-path' ),
+ navigateTo: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/tracks', () => ( {
+ recordEvent: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/data', () => ( {
+ useUser: jest.fn( () => ( {
+ user: null,
+ currentUserCan: jest.fn( () => false ),
+ } ) ),
+} ) );
+
+/**
+ * Internal dependencies
+ */
+import ProductCardFooter from '../product-card-footer';
+import { MarketplaceContext } from '../../../contexts/marketplace-context';
+import { MarketplaceContextType } from '../../../contexts/types';
+import { Product, ProductType } from '../../product-list/types';
+
+const context = {
+ selectedTab: 'extensions',
+ isProductInstalled: () => false,
+} as unknown as MarketplaceContextType;
+
+const product: Product = {
+ id: 1,
+ title: 'Test extension',
+ image: '',
+ type: ProductType.extension,
+ description: '',
+ vendorName: '',
+ vendorUrl: '',
+ icon: '',
+ url: '',
+ price: 0,
+ isInstallable: false,
+ currency: 'USD',
+ isOnSale: false,
+ regularPrice: 0,
+ reviewsCount: 10,
+};
+
+function renderFooter( averageRating: number | null | undefined ) {
+ return render(
+ <MarketplaceContext.Provider value={ context }>
+ <ProductCardFooter product={ { ...product, averageRating } } />
+ </MarketplaceContext.Provider>
+ );
+}
+
+describe( 'ProductCardFooter rating', () => {
+ it( 'renders the rating when the product has one', () => {
+ const { getByText } = renderFooter( 4.5 );
+
+ expect( getByText( '4.5' ) ).toBeInTheDocument();
+ } );
+
+ it( 'renders no rating when the product has none', () => {
+ const { container } = renderFooter( null );
+
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating'
+ )?.textContent
+ ).toBe( '' );
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating-icon'
+ )
+ ).toBeNull();
+ } );
+
+ it( 'renders no rating when the rating is zero', () => {
+ const { container } = renderFooter( 0 );
+
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating'
+ )?.textContent
+ ).toBe( '' );
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating-icon'
+ )
+ ).toBeNull();
+ } );
+
+ it( 'renders no rating when the API omits the rating', () => {
+ const { container } = renderFooter( undefined );
+
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating'
+ )?.textContent
+ ).toBe( '' );
+ expect(
+ container.querySelector(
+ '.woocommerce-marketplace__product-card__rating-icon'
+ )
+ ).toBeNull();
+ } );
+} );