Commit 0cef7675ed1 for woocommerce

commit 0cef7675ed1627bb079a83ed2e1232c062e041b0
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Tue Jul 7 20:48:46 2026 +0300

    e2e tests: harden analytics-data spec (#66371)

diff --git a/plugins/woocommerce/changelog/dev-testops-201-analytics-data-flaky b/plugins/woocommerce/changelog/dev-testops-201-analytics-data-flaky
new file mode 100644
index 00000000000..9c38ff41f4d
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-testops-201-analytics-data-flaky
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+E2E: de-flake analytics-data.spec by scoping analytics assertions to the spec's own products, polling for analytics-import readiness instead of a fixed wait, and matching summary tiles independently of the comparison delta text.
diff --git a/plugins/woocommerce/tests/e2e/playwright.config.ts b/plugins/woocommerce/tests/e2e/playwright.config.ts
index 6e8e6281abf..20344f92adf 100644
--- a/plugins/woocommerce/tests/e2e/playwright.config.ts
+++ b/plugins/woocommerce/tests/e2e/playwright.config.ts
@@ -120,9 +120,13 @@ export const setupProjects = [
  * `core-parallel` by default, except the other-project folders in `nonCoreSpecs`.
  */
 const serialRunSpecs = [
-	// Drains the whole store's Action Scheduler queue via `?process-waiting-actions`
-	// (other workers' order/product churn floods it past the 10s timeout) and asserts
-	// exact store-wide totals, polluted by concurrent orders.
+	// Forces immediate analytics-import mode by flipping the global
+	// `woocommerce_analytics_scheduled_import` option, and needs it to stay
+	// immediate while it creates orders and drains their imports. In
+	// `core-parallel` that would race `analytics-settings.spec.ts` — which runs in
+	// that pool and toggles the same option — over the shared store and Action
+	// Scheduler queue. Its numeric assertions are scoped to its own products, so
+	// they no longer depend on store-wide totals.
 	'**/tests/analytics/analytics-data.spec.ts',
 	// Asserts store-wide `$0.00 / Orders 0`, polluted by concurrent orders.
 	'**/tests/analytics/analytics-access.spec.ts',
diff --git a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-data.spec.ts b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-data.spec.ts
index cc01af5bb1c..7041d85e6dd 100644
--- a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-data.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-data.spec.ts
@@ -1,6 +1,7 @@
 /**
  * External dependencies
  */
+import type { Page } from '@playwright/test';
 import {
 	WC_ADMIN_API_PATH,
 	WC_API_PATH,
@@ -12,6 +13,29 @@ import {
 import { expect, tags, test as baseTest } from '../../fixtures/fixtures';
 import { ADMIN_STATE_PATH } from '../../playwright.config';

+/**
+ * Escape a string for literal use inside a RegExp.
+ */
+const escapeRegExp = ( value: string ) =>
+	value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
+
+/**
+ * Locate an analytics summary tile ("performance indicator") by its label and
+ * value, ignoring the volatile delta text that follows (e.g.
+ * "No change from Previous year:").
+ *
+ * The tile is a `menuitem` whose accessible name is
+ * `"<label> <value> <delta>"`, so we anchor on `"<label> <value>"` up to the
+ * next whitespace (or end of string). The trailing boundary is what stops
+ * `"Orders 10"` from also matching `"Orders 100"`.
+ */
+const summaryTile = ( page: Page, label: string, value: string ) =>
+	page.getByRole( 'menuitem', {
+		name: new RegExp(
+			`^${ escapeRegExp( `${ label } ${ value }` ) }(\\s|$)`
+		),
+	} );
+
 const test = baseTest.extend( {
 	storageState: ADMIN_STATE_PATH,

@@ -269,19 +293,56 @@ test.beforeAll( async ( { request, restApi } ) => {
 			);
 		} );

-	// Process the Action Scheduler tasks that import the orders created above.
+	// Import the orders created above into the analytics lookup tables, then wait
+	// for that import to actually complete before any test asserts on the data.
 	//
-	// In immediate mode each order schedules its import action for `time() + 5`
-	// (see SchedulerTraits::schedule_action), so wait for those actions to become
-	// due before draining the queue. Draining earlier skips them and the orders
-	// never import.
-	await new Promise( ( resolve ) => setTimeout( resolve, 5000 ) );
-
-	// Drain the Action Scheduler queue via the `process-waiting-actions`
-	// mu-plugin. It runs the queue runner on `init` and exits before rendering,
-	// so a plain HTTP request is enough — no browser page needed.
-	const response = await request.get( '?process-waiting-actions' );
-	expect( response.ok() ).toBeTruthy();
+	// In immediate mode each order scheduled its import action for `time() + 5`
+	// (see SchedulerTraits::schedule_action), so the actions are not due the
+	// instant this runs. Rather than sleeping a fixed amount and draining once,
+	// poll: on each pass drain the Action Scheduler queue via the
+	// `process-waiting-actions` mu-plugin, then re-read a WC Analytics report
+	// scoped to *this spec's products* until the expected totals appear. Looping
+	// the drain absorbs a queue backed up by earlier specs in this serial run — a
+	// single fixed-time drain could return before this spec's actions run,
+	// leaving the lookup tables empty and every assertion timing out.
+	await expect
+		.poll(
+			async () => {
+				// `expect.poll` awaits this callback outside its retry try/catch,
+				// so anything that throws here aborts the whole poll instead of
+				// retrying. Both requests can fail transiently while the queue is
+				// draining (`restApi.get` is axios and rejects on any non-2xx), so
+				// swallow errors and return an undefined total — the matcher then
+				// simply fails and the next iteration retries.
+				try {
+					await request.get( '?process-waiting-actions' );
+
+					const stats = await restApi.get< {
+						totals?: {
+							orders_count?: number;
+							items_sold?: number;
+						};
+					} >( 'wc-analytics/reports/products/stats', {
+						products: productIds.join( ',' ),
+						after: '2020-01-01T00:00:00',
+						before: '2050-01-01T00:00:00',
+					} );
+					return {
+						orders: stats.data?.totals?.orders_count,
+						itemsSold: stats.data?.totals?.items_sold,
+					};
+				} catch ( e ) {
+					return { orders: undefined, itemsSold: undefined };
+				}
+			},
+			{
+				message:
+					"This spec's 10 orders must be imported into the analytics lookup tables before the tests assert on them.",
+				timeout: 90_000,
+				intervals: [ 1_000, 2_000, 3_000 ],
+			}
+		)
+		.toEqual( { orders: 10, itemsSold: 110 } );
 } );

 test.afterAll( async ( { restApi } ) => {
@@ -305,7 +366,7 @@ test.afterAll( async ( { restApi } ) => {
 } );

 test(
-	'confirms correct summary numbers on overview page',
+	'renders the overview performance indicators',
 	{
 		tag: [ tags.PAYMENTS, tags.SERVICES ],
 	},
@@ -314,31 +375,28 @@ test(
 			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
 		);

-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Total sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Products sold 110 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Variations Sold 40 No change from Previous year:',
-			} )
-		).toBeVisible();
+		// The overview performance tiles are store-wide and cannot be scoped to
+		// this spec's data, so any exact total asserted here would be polluted
+		// by orders that earlier specs in this serial run leave in the shared
+		// store. Assert only that the performance section renders; the exact
+		// figures are verified on the product-scoped report tests below.
+		//
+		// Require a value (a digit or `$`) after the label so this matches the
+		// rendered tile ("Total sales $1,229.30 …") and not a bare-label
+		// `menuitem` an indicator picker might also contribute to the DOM.
+		for ( const label of [
+			'Total sales',
+			'Net sales',
+			'Orders',
+			'Products sold',
+			'Variations Sold',
+		] ) {
+			await expect(
+				page.getByRole( 'menuitem', {
+					name: new RegExp( `^${ escapeRegExp( label ) } [\\d$]` ),
+				} )
+			).toBeVisible();
+		}
 	}
 );

@@ -375,72 +433,34 @@ test(
 );

 test(
-	'use date filter on overview page',
+	'use date filter on products report',
 	{
 		tag: [ tags.PAYMENTS, tags.SERVICES ],
 	},
 	async ( { page } ) => {
+		// Scope the report to this spec's variable product so cumulative store
+		// state from earlier serial specs can't affect the totals. The date
+		// filter itself is a shared component, so exercising it here covers the
+		// same behaviour the overview page used to, without store-wide numbers.
+		const variableProductId = productIds[ productIds.length - 1 ];
 		await page.goto(
-			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
+			`wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Fproducts&filter=single_product&products=${ variableProductId }`
 		);

-		// assert that current month is shown and that values are for that
+		// Default range (month to date) reflects this spec's variable-product sales.
 		await expect( page.getByText( 'Month to date' ).first() ).toBeVisible();
+		await expect( summaryTile( page, 'Items sold', '40' ) ).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Total sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Products sold 110 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Variations Sold 40 No change from Previous year:',
-			} )
+			summaryTile( page, 'Net sales', '$260.00' )
 		).toBeVisible();

-		// click the date filter and change to Last month (should be no sales/orders)
+		// Last month predates this spec's (and every other spec's) orders, so the
+		// totals are a stable zero regardless of what else the store contains.
 		await page.getByRole( 'button', { name: 'Month to date' } ).click();
 		await page.getByText( 'Last month' ).click();
 		await page.getByRole( 'button', { name: 'Update' } ).click();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Total sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 0 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Products sold 0 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Variations Sold 0 No change from Previous year:',
-			} )
-		).toBeVisible();
+		await expect( summaryTile( page, 'Items sold', '0' ) ).toBeVisible();
+		await expect( summaryTile( page, 'Net sales', '$0.00' ) ).toBeVisible();
 	}
 );

@@ -454,45 +474,12 @@ test(
 			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue'
 		);

-		// assert that current month is shown and that values are for that
 		await expect( page.getByText( 'Month to date' ).first() ).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Gross sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Returns $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Coupons $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Taxes $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Shipping $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Total sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();

-		// click the date filter and change to custom date range (should be no sales/orders)
+		// The revenue report is store-wide (there is no product scope for it), so
+		// only assert on a date range that predates every spec's orders, where the
+		// totals are a stable zero regardless of what else the shared serial store
+		// contains. This still exercises the custom-date-range UI end to end.
 		await page.getByRole( 'button', { name: 'Month to date' } ).click();
 		await page.getByText( 'Custom', { exact: true } ).click();
 		await page
@@ -502,140 +489,50 @@ test(
 		await page.getByPlaceholder( 'mm/dd/yyyy' ).last().fill( '01/30/2022' );
 		await page.getByRole( 'button', { name: 'Update' } ).click();

-		// assert values updated
 		await expect(
 			page.getByRole( 'button', {
 				name: 'Custom (Jan 1 - 30, 2022) vs. Previous year (Jan 1 - 30, 2021)',
 			} )
 		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Gross sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Returns $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Coupons $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Taxes $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Shipping $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Total sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
+		for ( const label of [
+			'Gross sales',
+			'Returns',
+			'Coupons',
+			'Net sales',
+			'Taxes',
+			'Shipping',
+			'Total sales',
+		] ) {
+			await expect( summaryTile( page, label, '$0.00' ) ).toBeVisible();
+		}
 	}
 );

 test(
-	'use advanced filters on orders report',
+	'scope orders report via advanced product filter',
 	{
 		tag: [ tags.PAYMENTS, tags.SERVICES ],
 	},
 	async ( { page } ) => {
+		// Scope the orders report to this spec's data with the product advanced
+		// filter: every one of this spec's 10 orders contains productIds[0] and no
+		// other order in the shared store does, so the totals below are isolated
+		// from orders left behind by earlier serial specs. Pre-applying the filter
+		// via the URL also exercises the report's advanced-filter query path
+		// without the flaky multi-step filter-builder clicks.
 		await page.goto(
-			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Forders'
+			`wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Forders&filter=advanced&product_includes=${ productIds[ 0 ] }`
 		);

-		// no filters applied
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average order value $122.93 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average items per order 11 No change from Previous year:',
-			} )
-		).toBeVisible();
-
-		// apply some filters
-		await page.getByRole( 'button', { name: 'All orders' } ).click();
-		await page.getByText( 'Advanced filters' ).click();
-
-		await page.getByRole( 'button', { name: 'Add a filter' } ).click();
-		await page.getByRole( 'button', { name: 'Order status' } ).click();
-		await page
-			.getByLabel( 'Select an order status filter match' )
-			.selectOption( 'Is' );
-		await page
-			.getByLabel( 'Select an order status', { exact: true } )
-			.selectOption( 'Failed' );
-		await page.getByRole( 'link', { name: 'Filter', exact: true } ).click();
-
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 0 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average order value $0.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average items per order 0 No change from Previous year:',
-			} )
-		).toBeVisible();
-
-		await page
-			.getByLabel( 'Select an order status', { exact: true } )
-			.selectOption( 'Completed' );
-		await page.getByRole( 'link', { name: 'Filter', exact: true } ).click();
+		await expect( summaryTile( page, 'Orders', '10' ) ).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
+			summaryTile( page, 'Net sales', '$1,229.30' )
 		).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
+			summaryTile( page, 'Average order value', '$122.93' )
 		).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average order value $122.93 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Average items per order 11 No change from Previous year:',
-			} )
+			summaryTile( page, 'Average items per order', '11' )
 		).toBeVisible();
 	}
 );
@@ -646,68 +543,22 @@ test(
 		tag: [ tags.PAYMENTS, tags.SERVICES ],
 	},
 	async ( { page } ) => {
+		// Land already scoped to this spec's variable product. There is no
+		// store-wide "no filters" block to assert, so cumulative orders from
+		// earlier serial specs can't pollute these figures.
+		const variableProductId = productIds[ productIds.length - 1 ];
 		await page.goto(
-			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Fproducts'
+			`wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Fproducts&filter=single_product&products=${ variableProductId }&is-variable=1`
 		);

-		// no filters applied
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Items sold 110 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $1,229.30 No change from Previous year:',
-			} )
-		).toBeVisible();
+		await expect( summaryTile( page, 'Items sold', '40' ) ).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
-		).toBeVisible();
-
-		// apply some filters
-		await page.getByRole( 'button', { name: 'All products' } ).click();
-		await page.getByText( 'Single product' ).click();
-
-		// Search for single product
-		await page
-			.getByPlaceholder( 'Type to search for a product' )
-			.last()
-			.fill( 'Variable Product' );
-
-		await page
-			.getByRole( 'option', {
-				name: 'Variable Product',
-				exact: true,
-			} )
-			.click();
-
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Items sold 40 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $260.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
+			summaryTile( page, 'Net sales', '$260.00' )
 		).toBeVisible();
+		await expect( summaryTile( page, 'Orders', '10' ) ).toBeVisible();

+		// Drill into a single variation via the report's variation filter UI.
 		await page.getByText( 'All variations' ).click();
-
-		// Search for single product
 		await expect(
 			page.getByRole( 'button', { name: 'Single variation' } )
 		).toBeVisible();
@@ -721,21 +572,11 @@ test(
 			.getByRole( 'option', { name: 'Variable Product - Blue' } )
 			.click();

+		await expect( summaryTile( page, 'Items sold', '30' ) ).toBeVisible();
 		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Items sold 30 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Net sales $180.00 No change from Previous year:',
-			} )
-		).toBeVisible();
-		await expect(
-			page.getByRole( 'menuitem', {
-				name: 'Orders 10 No change from Previous year:',
-			} )
+			summaryTile( page, 'Net sales', '$180.00' )
 		).toBeVisible();
+		await expect( summaryTile( page, 'Orders', '10' ) ).toBeVisible();
 	}
 );