Commit 1435ec24841 for woocommerce
commit 1435ec24841379da9157468664992ef50525b670
Author: Miroslav Mitev <m1r0@users.noreply.github.com>
Date: Fri Aug 28 11:39:22 2026 +0300
Fix stale Revenue report rows when an extension filter changes (#68052)
* Fix stale Revenue report rows when an extension filter changes
diff --git a/plugins/woocommerce/changelog/66520-fix-revenue-table-stale-rows b/plugins/woocommerce/changelog/66520-fix-revenue-table-stale-rows
new file mode 100644
index 00000000000..eaf7d978b34
--- /dev/null
+++ b/plugins/woocommerce/changelog/66520-fix-revenue-table-stale-rows
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Analytics Revenue table rows keeping the first dataset loaded when an extension report filter changes.
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/revenue/table.js b/plugins/woocommerce/client/admin/client/analytics/report/revenue/table.js
index 013263ad916..d54e70bfd07 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/revenue/table.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/revenue/table.js
@@ -291,6 +291,8 @@ RevenueReportTable.contextType = CurrencyContext;
* @param {boolean} isRequesting
* @param {Object} tableQuery
* @param {Object} revenueData
+ * @param {string} dateType
+ * @param {Object} filteredTableQuery The query the data was fetched with, including report filter params.
* @return {Object} formatted tableData prop
*/
const formatProps = memoize(
@@ -306,11 +308,18 @@ const formatProps = memoize(
},
dateType,
} ),
- ( isError, isRequesting, tableQuery, revenueData, dateType ) =>
+ (
+ isError,
+ isRequesting,
+ tableQuery,
+ revenueData,
+ dateType,
+ filteredTableQuery
+ ) =>
[
isError,
isRequesting,
- stringify( tableQuery ),
+ stringify( filteredTableQuery ),
get( revenueData, [ 'totalResults' ], 0 ),
get( revenueData, [ 'data', 'intervals' ], EMPTY_ARRAY ).length,
dateType,
@@ -390,7 +399,8 @@ export default compose(
isRequesting,
tableQuery,
revenueData,
- dateType
+ dateType,
+ filteredTableQuery
);
} )
)( RevenueReportTable );
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/revenue/test/table.test.js b/plugins/woocommerce/client/admin/client/analytics/report/revenue/test/table.test.js
new file mode 100644
index 00000000000..9c8178b6152
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/report/revenue/test/table.test.js
@@ -0,0 +1,178 @@
+/**
+ * External dependencies
+ */
+import { render, screen, within } from '@testing-library/react';
+import { memoize } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import RevenueReportTable from '../table';
+
+// Report filter registered the way an extension would, through
+// woocommerce_admin_revenue_report_filters.
+const mockFilters = [
+ {
+ label: 'Order size',
+ param: 'big_only',
+ staticParams: [],
+ showFilters: () => true,
+ defaultValue: 'all',
+ filters: [
+ { label: 'All orders', value: 'all' },
+ { label: 'Over 100 only', value: '1' },
+ ],
+ },
+];
+
+const mockInterval = ( date, ordersCount ) => ( {
+ interval: date,
+ date_start: `${ date } 00:00:00`,
+ date_end: `${ date } 23:59:59`,
+ subtotals: {
+ orders_count: ordersCount,
+ gross_sales: ordersCount * 100,
+ total_sales: ordersCount * 100,
+ net_revenue: ordersCount * 100,
+ refunds: 0,
+ coupons: 0,
+ taxes: 0,
+ shipping: 0,
+ },
+} );
+
+// Both datasets have the same number of intervals and the same totalResults,
+// like the Revenue report does: one row per day, whatever the filter.
+const mockUnfiltered = {
+ totalResults: 2,
+ data: {
+ intervals: [
+ mockInterval( '2023-01-01', 2 ),
+ mockInterval( '2023-01-02', 2 ),
+ ],
+ },
+};
+
+const mockFiltered = {
+ totalResults: 2,
+ data: {
+ intervals: [
+ mockInterval( '2023-01-01', 1 ),
+ mockInterval( '2023-01-02', 1 ),
+ ],
+ },
+};
+
+// table.js memoizes at module scope, so those caches outlive each test. Hand
+// the tests a way to reset them by tracking every function memoize() returns.
+jest.mock( 'lodash', () => {
+ const actual = jest.requireActual( 'lodash' );
+ const memoized = [];
+
+ const mockMemoize = ( ...args ) => {
+ const fn = actual.memoize( ...args );
+ memoized.push( fn );
+ return fn;
+ };
+ mockMemoize.clearAll = () => memoized.forEach( ( fn ) => fn.cache.clear() );
+
+ return { ...actual, memoize: mockMemoize };
+} );
+
+jest.mock( '@wordpress/data', () => {
+ const actual = jest.requireActual( '@wordpress/data' );
+ const { createElement } = jest.requireActual( '@wordpress/element' );
+
+ return {
+ ...actual,
+ withSelect: ( mapSelectToProps ) => ( Wrapped ) => ( props ) =>
+ createElement( Wrapped, {
+ ...props,
+ ...mapSelectToProps( global.mockSelect, props ),
+ } ),
+ };
+} );
+
+// Stand-in for ReportTable rendering a row per interval, so the assertions can
+// read the dates and order counts back off the rendered table.
+jest.mock( '../../../components/report-table', () => {
+ const { createElement } = jest.requireActual( '@wordpress/element' );
+
+ return {
+ __esModule: true,
+ default: ( { tableData } ) =>
+ createElement(
+ 'table',
+ null,
+ createElement(
+ 'tbody',
+ null,
+ tableData.items.data.map( ( row ) =>
+ createElement(
+ 'tr',
+ { key: row.interval },
+ createElement( 'td', null, row.interval ),
+ createElement(
+ 'td',
+ null,
+ row.subtotals.orders_count
+ )
+ )
+ )
+ )
+ ),
+ };
+} );
+
+describe( 'RevenueReportTable', () => {
+ beforeEach( () => {
+ memoize.clearAll();
+
+ global.mockSelect = () => ( {
+ getSetting: () => ( {
+ woocommerce_default_date_range:
+ 'period=custom&compare=previous_period&after=2023-01-01&before=2023-01-02',
+ } ),
+ getOption: () => 'date_paid',
+ getReportStats: ( endpoint, query ) =>
+ query.big_only === '1' ? mockFiltered : mockUnfiltered,
+ getReportStatsError: () => null,
+ isResolving: () => false,
+ } );
+ } );
+
+ const revenueTable = ( query ) => (
+ <RevenueReportTable
+ query={ query }
+ filters={ mockFilters }
+ advancedFilters={ {} }
+ />
+ );
+
+ const getRenderedOrderCounts = () =>
+ screen
+ .getAllByRole( 'row' )
+ .map(
+ ( row ) => within( row ).getAllByRole( 'cell' )[ 1 ].textContent
+ );
+
+ it( 'renders the rows for the active report filter', () => {
+ const baseQuery = {
+ period: 'custom',
+ compare: 'previous_period',
+ after: '2023-01-01',
+ before: '2023-01-02',
+ };
+
+ const { rerender } = render( revenueTable( baseQuery ) );
+ expect( getRenderedOrderCounts() ).toEqual( [ '2', '2' ] );
+
+ // Same date range, same row count, only the filter param changes. The
+ // rows have to follow it instead of coming back from a stale cache.
+ rerender( revenueTable( { ...baseQuery, big_only: '1' } ) );
+ expect( getRenderedOrderCounts() ).toEqual( [ '1', '1' ] );
+
+ rerender( revenueTable( baseQuery ) );
+ expect( getRenderedOrderCounts() ).toEqual( [ '2', '2' ] );
+ } );
+} );