Commit 40dcbf3c74f for woocommerce

commit 40dcbf3c74f287333d99eb19716ae06a305e4015
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Oct 12 14:45:21 2026 +0300

    [tests] Move Analytics E2E assertions to Jest and consolidate 19 browser titles into 10 (#68626)

    * test(analytics): Move Analytics report matrices below E2E

    Three Analytics specs ran browser titles over the overview tiles,
    the report filters, the settings screen and the dashboard sections.
    Most of them loaded a report and read one number or one rendered
    control back, against a store whose totals other specs also write.

    What a filter puts in the report query, what the settings screen
    writes and confirms, and how the dashboard merges and reorders its
    sections are all decided in JavaScript. Move them there: the filter
    picker and the report filters gain Jest coverage in the shared
    components package, the report query builder in the data package,
    and the settings and dashboard behavior in the admin client. Two
    PHPUnit data-store tests take the report queries themselves.

    Keep the browser titles that prove the assembled screens, and add
    three that would otherwise lose their last browser coverage: the
    custom date range, the Orders report's advanced product filter, and
    the import status bar with its manual trigger.

    Consolidates the mega-branch slices:
    - Slice 058: test(analytics): Move report behavior below E2E
    - Slice 059: test(analytics): Move overview preferences below E2E
    - Slice 094: test(analytics): Consolidate import mode coverage
    - test: Support strict SQL report fixtures, which hardens the two
      report data-store tests the same work depends on

    Refs TESTOPS-288
    Refs #68046

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(analytics): Add three Analytics browser canaries

    Moving the Analytics report behavior below the browser takes the last
    titles that drove three screens end to end: the custom date range on a
    report, the Orders report scoped by the advanced product filter, and
    the import status bar with its manual trigger.

    Add one title for each, trimmed from the title the migration removes.
    The custom range types both dates into the real picker and reads back
    the comparison label the report renders. The advanced filter opens the
    Orders report with the filter already in the URL and reads two summary
    tiles only this spec's own orders can produce. The import status bar
    switches the store into scheduled mode, drains the Action Scheduler
    queue the way this spec's data setup already does, and proves the bar
    and its trigger reach the merchant.

    Refs TESTOPS-288
    Refs #68046

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(analytics): Cover the import trigger and the indicators endpoint

    Two demoted behaviours were left without an owner that could fail.

    Nothing fired a real POST to /wc-analytics/imports/trigger.
    import-status-bar.test.tsx mocks ../use-import-status, so its click
    reaches a jest.fn(), and its busy case is handed isTriggeringImport as a
    prop rather than reaching that state as a consequence of the click.
    Extend the scheduled-mode canary to click the real button, wait for the
    real response, and require the busy button to be disabled.

    GET /wc-analytics/reports/performance-indicators lost its only browser
    owner. The Jest replacement mocks @woocommerce/date wholesale and answers
    from a fixture keyed on its own appendTimestamp stub, so a renamed
    endpoint or a broken date boundary cannot turn it red. Add a short
    Overview canary that waits for that endpoint and reads one tile with a
    value beside its label, rather than a store-wide figure the shared store
    would make flaky.

    Proved by mutation, each one surgical. Forcing a 500 from
    AnalyticsImports::trigger_import fails only the trigger title; forcing one
    from PerformanceIndicators\Controller::get_items fails only the
    indicators title. Every sibling stayed green in both runs.

    Also record, at the top of customizable-validation.js, that it is trunk
    customizable.js moved here whole and otherwise unchanged.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(analytics): Name the import status bar title after its trigger click

    The Analytics import status bar title reads "shows the import status bar
    and its manual trigger in scheduled mode", which promises only that the
    bar and its button render.

    The test does more than that. It clicks the trigger, waits for the POST
    to /wc-analytics/imports/trigger, and asserts the button flips to a
    disabled "Analytics data import in progress". That click is the part no
    lower layer owns, and the title hides it, so a reader deciding what the
    browser suite still covers reads the title and gets the wrong answer.

    Rename it to "shows the import status bar and runs its manual trigger in
    scheduled mode".

    Refs TESTOPS-288

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * test(analytics): Assert the default Overview indicators and every Revenue tile in the browser

    Two Analytics browser titles assert one number where the screen shows
    several.

    "loads the overview performance indicators from their own endpoint"
    waits for the performance-indicators response and then checks a single
    Orders tile, so the other four default indicators can stop rendering
    without turning it red. "set custom date range on revenue report" drives
    the custom-range UI and then checks Net sales alone, leaving the other
    six Revenue summary tiles unowned in the browser.

    Assert the whole default set in each: the five Overview indicators
    (Total sales, Net sales, Orders, Products sold, Variations Sold) and the
    seven Revenue tiles (Gross sales, Returns, Coupons, Net sales, Taxes,
    Shipping, Total sales). Both loops match a label followed by a value, so
    they read the rendered tile rather than a bare label the indicator
    picker also puts in the DOM, and both run on the page load the title
    already performs.

    Refs TESTOPS-288

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * test(admin): Assert the add-back button title for a hidden dashboard section

    The dashboard's "Add more sections" panel labels each button with the
    bare section title, so the tooltip is the only place it says what
    clicking does. Nothing asserts that tooltip: customizable.js finds the
    button by its accessible name ("Performance") and clicks it, so the
    title attribute could say anything, or nothing, and the suite stays
    green.

    Assert the title reads "Add Performance section" before clicking it, in
    the title that already adds the hidden section back.

    Refs TESTOPS-288

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * test(analytics): Move the long-status fixture fix to its own PR

    The two report data-store tests carried a change to how their
    long-custom-status fixture is built. That is a fixture fix, unrelated
    to moving the Analytics matrices below E2E, and review asked for it
    separately.

    Restore both files to their trunk state here. The fixture work
    continues in #68921, which also covers the HPOS path the version on
    this branch left alone.

    Refs TESTOPS-288
    Refs #68046

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(e2e): Match Analytics responses on their exact date range

    The date filter title listened for any products/stats response for
    the product. After Update the page sends three such requests (the
    selected period's summary, the previous-year summary and the chart),
    so the check could land on the previous-year one and still pass by
    deriving the expected month from it. A Month to date chart response
    still in flight could also be picked up and fail the before check.

    Compute last month's start and end in UTC before clicking Update and
    only accept a response carrying exactly that after/before. The e2e
    store keeps WordPress's default UTC timezone.

    The performance indicators title claimed to catch a broken date
    boundary but only asserted labels with some value. It now matches
    the default Month to date range on after/before, so a wrong boundary
    times out the response wait.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): Keep the Orders report average tiles in the browser

    The advanced product filter title dropped the Average order value and
    Average items per order assertions as arithmetic over the two tiles
    it kept. They were the only check that those tiles render on the
    Orders report, so removing them from the report config would have
    gone unnoticed. Restore both.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * style: Reformat a ternary in the dashboard preferences test

    Trunk's wp-prettier upgrade (#69365) changed how a closing brace in a
    multi-line ternary is indented, and ESLint's prettier rule now
    reports it as an error on this file, which CI lints as changed.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test: Merge the paired dashboard section preference tests

    Each of the four section actions (move down, move up, remove, add
    back) had two tests that rendered and acted identically and differed
    only in whether they asserted the saved dashboard_sections payload or
    the rendered heading order. Merge each pair into one test that checks
    both, keeping the "...and saves..." names. The add-back test keeps its
    tooltip check before the click.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): Match the indicator tiles' own Month to date request

    The previous commit matched the performance-indicators request on a
    before of today at 23:59:59. The tiles actually send before as the
    current time when the range ends today. A later re-render asks again
    up to 23:59:59, and the exact match was catching that second request,
    so the title passed without checking the tiles' own range.

    Require after at the first of the month and a before that falls
    today, no earlier than the moment the test started. That matches the
    tiles' first request, and a before cut off at the start of today
    still times the title out (checked by mutating the indicator query to
    use the start of day).

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): Wait for the section layout save, not the tour dismissal

    Trunk's Analytics Overview metrics tour (#69097) marks itself as seen
    through the same user endpoint the dashboard uses for its section
    layout. This spec creates its own administrator, so that user sees the
    tour: opening the ellipsis menu writes the tour preference, and moving
    a section writes the layout a moment later.

    The save helper matched any non-GET request to the user endpoint, so it
    resolved on the tour write. The test then reloaded while the layout
    save was still in flight, the reloaded page was rendered from a user
    record that did not hold the new order yet, and the sections came back
    in the default order. All three Core parallel shards failed on this.

    Make the helper match only the write that carries `dashboard_sections`,
    and mark the tour as seen for this spec's administrator, as trunk's
    site setup does for the shared admin. Either change fixes the failure
    on its own; both keep this spec about section order and independent of
    the tour, which has its own spec.

    Refs TESTOPS-288

    Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git a/packages/js/components/changelog/testops-288-analytics-reports b/packages/js/components/changelog/testops-288-analytics-reports
new file mode 100644
index 00000000000..658e6c4559d
--- /dev/null
+++ b/packages/js/components/changelog/testops-288-analytics-reports
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move Analytics report, settings and dashboard matrices below E2E; three browser titles are added for behaviors that would otherwise lose their last coverage.
+
diff --git a/packages/js/components/src/filter-picker/test/index.js b/packages/js/components/src/filter-picker/test/index.js
index c3bac7db0a5..aee88912505 100644
--- a/packages/js/components/src/filter-picker/test/index.js
+++ b/packages/js/components/src/filter-picker/test/index.js
@@ -1,10 +1,11 @@
 /**
  * External dependencies
  */
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { createElement } from '@wordpress/element';
 import { logged } from '@wordpress/deprecated';
+import { updateQueryString } from '@woocommerce/navigation';

 /**
  * Internal dependencies
@@ -15,7 +16,18 @@ import Search from '../../search';
 import productAutocompleter from '../../search/autocompleters/product';
 // Due to Jest implementation we cannot mock it only for specific tests.
 // If your test requires non-mocked Search, move them to another test file.
-jest.mock( '../../search' );
+jest.mock( '../../search', () => ( {
+	__esModule: true,
+	default: jest.fn(),
+} ) );
+jest.mock( '../../search/autocompleters/product', () => ( {
+	__esModule: true,
+	default: { name: 'products' },
+} ) );
+jest.mock( '@woocommerce/navigation', () => ( {
+	...jest.requireActual( '@woocommerce/navigation' ),
+	updateQueryString: jest.fn(),
+} ) );

 describe( 'FilterPicker', () => {
 	it( 'should render the example from the storybook', async () => {
@@ -238,4 +250,206 @@ describe( 'FilterPicker', () => {
 			expect( allParams.includes( 'param_2' ) ).toBeTruthy();
 		} );
 	} );
+
+	describe( 'search selection query updates', () => {
+		const advancedFilters = {
+			filters: {
+				product: {
+					rules: [ { value: 'includes' }, { value: 'excludes' } ],
+				},
+			},
+		};
+
+		const productFilter = {
+			component: 'Search',
+			label: 'Single product',
+			value: 'single_product',
+			settings: {
+				type: 'products',
+				param: 'products',
+				getLabels: () =>
+					Promise.resolve( [ { key: 999, label: 'Old product' } ] ),
+				labels: {
+					placeholder: 'Type to search for a product',
+					button: 'Single product',
+				},
+			},
+		};
+
+		const variationFilter = {
+			component: 'Search',
+			label: 'Single variation',
+			value: 'single_variation',
+			settings: {
+				type: 'variations',
+				param: 'variations',
+				getLabels: () =>
+					Promise.resolve( [ { key: 999, label: 'Old variation' } ] ),
+				labels: {
+					placeholder: 'Type to search for a variation',
+					button: 'Single variation',
+				},
+			},
+		};
+
+		beforeEach( () => {
+			jest.clearAllMocks();
+			Search.mockImplementation( ( { onChange, selected, type } ) => {
+				const choice =
+					type === 'products'
+						? { key: 101, label: 'Product 101' }
+						: { key: 202, label: 'Variation 202' };
+				return (
+					<div>
+						{ selected.map( ( tag ) => (
+							<span key={ tag.key }>{ tag.label }</span>
+						) ) }
+						<button onClick={ () => onChange( [ choice ] ) }>
+							Choose { choice.label }
+						</button>
+					</div>
+				);
+			} );
+		} );
+
+		it( 'selects a product, preserves static parameters, and clears stale filters', async () => {
+			const onFilterSelect = jest.fn();
+			const query = {
+				period: 'last_month',
+				compare: 'previous_year',
+				filter: 'single_product',
+				products: 999,
+				variations: 404,
+				product_includes: 303,
+				product_excludes: 505,
+			};
+			const config = {
+				label: 'Show',
+				staticParams: [ 'period', 'compare' ],
+				param: 'filter',
+				showFilters: () => true,
+				filters: [
+					{ label: 'All products', value: 'all' },
+					productFilter,
+					variationFilter,
+				],
+			};
+
+			render(
+				<FilterPicker
+					advancedFilters={ advancedFilters }
+					config={ config }
+					onFilterSelect={ onFilterSelect }
+					path="/analytics/products"
+					query={ query }
+				/>
+			);
+
+			const persistedButton = await screen.findByRole( 'button', {
+				name: /Old product.*Single product/,
+			} );
+			await userEvent.click( persistedButton );
+			await userEvent.click(
+				screen.getByRole( 'button', { name: 'Choose Product 101' } )
+			);
+
+			// `product_excludes` is deliberately absent: an advanced filter that
+			// does not allow multiples is reset through its first matching rule
+			// only, so `includes` is cleared and `excludes` is left as it was.
+			const expectedUpdate = {
+				filter: 'single_product',
+				products: 101,
+				period: 'last_month',
+				compare: 'previous_year',
+				variations: undefined,
+				product_includes: undefined,
+			};
+			expect( updateQueryString ).toHaveBeenCalledTimes( 1 );
+			// Read the recorded call back and compare it strictly.
+			// toHaveBeenCalledWith equates a key set to undefined with an
+			// absent one, and the cleared filter params are the behavior
+			// under test: dropping them would leave the assertion green.
+			expect( updateQueryString.mock.calls[ 0 ] ).toStrictEqual( [
+				expectedUpdate,
+				'/analytics/products',
+				query,
+			] );
+			expect( onFilterSelect ).toHaveBeenCalledTimes( 1 );
+			expect( onFilterSelect.mock.calls[ 0 ] ).toStrictEqual( [
+				expectedUpdate,
+			] );
+			expect(
+				screen.getByRole( 'button', {
+					name: /Product 101.*Single product/,
+				} )
+			).toBeInTheDocument();
+		} );
+
+		it( 'selects a variation and preserves its product and date scope', async () => {
+			const onFilterSelect = jest.fn();
+			const query = {
+				period: 'last_month',
+				compare: 'previous_year',
+				filter: 'single_product',
+				products: 101,
+				'filter-variations': 'single_variation',
+				variations: 999,
+			};
+			const config = {
+				label: 'Variation',
+				staticParams: [ 'filter', 'products', 'period', 'compare' ],
+				param: 'filter-variations',
+				showFilters: () => true,
+				filters: [
+					{ label: 'All variations', value: 'all' },
+					productFilter,
+					variationFilter,
+				],
+			};
+
+			render(
+				<FilterPicker
+					advancedFilters={ { filters: {} } }
+					config={ config }
+					onFilterSelect={ onFilterSelect }
+					path="/analytics/products"
+					query={ query }
+				/>
+			);
+
+			const persistedButton = await screen.findByRole( 'button', {
+				name: /Old variation.*Single variation/,
+			} );
+			await userEvent.click( persistedButton );
+			await userEvent.click(
+				screen.getByRole( 'button', {
+					name: 'Choose Variation 202',
+				} )
+			);
+
+			const expectedUpdate = {
+				'filter-variations': 'single_variation',
+				variations: 202,
+				filter: 'single_product',
+				products: 101,
+				period: 'last_month',
+				compare: 'previous_year',
+			};
+			expect( updateQueryString ).toHaveBeenCalledTimes( 1 );
+			expect( updateQueryString.mock.calls[ 0 ] ).toStrictEqual( [
+				expectedUpdate,
+				'/analytics/products',
+				query,
+			] );
+			expect( onFilterSelect ).toHaveBeenCalledTimes( 1 );
+			expect( onFilterSelect.mock.calls[ 0 ] ).toStrictEqual( [
+				expectedUpdate,
+			] );
+			expect(
+				screen.getByRole( 'button', {
+					name: /Variation 202.*Single variation/,
+				} )
+			).toBeInTheDocument();
+		} );
+	} );
 } );
diff --git a/packages/js/components/src/filters/test/index.js b/packages/js/components/src/filters/test/index.js
new file mode 100644
index 00000000000..ea0e8f89ff7
--- /dev/null
+++ b/packages/js/components/src/filters/test/index.js
@@ -0,0 +1,146 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { updateQueryString } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import ReportFilters from '../';
+
+jest.mock( '@woocommerce/navigation', () => ( {
+	...jest.requireActual( '@woocommerce/navigation' ),
+	updateQueryString: jest.fn(),
+} ) );
+
+jest.mock( '../../advanced-filters', () => () => null );
+jest.mock( '../../compare-filter', () => ( {
+	CompareFilter: () => null,
+} ) );
+jest.mock( '../../filter-picker', () => () => null );
+
+const path = '/analytics/revenue';
+const query = {
+	period: 'today',
+	compare: 'previous_year',
+	filter: 'all',
+};
+
+const dateQuery = {
+	period: 'today',
+	compare: 'previous_year',
+	after: null,
+	before: null,
+	primaryDate: {
+		label: 'Today',
+		range: 'February 15, 2022',
+	},
+	secondaryDate: {
+		label: 'Previous year',
+		range: 'February 15, 2021',
+	},
+};
+
+const renderFilters = ( onDateSelect ) =>
+	render(
+		<ReportFilters
+			dateQuery={ dateQuery }
+			filters={ [] }
+			isoDateFormat="YYYY-MM-DD"
+			onDateSelect={ onDateSelect }
+			path={ path }
+			query={ query }
+		/>
+	);
+
+const openDatePicker = async () => {
+	await userEvent.click(
+		screen.getByRole( 'button', {
+			name: /Today \(February 15, 2022\).*Previous year/,
+		} )
+	);
+};
+
+describe( 'ReportFilters date selection', () => {
+	beforeEach( () => {
+		jest.useFakeTimers();
+		jest.setSystemTime( new Date( '2022-02-15T12:00:00Z' ) );
+		jest.clearAllMocks();
+	} );
+
+	afterEach( () => {
+		jest.useRealTimers();
+	} );
+
+	it( 'maps the Last month preset to navigation and the selection callback', async () => {
+		const onDateSelect = jest.fn();
+		renderFilters( onDateSelect );
+		await openDatePicker();
+
+		await userEvent.click(
+			screen.getByRole( 'radio', { name: 'Last month' } )
+		);
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Update' } )
+		);
+
+		const expectedSelection = {
+			period: 'last_month',
+			compare: 'previous_year',
+			after: undefined,
+			before: undefined,
+		};
+		expect( updateQueryString ).toHaveBeenCalledTimes( 1 );
+		// Read the recorded call back and compare it strictly.
+		// toHaveBeenCalledWith equates a key set to undefined with an absent
+		// one, and clearing `after` and `before` is exactly what choosing a
+		// preset does: dropping it would leave the assertion green.
+		expect( updateQueryString.mock.calls[ 0 ] ).toStrictEqual( [
+			expectedSelection,
+			path,
+			query,
+		] );
+		expect( onDateSelect ).toHaveBeenCalledTimes( 1 );
+		expect( onDateSelect.mock.calls[ 0 ] ).toStrictEqual( [
+			expectedSelection,
+		] );
+	} );
+
+	it( 'maps a custom date range to navigation and the selection callback', async () => {
+		const onDateSelect = jest.fn();
+		renderFilters( onDateSelect );
+		await openDatePicker();
+
+		await userEvent.click( screen.getByRole( 'tab', { name: 'Custom' } ) );
+		await userEvent.type(
+			screen.getByRole( 'textbox', { name: 'Start Date' } ),
+			'01/01/2022'
+		);
+		await userEvent.type(
+			screen.getByRole( 'textbox', { name: 'End Date' } ),
+			'01/30/2022'
+		);
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Update' } )
+		);
+
+		const expectedSelection = {
+			period: 'custom',
+			compare: 'previous_year',
+			after: '2022-01-01',
+			before: '2022-01-30',
+		};
+		expect( updateQueryString ).toHaveBeenCalledTimes( 1 );
+		expect( updateQueryString.mock.calls[ 0 ] ).toStrictEqual( [
+			expectedSelection,
+			path,
+			query,
+		] );
+		expect( onDateSelect ).toHaveBeenCalledTimes( 1 );
+		expect( onDateSelect.mock.calls[ 0 ] ).toStrictEqual( [
+			expectedSelection,
+		] );
+	} );
+} );
diff --git a/packages/js/data/changelog/testops-288-analytics-reports b/packages/js/data/changelog/testops-288-analytics-reports
new file mode 100644
index 00000000000..658e6c4559d
--- /dev/null
+++ b/packages/js/data/changelog/testops-288-analytics-reports
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move Analytics report, settings and dashboard matrices below E2E; three browser titles are added for behaviors that would otherwise lose their last coverage.
+
diff --git a/packages/js/data/src/reports/test/utils.ts b/packages/js/data/src/reports/test/utils.ts
index c3c145154b0..b4bfca2e27b 100644
--- a/packages/js/data/src/reports/test/utils.ts
+++ b/packages/js/data/src/reports/test/utils.ts
@@ -9,6 +9,7 @@ import { addFilter, removeFilter } from '@wordpress/hooks';
 import {
 	getFilterQuery,
 	getReportChartData,
+	getRequestQuery,
 	usesServerSideSearch,
 } from '../utils';

@@ -279,3 +280,163 @@ describe( 'getReportChartData()', () => {
 		} );
 	} );
 } );
+
+type QueryOptions = Parameters< typeof getRequestQuery >[ 0 ];
+
+const fields = [ 'orders_count', 'items_sold' ];
+
+const productFilter = {
+	param: 'filter',
+	filters: [
+		{ value: 'all' },
+		{
+			value: 'select_product',
+			subFilters: [
+				{
+					value: 'single_product',
+					settings: { param: 'products' },
+				},
+			],
+		},
+		{ value: 'advanced' },
+	],
+};
+
+const variationFilter = {
+	param: 'filter-variations',
+	filters: [
+		{ value: 'all' },
+		{
+			value: 'select_variation',
+			subFilters: [
+				{
+					value: 'single_variation',
+					settings: { param: 'variations' },
+				},
+			],
+		},
+	],
+};
+
+const advancedFilters = {
+	filters: {
+		product: {
+			rules: [ { value: 'includes' }, { value: 'excludes' } ],
+			input: { component: 'Search' },
+		},
+	},
+};
+
+const createOptions = ( query: Record< string, string > ): QueryOptions =>
+	( {
+		endpoint: 'products',
+		dataType: 'primary',
+		query,
+		limitBy: [ 'products', 'variations' ],
+		filters: [ productFilter, variationFilter ],
+		advancedFilters,
+		defaultDateRange: 'period=month&compare=previous_year',
+		tableQuery: {},
+		fields,
+		selector: {} as QueryOptions[ 'selector' ],
+		select: jest.fn(),
+	} ) as QueryOptions;
+
+describe( 'report request query mapping', () => {
+	beforeAll( () => {
+		jest.useFakeTimers();
+		jest.setSystemTime( new Date( '2022-02-15T12:00:00Z' ) );
+	} );
+
+	afterAll( () => {
+		jest.useRealTimers();
+	} );
+
+	it( 'maps report filter queries into REST requests', () => {
+		const cases = [
+			{
+				name: 'last month',
+				query: {
+					period: 'last_month',
+					compare: 'previous_year',
+				},
+				filterQuery: {},
+				after: '2022-01-01T00:00:00',
+				before: '2022-01-31T23:59:59',
+			},
+			{
+				name: 'custom range',
+				query: {
+					period: 'custom',
+					compare: 'previous_year',
+					after: '2022-01-01',
+					before: '2022-01-30',
+				},
+				filterQuery: {},
+				after: '2022-01-01T00:00:00',
+				before: '2022-01-30T23:59:59',
+			},
+			{
+				name: 'advanced product include',
+				query: {
+					period: 'last_month',
+					compare: 'previous_year',
+					filter: 'advanced',
+					product_includes: '101',
+				},
+				filterQuery: { match: 'all', product_includes: '101' },
+				after: '2022-01-01T00:00:00',
+				before: '2022-01-31T23:59:59',
+			},
+			{
+				name: 'single product',
+				query: {
+					period: 'last_month',
+					compare: 'previous_year',
+					filter: 'single_product',
+					products: '101',
+				},
+				filterQuery: { products: '101' },
+				after: '2022-01-01T00:00:00',
+				before: '2022-01-31T23:59:59',
+			},
+			{
+				name: 'single variation',
+				query: {
+					period: 'last_month',
+					compare: 'previous_year',
+					'filter-variations': 'single_variation',
+					variations: '202',
+				},
+				filterQuery: { variations: '202' },
+				after: '2022-01-01T00:00:00',
+				before: '2022-01-31T23:59:59',
+			},
+		];
+
+		cases.forEach( ( testCase ) => {
+			const options = createOptions( testCase.query );
+			const actualFilterQuery = getFilterQuery( options );
+			const requestQuery = getRequestQuery( options );
+
+			expect( {
+				name: testCase.name,
+				filterQuery: actualFilterQuery,
+				requestQuery,
+			} ).toEqual( {
+				name: testCase.name,
+				filterQuery: testCase.filterQuery,
+				requestQuery: {
+					order: 'asc',
+					interval: 'day',
+					per_page: 100,
+					after: testCase.after,
+					before: testCase.before,
+					segmentby: undefined,
+					fields,
+					...testCase.filterQuery,
+				},
+			} );
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/changelog/testops-288-analytics-reports b/plugins/woocommerce/changelog/testops-288-analytics-reports
new file mode 100644
index 00000000000..658e6c4559d
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-analytics-reports
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move Analytics report, settings and dashboard matrices below E2E; three browser titles are added for behaviors that would otherwise lose their last coverage.
+
diff --git a/plugins/woocommerce/client/admin/client/analytics/settings/test/date-type.test.js b/plugins/woocommerce/client/admin/client/analytics/settings/test/date-type.test.js
index dd4f4aed62b..d121f3de69c 100644
--- a/plugins/woocommerce/client/admin/client/analytics/settings/test/date-type.test.js
+++ b/plugins/woocommerce/client/admin/client/analytics/settings/test/date-type.test.js
@@ -2,7 +2,10 @@
  * External dependencies
  */
 import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import { useSettings } from '@woocommerce/data';
+import { useState } from '@wordpress/element';
+import { recordEvent } from '@woocommerce/tracks';

 /**
  * Internal dependencies
@@ -18,6 +21,27 @@ jest.mock( '@woocommerce/tracks', () => ( {
 	recordEvent: jest.fn(),
 } ) );

+// Load only the real component modules used by Analytics Settings. Importing
+// the package barrel eagerly loads unrelated search code whose linked
+// dependency is not collectable in this package's Jest runtime.
+jest.mock( '@woocommerce/components', () => ( {
+	DateRangeFilterPicker: jest.requireActual(
+		'@woocommerce/components/date-range-filter-picker'
+	).default,
+	ScrollTo: jest.requireActual( '@woocommerce/components/scroll-to' ).default,
+	SectionHeader: jest.requireActual(
+		'@woocommerce/components/section-header'
+	).default,
+} ) );
+
+// The linked interpolation package has no resolvable emitted Babel runtime in
+// this isolated Jest workspace. Analytics Settings only needs its mixed text
+// result here; none of the setting controls under test depend on interpolation.
+jest.mock( '@automattic/interpolate-components', () => ( {
+	__esModule: true,
+	default: ( { mixedString } ) => mixedString,
+} ) );
+
 jest.mock( '../historical-data', () => ( {
 	__esModule: true,
 	default: () => <div>Historical Data</div>,
@@ -29,26 +53,61 @@ jest.mock( '../default-date', () => ( {
 } ) );

 describe( 'Analytics settings - date type', () => {
+	const mockPersistSettings = jest.fn();
 	const mockUpdateAndPersistSettings = jest.fn();
+	const mockUpdateSettings = jest.fn();
+	const initialSettings = {
+		woocommerce_excluded_report_order_statuses: [
+			'pending',
+			'cancelled',
+			'failed',
+		],
+		woocommerce_actionable_order_statuses: [ 'processing', 'on-hold' ],
+		woocommerce_default_date_range: 'period=month&compare=previous_year',
+		woocommerce_date_type: 'date_completed',
+		woocommerce_analytics_scheduled_import: 'yes',
+	};
+	let currentSettings;
+	let setHarnessSettings;
+	let navigationQuerySnapshots;
+
+	const SettingsHarness = ( props ) => {
+		const [ settings, setSettings ] = useState( initialSettings );
+		currentSettings = settings;
+		setHarnessSettings = setSettings;
+
+		return <Settings { ...props } />;
+	};

 	beforeEach( () => {
 		jest.clearAllMocks();
+		currentSettings = initialSettings;
+		setHarnessSettings = null;
+		navigationQuerySnapshots = [];
+		window.wpNavMenuUrlUpdate = jest.fn( ( query ) => {
+			navigationQuerySnapshots.push( { ...query } );
+		} );
+		mockUpdateSettings.mockImplementation( ( namespace, settings ) => {
+			currentSettings = settings;
+			if ( setHarnessSettings ) {
+				setHarnessSettings( settings );
+			}
+		} );

-		useSettings.mockReturnValue( {
+		useSettings.mockImplementation( () => ( {
 			settingsError: false,
 			isRequesting: false,
 			isDirty: false,
-			persistSettings: jest.fn(),
+			persistSettings: mockPersistSettings,
 			updateAndPersistSettings: mockUpdateAndPersistSettings,
-			updateSettings: jest.fn(),
-			wcAdminSettings: {
-				woocommerce_date_type: 'date_completed',
-			},
-		} );
+			updateSettings: mockUpdateSettings,
+			wcAdminSettings: currentSettings,
+		} ) );
 	} );

 	afterEach( () => {
 		jest.restoreAllMocks();
+		delete window.wpNavMenuUrlUpdate;
 	} );

 	it( 'defines date_paid as the default value, matching reports behavior', () => {
@@ -79,4 +138,161 @@ describe( 'Analytics settings - date type', () => {
 			} )
 		);
 	} );
+
+	it( 'updates, saves, and resets analytics settings through real controls', async () => {
+		jest.spyOn( window, 'confirm' ).mockReturnValue( true );
+		const query = {
+			period: 'last_month',
+			compare: 'previous_year',
+			before: '2022-01-31',
+			after: '2022-01-01',
+			interval: 'day',
+			type: 'primary',
+			unrelated: 'preserved',
+		};
+
+		render(
+			<SettingsHarness createNotice={ jest.fn() } query={ query } />
+		);
+
+		await userEvent.click(
+			screen.getAllByRole( 'checkbox', { name: 'On hold' } )[ 0 ]
+		);
+		await userEvent.click(
+			screen.getAllByRole( 'checkbox', {
+				name: 'Pending payment',
+			} )[ 1 ]
+		);
+		await userEvent.click(
+			screen.getAllByRole( 'checkbox', { name: 'Failed' } )[ 1 ]
+		);
+
+		await userEvent.selectOptions(
+			screen.getByRole( 'combobox' ),
+			'date_created'
+		);
+
+		const expectedSettings = {
+			...initialSettings,
+			woocommerce_excluded_report_order_statuses: [
+				'pending',
+				'cancelled',
+				'failed',
+				'on-hold',
+			],
+			woocommerce_actionable_order_statuses: [
+				'processing',
+				'on-hold',
+				'pending',
+				'failed',
+			],
+			woocommerce_default_date_range:
+				'period=month&compare=previous_year',
+			woocommerce_date_type: 'date_created',
+		};
+
+		expect( mockUpdateSettings ).toHaveBeenNthCalledWith(
+			1,
+			'wcAdminSettings',
+			{
+				...initialSettings,
+				woocommerce_excluded_report_order_statuses: [
+					'pending',
+					'cancelled',
+					'failed',
+					'on-hold',
+				],
+			}
+		);
+		expect( mockUpdateSettings ).toHaveBeenNthCalledWith(
+			2,
+			'wcAdminSettings',
+			{
+				...initialSettings,
+				woocommerce_excluded_report_order_statuses: [
+					'pending',
+					'cancelled',
+					'failed',
+					'on-hold',
+				],
+				woocommerce_actionable_order_statuses: [
+					'processing',
+					'on-hold',
+					'pending',
+				],
+			}
+		);
+		expect( mockUpdateSettings ).toHaveBeenNthCalledWith(
+			3,
+			'wcAdminSettings',
+			{
+				...expectedSettings,
+				woocommerce_date_type: 'date_completed',
+			}
+		);
+		expect( mockUpdateSettings ).toHaveBeenNthCalledWith(
+			4,
+			'wcAdminSettings',
+			expectedSettings
+		);
+
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Save settings' } )
+		);
+
+		expect( mockPersistSettings ).toHaveBeenCalledTimes( 1 );
+		expect( recordEvent ).toHaveBeenNthCalledWith(
+			1,
+			'analytics_settings_save',
+			expectedSettings
+		);
+		expect( window.wpNavMenuUrlUpdate ).toHaveBeenCalledTimes( 1 );
+		// Strict for consistency with the two filter suites, and because the six
+		// report keys are cleared by being set to undefined rather than deleted,
+		// which a loose comparison would treat as absent.
+		expect( navigationQuerySnapshots ).toStrictEqual( [
+			{
+				period: undefined,
+				compare: undefined,
+				before: undefined,
+				after: undefined,
+				interval: undefined,
+				type: undefined,
+				unrelated: 'preserved',
+			},
+		] );
+
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Reset defaults' } )
+		);
+
+		// Spelled out rather than mapped out of the same config module the
+		// component reads: a derived expectation moves with the config and so
+		// cannot catch a changed default. The key comparison below keeps this
+		// honest if a setting is ever added to the page.
+		const defaultSettings = {
+			woocommerce_excluded_report_order_statuses: [
+				'pending',
+				'cancelled',
+				'failed',
+			],
+			woocommerce_actionable_order_statuses: [ 'processing', 'on-hold' ],
+			woocommerce_default_date_range:
+				'period=month&compare=previous_year',
+			woocommerce_date_type: 'date_paid',
+			woocommerce_analytics_scheduled_import: 'yes',
+		};
+		expect( Object.keys( config ) ).toEqual(
+			Object.keys( defaultSettings )
+		);
+		expect( mockUpdateAndPersistSettings ).toHaveBeenCalledTimes( 1 );
+		expect( mockUpdateAndPersistSettings ).toHaveBeenCalledWith(
+			'wcAdminSettings',
+			defaultSettings
+		);
+		expect( recordEvent ).toHaveBeenNthCalledWith(
+			2,
+			'analytics_settings_reset_defaults'
+		);
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/analytics/settings/test/index.test.js b/plugins/woocommerce/client/admin/client/analytics/settings/test/index.test.js
index 0a68ef0f5f8..77f37514f2e 100644
--- a/plugins/woocommerce/client/admin/client/analytics/settings/test/index.test.js
+++ b/plugins/woocommerce/client/admin/client/analytics/settings/test/index.test.js
@@ -115,6 +115,27 @@ describe( 'Settings - Import Mode Modal', () => {
 		).toBeChecked();
 	} );

+	it( 'shows immediate import mode when the scheduled setting is absent', () => {
+		useSettings.mockReturnValue( {
+			settingsError: false,
+			isRequesting: false,
+			isDirty: false,
+			persistSettings: mockPersistSettings,
+			updateAndPersistSettings: jest.fn(),
+			updateSettings: mockUpdateSettings,
+			wcAdminSettings: {},
+		} );
+
+		render( <Settings createNotice={ jest.fn() } query={ {} } /> );
+
+		expect(
+			screen.getByRole( 'radio', { name: /immediately/i } )
+		).toBeChecked();
+		expect(
+			screen.getByRole( 'radio', { name: /scheduled/i } )
+		).not.toBeChecked();
+	} );
+
 	it( 'shows modal when switching from scheduled to immediate mode', async () => {
 		render( <Settings createNotice={ jest.fn() } query={ {} } /> );

@@ -162,6 +183,9 @@ describe( 'Settings - Import Mode Modal', () => {
 		} );

 		expect( mockUpdateSettings ).not.toHaveBeenCalled();
+		expect(
+			screen.getByRole( 'radio', { name: /scheduled/i } )
+		).toBeChecked();
 	} );

 	it( 'updates setting when modal is confirmed', async () => {
diff --git a/plugins/woocommerce/client/admin/client/dashboard/store-performance/test/index.js b/plugins/woocommerce/client/admin/client/dashboard/store-performance/test/index.js
index 9c7196d2437..4b371a6094d 100644
--- a/plugins/woocommerce/client/admin/client/dashboard/store-performance/test/index.js
+++ b/plugins/woocommerce/client/admin/client/dashboard/store-performance/test/index.js
@@ -2,34 +2,179 @@
  * External dependencies
  */
 import { fireEvent, render, screen } from '@testing-library/react';
+import { reportsStore, settingsStore } from '@woocommerce/data';

 /**
  * Internal dependencies
  */
 import StorePerformance from '../index';

+const primaryData = {
+	data: [
+		{
+			stat: 'revenue/total_sales',
+			chart: 'total_sales',
+			format: 'currency',
+			value: 1234.5,
+			_links: { report: [ { href: '/analytics/revenue' } ] },
+		},
+		{
+			stat: 'revenue/net_revenue',
+			chart: 'net_revenue',
+			format: 'currency',
+			value: 987.65,
+			_links: { report: [ { href: '/analytics/revenue' } ] },
+		},
+		{
+			stat: 'orders/orders_count',
+			chart: 'orders_count',
+			format: 'number',
+			value: 12,
+			_links: { report: [ { href: '/analytics/orders' } ] },
+		},
+		{
+			stat: 'products/items_sold',
+			chart: 'items_sold',
+			format: 'number',
+			value: 34,
+			_links: { report: [ { href: '/analytics/products' } ] },
+		},
+		{
+			stat: 'variations/items_sold',
+			chart: 'variations_sold',
+			format: 'number',
+			value: 56,
+			_links: { report: [ { href: '/analytics/variations' } ] },
+		},
+	],
+};
+
+const secondaryData = {
+	data: primaryData.data.map( ( item ) => ( {
+		...item,
+		value: item.value / 2,
+	} ) ),
+};
+
+const mockGetReportItems = jest.fn( ( _endpoint, query ) => {
+	const response =
+		query.after === 'primary-after-start' ? primaryData : secondaryData;
+	const requestedStats = query.stats.split( ',' );
+
+	return {
+		data: response.data.filter( ( item ) =>
+			requestedStats.includes( item.stat )
+		),
+	};
+} );
+
+const mockSelect = jest.fn( ( store ) => {
+	if ( store === reportsStore ) {
+		return {
+			getReportItems: mockGetReportItems,
+			getReportItemsError: jest.fn().mockReturnValue( null ),
+			isResolving: jest.fn().mockReturnValue( false ),
+		};
+	}
+
+	if ( store === settingsStore ) {
+		return {
+			getSetting: jest.fn().mockReturnValue( {
+				woocommerce_default_date_range:
+					'period=month&compare=previous_year',
+			} ),
+		};
+	}
+
+	throw new Error( 'Unexpected store selection' );
+} );
+
 jest.mock( '@wordpress/data', () => ( {
 	...jest.requireActual( '@wordpress/data' ),
-	withSelect: ( mapSelect ) => ( Component ) => ( props ) => (
-		<Component { ...props } { ...mapSelect( jest.fn(), props ) } />
+	withSelect: ( mapSelect ) => ( WrappedComponent ) => ( props ) => (
+		<WrappedComponent { ...props } { ...mapSelect( mockSelect, props ) } />
 	),
 } ) );

+// Load only the real component modules used by StorePerformance. Importing the
+// package barrel eagerly loads unrelated search code whose linked dependency is
+// not collectable in this package's Jest runtime.
 jest.mock( '@woocommerce/components', () => ( {
-	...jest.requireActual( '@woocommerce/components' ),
-	SectionHeader: ( { title, menu } ) => (
-		<div>
-			{ title }
-			{ menu }
-		</div>
-	),
-	EllipsisMenu: ( { onToggle } ) => (
-		<button onClick={ onToggle }>Open menu</button>
-	),
+	EllipsisMenu: jest.requireActual( '@woocommerce/components/ellipsis-menu' )
+		.default,
+	MenuItem: jest.requireActual(
+		'@woocommerce/components/ellipsis-menu/menu-item'
+	).default,
+	MenuTitle: jest.requireActual(
+		'@woocommerce/components/ellipsis-menu/menu-title'
+	).default,
+	SectionHeader: jest.requireActual(
+		'@woocommerce/components/section-header'
+	).default,
+	SummaryList: jest.requireActual( '@woocommerce/components/summary' )
+		.default,
+	SummaryListPlaceholder: jest.requireActual(
+		'@woocommerce/components/summary/placeholder'
+	).SummaryListPlaceholder,
+	SummaryNumber: jest.requireActual(
+		'@woocommerce/components/summary/number'
+	).default,
+} ) );
+
+jest.mock( '@woocommerce/date', () => ( {
+	...jest.requireActual( '@woocommerce/date' ),
+	appendTimestamp: ( value, position ) => `${ value.key }-${ position }`,
+	getCurrentDates: () => ( {
+		primary: {
+			after: { key: 'primary-after' },
+			before: { key: 'primary-before', isSame: () => false },
+		},
+		secondary: {
+			after: { key: 'secondary-after' },
+			before: { key: 'secondary-before', isSame: () => false },
+		},
+	} ),
+	getDateParamsFromQuery: () => ( { compare: 'previous_year' } ),
+	getStoreTimeZoneMoment: () => ( {} ),
+} ) );
+
+jest.mock( '@woocommerce/navigation', () => ( {
+	...jest.requireActual( '@woocommerce/navigation' ),
+	getNewPath: ( query, path, extras ) =>
+		`${ path }?chart=${ extras.chart }&persisted=${ query.persisted }`,
+	getPersistedQuery: () => ( { persisted: 'yes' } ),
 } ) );

 jest.mock( '~/utils/admin-settings', () => ( {
-	getAdminSetting: () => ( { performanceIndicators: [] } ),
+	getAdminSetting: () => ( {
+		performanceIndicators: [
+			{
+				chart: 'total_sales',
+				label: 'Total sales',
+				stat: 'revenue/total_sales',
+			},
+			{
+				chart: 'net_revenue',
+				label: 'Net sales',
+				stat: 'revenue/net_revenue',
+			},
+			{
+				chart: 'orders_count',
+				label: 'Orders',
+				stat: 'orders/orders_count',
+			},
+			{
+				chart: 'items_sold',
+				label: 'Products sold',
+				stat: 'products/items_sold',
+			},
+			{
+				chart: 'variations_sold',
+				label: 'Variations Sold',
+				stat: 'variations/items_sold',
+			},
+		],
+	} ),
 } ) );

 jest.mock( '~/guided-tours/performance-metrics-tour', () => ( {
@@ -38,14 +183,74 @@ jest.mock( '~/guided-tours/performance-metrics-tour', () => ( {
 	),
 } ) );

-describe( 'StorePerformance tour', () => {
-	it( 'tells the tour when the menu is opened', () => {
-		render( <StorePerformance hiddenBlocks={ [] } query={ {} } /> );
+describe( 'StorePerformance', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+	} );
+
+	it( 'renders all configured performance indicators with accessible values', () => {
+		render(
+			<StorePerformance
+				controls={ () => null }
+				filters={ [] }
+				hiddenBlocks={ [] }
+				query={ {
+					period: 'month',
+					compare: 'previous_year',
+				} }
+				title="Store Performance"
+			/>
+		);
+
+		expect(
+			screen.getByRole( 'menuitem', {
+				name: /^Total sales \$1,234\.50(\s|$)/,
+			} )
+		).toBeInTheDocument();
+		expect( screen.getAllByRole( 'menuitem' ) ).toHaveLength( 5 );
+		expect(
+			screen.getByRole( 'menuitem', {
+				name: /^Net sales \$987\.65(\s|$)/,
+			} )
+		).toBeInTheDocument();
+		expect(
+			screen.getByRole( 'menuitem', { name: /^Orders 12(\s|$)/ } )
+		).toBeInTheDocument();
+		expect(
+			screen.getByRole( 'menuitem', {
+				name: /^Products sold 34(\s|$)/,
+			} )
+		).toBeInTheDocument();
+		expect(
+			screen.getByRole( 'menuitem', {
+				name: /^Variations Sold 56(\s|$)/,
+			} )
+		).toBeInTheDocument();
+	} );
+
+	it( 'tells the tour when the menu is opened', async () => {
+		render(
+			<StorePerformance
+				controls={ () => null }
+				filters={ [] }
+				hiddenBlocks={ [] }
+				query={ {
+					period: 'month',
+					compare: 'previous_year',
+				} }
+				title="Store Performance"
+			/>
+		);

 		expect( screen.getByText( 'Menu not opened' ) ).toBeInTheDocument();

-		fireEvent.click( screen.getByRole( 'button', { name: 'Open menu' } ) );
+		fireEvent.click(
+			screen.getByRole( 'button', {
+				name: 'Choose which analytics to display and the section name',
+			} )
+		);

-		expect( screen.getByText( 'Menu opened' ) ).toBeInTheDocument();
+		// Awaiting lets the popover finish positioning itself after the click.
+		expect( await screen.findByText( 'Menu opened' ) ).toBeInTheDocument();
 	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/dashboard/test/customizable-validation.js b/plugins/woocommerce/client/admin/client/dashboard/test/customizable-validation.js
new file mode 100644
index 00000000000..8c49e046b6a
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/dashboard/test/customizable-validation.js
@@ -0,0 +1,534 @@
+/**
+ * This file is trunk's `customizable.js` moved here whole and otherwise
+ * unchanged, so diffing it against that file shows only the move.
+ */
+
+/**
+ * External dependencies
+ */
+import { fireEvent, render } from '@testing-library/react';
+import { useUserPreferences } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import CustomizableDashboard, {
+	mergeSectionsWithDefaults,
+} from '../customizable';
+
+const DEFAULT_SECTIONS = [
+	{
+		key: 'store-performance',
+		component: () => null,
+		title: 'Performance',
+		isVisible: true,
+		icon: 'arrow-right',
+		hiddenBlocks: [ 'taxes/order_tax' ],
+	},
+	{
+		key: 'charts',
+		component: () => null,
+		title: 'Charts',
+		isVisible: true,
+		icon: 'chart-bar',
+		hiddenBlocks: [ 'coupons_amount' ],
+	},
+];
+
+// Reassigned by the tests that need the `woocommerce_dashboard_default_sections`
+// filter to have returned something else. Read through a getter so the mock is
+// resolved when the dashboard dereferences it, not when the module is imported.
+let mockDefaultSections = DEFAULT_SECTIONS;
+
+jest.mock( '../default-sections', () => ( {
+	__esModule: true,
+	DEFAULT_SECTIONS_FILTER: 'woocommerce_dashboard_default_sections',
+	get default() {
+		return mockDefaultSections;
+	},
+} ) );
+
+jest.mock( '../section', () => ( { title, onRemove, onTitleUpdate } ) => (
+	<div>
+		{ title }
+		<button title={ `Hide ${ title }` } onClick={ onRemove } />
+		<button
+			title={ `Rename ${ title }` }
+			onClick={ () => onTitleUpdate( { rendered: 'Renamed' } ) }
+		/>
+	</div>
+) );
+
+jest.mock( '../../analytics/components/report-header', () => ( {
+	ReportHeader: () => null,
+} ) );
+
+jest.mock( '@woocommerce/data', () => ( {
+	settingsStore: 'wc/admin/settings',
+	useUserPreferences: jest.fn(),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	withSelect: () => ( Component ) => ( props ) => (
+		<Component { ...props } defaultDateRange="period=month" />
+	),
+} ) );
+
+afterEach( () => {
+	mockDefaultSections = DEFAULT_SECTIONS;
+} );
+
+describe( 'mergeSectionsWithDefaults', () => {
+	it( 'returns the defaults when nothing is stored', () => {
+		expect( mergeSectionsWithDefaults( undefined ) ).toHaveLength( 2 );
+		expect( mergeSectionsWithDefaults( '' ) ).toHaveLength( 2 );
+		expect( mergeSectionsWithDefaults( [] ) ).toHaveLength( 2 );
+	} );
+
+	it( 'returns the defaults when the stored preference is not an array', () => {
+		// `[,,]` is not valid JSON, so it reaches the dashboard as a raw string.
+		expect( mergeSectionsWithDefaults( '[,,]' ) ).toHaveLength( 2 );
+		expect( mergeSectionsWithDefaults( {} ) ).toHaveLength( 2 );
+	} );
+
+	it( 'returns the defaults when every stored section is malformed', () => {
+		expect( mergeSectionsWithDefaults( [ null, null ] ) ).toHaveLength( 2 );
+		expect(
+			mergeSectionsWithDefaults( [ undefined, 'charts', 42, {} ] )
+		).toHaveLength( 2 );
+	} );
+
+	it( 'ignores a stored icon, which is a stale React node', () => {
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{ key: 'charts', icon: { props: {} } },
+		] );
+
+		expect( charts.icon ).toBe( 'chart-bar' );
+	} );
+
+	it( 'keeps the well formed sections and drops the malformed ones', () => {
+		const sections = mergeSectionsWithDefaults( [
+			null,
+			{ key: 'charts', title: 'My charts', isVisible: false },
+		] );
+
+		expect( sections ).toHaveLength( 2 );
+		expect( sections[ 0 ] ).toMatchObject( {
+			key: 'charts',
+			title: 'My charts',
+			isVisible: false,
+		} );
+		expect( sections[ 1 ] ).toMatchObject( {
+			key: 'store-performance',
+			title: 'Performance',
+		} );
+	} );
+
+	it( 'drops stored keys that no longer exist', () => {
+		const sections = mergeSectionsWithDefaults( [ { key: 'gone' } ] );
+
+		expect( sections.map( ( section ) => section.key ) ).toEqual( [
+			'store-performance',
+			'charts',
+		] );
+	} );
+
+	it( 'ignores a stored hiddenBlocks that is not an array', () => {
+		// Every section component calls `hiddenBlocks.includes()` on it.
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{ key: 'charts', title: 'My charts', hiddenBlocks: null },
+		] );
+
+		expect( charts.hiddenBlocks ).toEqual( [ 'coupons_amount' ] );
+		expect( charts.title ).toBe( 'My charts' );
+	} );
+
+	it( 'ignores a stored isVisible that is not a boolean', () => {
+		// A non boolean hides the section without listing it under "Add more
+		// sections", so the merchant cannot bring it back.
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{ key: 'charts', title: 'My charts', isVisible: 0 },
+		] );
+
+		expect( charts.isVisible ).toBe( true );
+		expect( charts.title ).toBe( 'My charts' );
+	} );
+
+	it( 'ignores a stored title that is not a string', () => {
+		// The title is rendered as a React child by every section header.
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{
+				key: 'charts',
+				title: { rendered: 'My charts' },
+				isVisible: false,
+			},
+		] );
+
+		expect( charts.title ).toBe( 'Charts' );
+		expect( charts.isVisible ).toBe( false );
+	} );
+
+	it( 'drops a default section that no section can be built from', () => {
+		// The default sections filter is a third party surface too.
+		mockDefaultSections = [
+			null,
+			'charts',
+			{ component: () => null, title: 'No key', isVisible: true },
+			{ key: 'no-component', title: 'No component', isVisible: true },
+			...DEFAULT_SECTIONS,
+		];
+
+		const sections = mergeSectionsWithDefaults( undefined );
+
+		expect( sections.map( ( section ) => section.key ) ).toEqual( [
+			'store-performance',
+			'charts',
+		] );
+	} );
+
+	it( 'fills in a default hiddenBlocks that is not an array', () => {
+		// Nothing sits behind a default, and every section component calls
+		// `hiddenBlocks.includes()` on it.
+		mockDefaultSections = [
+			{ ...DEFAULT_SECTIONS[ 1 ], hiddenBlocks: null },
+		];
+
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{ key: 'charts', hiddenBlocks: 'coupons_amount' },
+		] );
+
+		expect( charts.hiddenBlocks ).toEqual( [] );
+	} );
+
+	it( 'falls back to a readable value for a corrupted default title', () => {
+		// The title is rendered as a React child by every section header.
+		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], title: {} } ];
+
+		const [ charts ] = mergeSectionsWithDefaults( [
+			{ key: 'charts', title: { rendered: 'My charts' } },
+		] );
+
+		expect( charts.title ).toBe( '' );
+	} );
+
+	it( 'keeps a default section that registers a truthy isVisible', () => {
+		// The filter never enforced a boolean and the dashboard rendered any
+		// truthy value, so dropping it would make the section unreachable.
+		mockDefaultSections = [
+			{ ...DEFAULT_SECTIONS[ 0 ], isVisible: 1 },
+			{ ...DEFAULT_SECTIONS[ 1 ], isVisible: 'yes' },
+		];
+
+		const sections = mergeSectionsWithDefaults( undefined );
+
+		expect( sections.map( ( section ) => section.isVisible ) ).toEqual( [
+			true,
+			true,
+		] );
+	} );
+
+	it( 'offers a default section that registers a falsy isVisible', () => {
+		// `undefined` is neither visible nor listed under "Add more sections".
+		mockDefaultSections = [
+			{ ...DEFAULT_SECTIONS[ 0 ], isVisible: 0 },
+			{ ...DEFAULT_SECTIONS[ 1 ], isVisible: undefined },
+		];
+
+		const sections = mergeSectionsWithDefaults( undefined );
+
+		expect( sections.map( ( section ) => section.isVisible ) ).toEqual( [
+			false,
+			false,
+		] );
+	} );
+
+	it( 'keeps a default section that registers a numeric title', () => {
+		// React prints a number, so the header rendered it before.
+		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], title: 2026 } ];
+
+		const [ charts ] = mergeSectionsWithDefaults( undefined );
+
+		expect( charts.title ).toBe( '2026' );
+	} );
+
+	it( 'keeps a default section keyed by a number', () => {
+		// The key is matched by strict equality and round trips through the
+		// stored JSON, so a number ties the section back to its default.
+		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], key: 42 } ];
+
+		const sections = mergeSectionsWithDefaults( [
+			{ key: 42, title: 'My charts' },
+		] );
+
+		expect( sections ).toHaveLength( 1 );
+		expect( sections[ 0 ] ).toMatchObject( {
+			key: 42,
+			title: 'My charts',
+		} );
+	} );
+} );
+
+describe( 'CustomizableDashboard', () => {
+	const updateUserPreferences = jest.fn();
+
+	const renderDashboard = ( dashboardSections ) => {
+		// The stored preference is JSON parsed on every render, so the dashboard
+		// gets a new reference each time and the repair has to guard itself.
+		useUserPreferences.mockImplementation( () => ( {
+			updateUserPreferences,
+			dashboard_sections: Array.isArray( dashboardSections )
+				? [ ...dashboardSections ]
+				: dashboardSections,
+		} ) );
+
+		return render(
+			<CustomizableDashboard path="/analytics/overview" query={ {} } />
+		);
+	};
+
+	beforeEach( () => {
+		updateUserPreferences.mockReset();
+	} );
+
+	it( 'renders the default sections when the stored preference is corrupted', () => {
+		const { getByText } = renderDashboard( [ null, null ] );
+
+		expect( getByText( 'Performance' ) ).toBeInTheDocument();
+		expect( getByText( 'Charts' ) ).toBeInTheDocument();
+	} );
+
+	it( 'renders a registered section that is visible by a truthy value', () => {
+		// An extension section registered the way the released dashboard
+		// accepted it, on a store that never customized the dashboard.
+		mockDefaultSections = [
+			...DEFAULT_SECTIONS,
+			{
+				key: 'my-extension',
+				component: () => null,
+				title: 'Mine',
+				isVisible: 1,
+				hiddenBlocks: [],
+			},
+		];
+
+		const { getByText } = renderDashboard( undefined );
+
+		expect( getByText( 'Mine' ) ).toBeInTheDocument();
+	} );
+
+	it( 'offers a hidden section the filter registered without an icon', () => {
+		// `Icon` clones the icon it is handed, so anything but a React element
+		// throws where the merchant goes to bring the section back.
+		mockDefaultSections = [
+			...DEFAULT_SECTIONS,
+			{
+				key: 'my-extension',
+				component: () => null,
+				title: 'Mine',
+				isVisible: 0,
+				hiddenBlocks: [],
+			},
+		];
+
+		const { getByTitle } = renderDashboard( undefined );
+		fireEvent.click( getByTitle( 'Add more sections' ) );
+
+		expect( getByTitle( 'Add Mine section' ) ).toBeInTheDocument();
+	} );
+
+	it( 'renders the icon of a hidden section that provides one', () => {
+		mockDefaultSections = [
+			...DEFAULT_SECTIONS,
+			{
+				key: 'my-extension',
+				component: () => null,
+				title: 'Mine',
+				isVisible: false,
+				icon: <svg />,
+				hiddenBlocks: [],
+			},
+		];
+
+		// The dropdown renders in a popover, so it lands outside `container`.
+		const { baseElement, getByTitle } = renderDashboard( undefined );
+		fireEvent.click( getByTitle( 'Add more sections' ) );
+
+		expect(
+			baseElement.querySelector( '.my-extension__icon' )
+		).toBeInTheDocument();
+	} );
+
+	it( 'repairs a corrupted preference once, without the React nodes', () => {
+		const { rerender } = renderDashboard( [ null, null ] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: [
+				{
+					key: 'store-performance',
+					title: 'Performance',
+					isVisible: true,
+					hiddenBlocks: [ 'taxes/order_tax' ],
+				},
+				{
+					key: 'charts',
+					title: 'Charts',
+					isVisible: true,
+					hiddenBlocks: [ 'coupons_amount' ],
+				},
+			],
+		} );
+
+		// The preference is re-read on every render, so the repair must not loop.
+		rerender(
+			<CustomizableDashboard path="/analytics/overview" query={ {} } />
+		);
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+	} );
+
+	it( 'repairs a preference stored as a raw string', () => {
+		renderDashboard( '[,,]' );
+
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+	} );
+
+	it( 'repairs a preference holding a corrupted hiddenBlocks', () => {
+		renderDashboard( [
+			{ key: 'charts', isVisible: true, hiddenBlocks: null },
+		] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				expect.objectContaining( {
+					key: 'charts',
+					hiddenBlocks: [ 'coupons_amount' ],
+				} ),
+			] ),
+		} );
+	} );
+
+	it( 'repairs a preference holding a corrupted title', () => {
+		renderDashboard( [ { key: 'charts', isVisible: true, title: {} } ] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				expect.objectContaining( { key: 'charts', title: 'Charts' } ),
+			] ),
+		} );
+	} );
+
+	it( 'repairs a preference holding a corrupted isVisible', () => {
+		renderDashboard( [ { key: 'charts', isVisible: 0 } ] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				expect.objectContaining( { key: 'charts', isVisible: true } ),
+			] ),
+		} );
+	} );
+
+	it( 'keeps a stored section the dashboard does not know about', () => {
+		// A section registered by an extension that is currently deactivated.
+		renderDashboard( [
+			null,
+			{ key: 'my-extension', title: 'Mine', isVisible: false },
+		] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				{ key: 'my-extension', title: 'Mine', isVisible: false },
+			] ),
+		} );
+	} );
+
+	it( 'keeps an unknown section that holds a corrupted field', () => {
+		// There is no default to patch the field up from, so it is dropped and
+		// the rest of the entry survives.
+		renderDashboard( [
+			null,
+			{
+				key: 'my-extension',
+				title: 'Mine',
+				isVisible: false,
+				hiddenBlocks: null,
+			},
+		] );
+
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				{ key: 'my-extension', title: 'Mine', isVisible: false },
+			] ),
+		} );
+	} );
+
+	it( 'does not store anything when there is nothing usable to fall back to', () => {
+		// Storing an empty list would only be repaired again on the next visit.
+		mockDefaultSections = [];
+
+		renderDashboard( [ null, null ] );
+
+		expect( updateUserPreferences ).not.toHaveBeenCalled();
+	} );
+
+	it( 'leaves a well formed preference alone', () => {
+		renderDashboard( [ { key: 'charts', isVisible: true } ] );
+
+		expect( updateUserPreferences ).not.toHaveBeenCalled();
+	} );
+
+	it( 'never stores the React nodes when a section is customized', () => {
+		const { getByTitle } = renderDashboard( [
+			{ key: 'charts', title: 'Charts', isVisible: true },
+			{
+				key: 'store-performance',
+				title: 'Performance',
+				isVisible: true,
+			},
+		] );
+
+		fireEvent.click( getByTitle( 'Hide Charts' ) );
+
+		const [ [ { dashboard_sections: stored } ] ] =
+			updateUserPreferences.mock.calls;
+		stored.forEach( ( section ) => {
+			expect( section ).not.toHaveProperty( 'icon' );
+			expect( section ).not.toHaveProperty( 'component' );
+		} );
+	} );
+
+	it( 'sanitizes a value a section component hands back', () => {
+		// The update callbacks are passed to third party section components.
+		const { getByTitle } = renderDashboard( [
+			{ key: 'charts', title: 'Charts', isVisible: true },
+		] );
+
+		fireEvent.click( getByTitle( 'Rename Charts' ) );
+
+		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+			dashboard_sections: expect.arrayContaining( [
+				{
+					key: 'charts',
+					isVisible: true,
+					hiddenBlocks: [ 'coupons_amount' ],
+				},
+			] ),
+		} );
+	} );
+
+	it( 'does not store anything when the dashboard was never customized', () => {
+		renderDashboard( '' );
+
+		expect( updateUserPreferences ).not.toHaveBeenCalled();
+	} );
+
+	it( 'does not store anything when the stored preference is empty', () => {
+		// An empty list means the defaults are in use, same as no preference.
+		renderDashboard( [] );
+
+		expect( updateUserPreferences ).not.toHaveBeenCalled();
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/dashboard/test/customizable.js b/plugins/woocommerce/client/admin/client/dashboard/test/customizable.js
index b3965bbcd8f..1072013e1d5 100644
--- a/plugins/woocommerce/client/admin/client/dashboard/test/customizable.js
+++ b/plugins/woocommerce/client/admin/client/dashboard/test/customizable.js
@@ -1,529 +1,327 @@
 /**
  * External dependencies
  */
-import { fireEvent, render } from '@testing-library/react';
-import { useUserPreferences } from '@woocommerce/data';
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Button } from '@wordpress/components';
+import { Component, useState } from '@wordpress/element';

 /**
  * Internal dependencies
  */
-import CustomizableDashboard, {
-	mergeSectionsWithDefaults,
-} from '../customizable';
-
-const DEFAULT_SECTIONS = [
-	{
-		key: 'store-performance',
-		component: () => null,
-		title: 'Performance',
-		isVisible: true,
-		icon: 'arrow-right',
-		hiddenBlocks: [ 'taxes/order_tax' ],
-	},
-	{
-		key: 'charts',
-		component: () => null,
-		title: 'Charts',
-		isVisible: true,
-		icon: 'chart-bar',
-		hiddenBlocks: [ 'coupons_amount' ],
-	},
-];
-
-// Reassigned by the tests that need the `woocommerce_dashboard_default_sections`
-// filter to have returned something else. Read through a getter so the mock is
-// resolved when the dashboard dereferences it, not when the module is imported.
-let mockDefaultSections = DEFAULT_SECTIONS;
-
-jest.mock( '../default-sections', () => ( {
-	__esModule: true,
-	DEFAULT_SECTIONS_FILTER: 'woocommerce_dashboard_default_sections',
-	get default() {
-		return mockDefaultSections;
-	},
-} ) );
-
-jest.mock( '../section', () => ( { title, onRemove, onTitleUpdate } ) => (
-	<div>
-		{ title }
-		<button title={ `Hide ${ title }` } onClick={ onRemove } />
-		<button
-			title={ `Rename ${ title }` }
-			onClick={ () => onTitleUpdate( { rendered: 'Renamed' } ) }
-		/>
-	</div>
-) );
+import CustomizableDashboard from '../customizable';
+
+let mockPreferenceSeed;
+let mockPreferencePayloads;
+
+const mockUseUserPreferences = jest.fn( () => {
+	const [ preferences, setPreferences ] = useState( () => ( {
+		dashboard_sections: mockPreferenceSeed
+			? mockPreferenceSeed.map( ( section ) => ( { ...section } ) )
+			: undefined,
+	} ) );
+
+	return {
+		...preferences,
+		updateUserPreferences: ( payload ) => {
+			mockPreferencePayloads.push(
+				payload.dashboard_sections
+					? {
+							dashboard_sections: payload.dashboard_sections.map(
+								( { key, isVisible } ) => ( {
+									key,
+									isVisible,
+								} )
+							),
+						}
+					: { ...payload }
+			);
+			setPreferences( ( current ) => ( { ...current, ...payload } ) );
+		},
+	};
+} );

-jest.mock( '../../analytics/components/report-header', () => ( {
-	ReportHeader: () => null,
+const mockSelect = jest.fn( () => ( {
+	getSetting: () => ( {
+		woocommerce_default_date_range: 'period=month&compare=previous_year',
+	} ),
 } ) );

-jest.mock( '@woocommerce/data', () => ( {
-	settingsStore: 'wc/admin/settings',
-	useUserPreferences: jest.fn(),
-} ) );
+const mockReportBody = class extends Component {
+	state = { isOpen: false };
+
+	render() {
+		const {
+			controls: Controls,
+			isFirst,
+			isLast,
+			onMove,
+			onRemove,
+			onTitleBlur,
+			onTitleChange,
+			title,
+			titleInput,
+		} = this.props;
+		const { isOpen } = this.state;
+		const onToggle = () => this.setState( { isOpen: ! isOpen } );
+
+		return (
+			<section aria-label={ `${ title } dashboard section` }>
+				<h2>{ title }</h2>
+				<Button
+					aria-expanded={ isOpen }
+					aria-label={ `${ title } section options` }
+					onClick={ onToggle }
+				>
+					{ title } options
+				</Button>
+				{ isOpen && (
+					<div role="menu">
+						<Controls
+							isFirst={ isFirst }
+							isLast={ isLast }
+							onMove={ onMove }
+							onRemove={ onRemove }
+							onTitleBlur={ onTitleBlur }
+							onTitleChange={ onTitleChange }
+							onToggle={ onToggle }
+							titleInput={ titleInput }
+						/>
+					</div>
+				) }
+			</section>
+		);
+	}
+};

 jest.mock( '@wordpress/data', () => ( {
 	...jest.requireActual( '@wordpress/data' ),
-	withSelect: () => ( Component ) => ( props ) => (
-		<Component { ...props } defaultDateRange="period=month" />
+	withSelect: ( mapSelect ) => ( WrappedComponent ) => ( props ) => (
+		<WrappedComponent { ...props } { ...mapSelect( mockSelect, props ) } />
 	),
 } ) );

-afterEach( () => {
-	mockDefaultSections = DEFAULT_SECTIONS;
-} );
-
-describe( 'mergeSectionsWithDefaults', () => {
-	it( 'returns the defaults when nothing is stored', () => {
-		expect( mergeSectionsWithDefaults( undefined ) ).toHaveLength( 2 );
-		expect( mergeSectionsWithDefaults( '' ) ).toHaveLength( 2 );
-		expect( mergeSectionsWithDefaults( [] ) ).toHaveLength( 2 );
-	} );
-
-	it( 'returns the defaults when the stored preference is not an array', () => {
-		// `[,,]` is not valid JSON, so it reaches the dashboard as a raw string.
-		expect( mergeSectionsWithDefaults( '[,,]' ) ).toHaveLength( 2 );
-		expect( mergeSectionsWithDefaults( {} ) ).toHaveLength( 2 );
-	} );
-
-	it( 'returns the defaults when every stored section is malformed', () => {
-		expect( mergeSectionsWithDefaults( [ null, null ] ) ).toHaveLength( 2 );
-		expect(
-			mergeSectionsWithDefaults( [ undefined, 'charts', 42, {} ] )
-		).toHaveLength( 2 );
-	} );
-
-	it( 'ignores a stored icon, which is a stale React node', () => {
-		const [ charts ] = mergeSectionsWithDefaults( [
-			{ key: 'charts', icon: { props: {} } },
-		] );
-
-		expect( charts.icon ).toBe( 'chart-bar' );
-	} );
-
-	it( 'keeps the well formed sections and drops the malformed ones', () => {
-		const sections = mergeSectionsWithDefaults( [
-			null,
-			{ key: 'charts', title: 'My charts', isVisible: false },
-		] );
-
-		expect( sections ).toHaveLength( 2 );
-		expect( sections[ 0 ] ).toMatchObject( {
-			key: 'charts',
-			title: 'My charts',
-			isVisible: false,
-		} );
-		expect( sections[ 1 ] ).toMatchObject( {
-			key: 'store-performance',
-			title: 'Performance',
-		} );
-	} );
+jest.mock( '@woocommerce/data', () => ( {
+	settingsStore: 'settingsStore',
+	useUserPreferences: () => mockUseUserPreferences(),
+} ) );

-	it( 'drops stored keys that no longer exist', () => {
-		const sections = mergeSectionsWithDefaults( [ { key: 'gone' } ] );
+// Load only the real component modules used by the dashboard owner. Importing
+// the package barrel eagerly loads unrelated search code that is not involved
+// in section preference behavior.
+jest.mock( '@woocommerce/components', () => ( {
+	H: jest.requireActual( '@woocommerce/components/section/header' ).H,
+	MenuItem: jest.requireActual(
+		'@woocommerce/components/ellipsis-menu/menu-item'
+	).default,
+	Spinner: () => null,
+} ) );

-		expect( sections.map( ( section ) => section.key ) ).toEqual( [
-			'store-performance',
-			'charts',
-		] );
-	} );
+jest.mock( '@woocommerce/currency', () => {
+	const { createContext } = jest.requireActual( '@wordpress/element' );

-	it( 'ignores a stored hiddenBlocks that is not an array', () => {
-		// Every section component calls `hiddenBlocks.includes()` on it.
-		const [ charts ] = mergeSectionsWithDefaults( [
-			{ key: 'charts', title: 'My charts', hiddenBlocks: null },
-		] );
-
-		expect( charts.hiddenBlocks ).toEqual( [ 'coupons_amount' ] );
-		expect( charts.title ).toBe( 'My charts' );
-	} );
+	return {
+		CurrencyContext: createContext( {} ),
+		getFilteredCurrencyInstance: () => ( {} ),
+	};
+} );

-	it( 'ignores a stored isVisible that is not a boolean', () => {
-		// A non boolean hides the section without listing it under "Add more
-		// sections", so the merchant cannot bring it back.
-		const [ charts ] = mergeSectionsWithDefaults( [
-			{ key: 'charts', title: 'My charts', isVisible: 0 },
-		] );
+jest.mock( '@woocommerce/navigation', () => ( {
+	getQuery: () => ( {} ),
+} ) );

-		expect( charts.isVisible ).toBe( true );
-		expect( charts.title ).toBe( 'My charts' );
-	} );
+jest.mock( '@woocommerce/tracks', () => ( {
+	recordEvent: jest.fn(),
+} ) );

-	it( 'ignores a stored title that is not a string', () => {
-		// The title is rendered as a React child by every section header.
-		const [ charts ] = mergeSectionsWithDefaults( [
-			{
-				key: 'charts',
-				title: { rendered: 'My charts' },
-				isVisible: false,
-			},
-		] );
+jest.mock( '../../analytics/components/report-header', () => ( {
+	ReportHeader: () => null,
+} ) );

-		expect( charts.title ).toBe( 'Charts' );
-		expect( charts.isVisible ).toBe( false );
-	} );
+jest.mock( '../dashboard-charts', () => ( {
+	__esModule: true,
+	default: mockReportBody,
+} ) );

-	it( 'drops a default section that no section can be built from', () => {
-		// The default sections filter is a third party surface too.
-		mockDefaultSections = [
-			null,
-			'charts',
-			{ component: () => null, title: 'No key', isVisible: true },
-			{ key: 'no-component', title: 'No component', isVisible: true },
-			...DEFAULT_SECTIONS,
-		];
-
-		const sections = mergeSectionsWithDefaults( undefined );
-
-		expect( sections.map( ( section ) => section.key ) ).toEqual( [
-			'store-performance',
-			'charts',
-		] );
-	} );
+jest.mock( '../leaderboards', () => ( {
+	__esModule: true,
+	default: mockReportBody,
+} ) );

-	it( 'fills in a default hiddenBlocks that is not an array', () => {
-		// Nothing sits behind a default, and every section component calls
-		// `hiddenBlocks.includes()` on it.
-		mockDefaultSections = [
-			{ ...DEFAULT_SECTIONS[ 1 ], hiddenBlocks: null },
-		];
+jest.mock( '../store-performance', () => ( {
+	__esModule: true,
+	default: mockReportBody,
+} ) );

-		const [ charts ] = mergeSectionsWithDefaults( [
-			{ key: 'charts', hiddenBlocks: 'coupons_amount' },
-		] );
+const renderDashboard = ( preferenceSeed ) => {
+	mockPreferenceSeed = preferenceSeed;

-		expect( charts.hiddenBlocks ).toEqual( [] );
-	} );
+	return render(
+		<CustomizableDashboard path="/analytics/overview" query={ {} } />
+	);
+};

-	it( 'falls back to a readable value for a corrupted default title', () => {
-		// The title is rendered as a React child by every section header.
-		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], title: {} } ];
+const getVisibleSectionTitles = async () => {
+	const headings = await screen.findAllByRole( 'heading', { level: 2 } );

-		const [ charts ] = mergeSectionsWithDefaults( [
-			{ key: 'charts', title: { rendered: 'My charts' } },
-		] );
+	return headings.map( ( heading ) => heading.textContent );
+};

-		expect( charts.title ).toBe( '' );
-	} );
+const openSectionOptions = async ( title ) => {
+	await userEvent.click(
+		await screen.findByRole( 'button', {
+			name: `${ title } section options`,
+		} )
+	);
+};

-	it( 'keeps a default section that registers a truthy isVisible', () => {
-		// The filter never enforced a boolean and the dashboard rendered any
-		// truthy value, so dropping it would make the section unreachable.
-		mockDefaultSections = [
-			{ ...DEFAULT_SECTIONS[ 0 ], isVisible: 1 },
-			{ ...DEFAULT_SECTIONS[ 1 ], isVisible: 'yes' },
-		];
+const invokeSectionAction = async ( title, action ) => {
+	await openSectionOptions( title );
+	await userEvent.click(
+		await screen.findByRole( 'menuitem', { name: new RegExp( action ) } )
+	);
+};

-		const sections = mergeSectionsWithDefaults( undefined );
+const getLastPreferencePayload = () =>
+	mockPreferencePayloads[ mockPreferencePayloads.length - 1 ];

-		expect( sections.map( ( section ) => section.isVisible ) ).toEqual( [
-			true,
-			true,
-		] );
+describe( 'CustomizableDashboard section preferences', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockPreferenceSeed = undefined;
+		mockPreferencePayloads = [];
 	} );

-	it( 'offers a default section that registers a falsy isVisible', () => {
-		// `undefined` is neither visible nor listed under "Add more sections".
-		mockDefaultSections = [
-			{ ...DEFAULT_SECTIONS[ 0 ], isVisible: 0 },
-			{ ...DEFAULT_SECTIONS[ 1 ], isVisible: undefined },
-		];
+	it( 'renders Performance, Charts, and Leaderboards in default order', async () => {
+		renderDashboard();

-		const sections = mergeSectionsWithDefaults( undefined );
-
-		expect( sections.map( ( section ) => section.isVisible ) ).toEqual( [
-			false,
-			false,
+		expect( await getVisibleSectionTitles() ).toEqual( [
+			'Performance',
+			'Charts',
+			'Leaderboards',
 		] );
 	} );

-	it( 'keeps a default section that registers a numeric title', () => {
-		// React prints a number, so the header rendered it before.
-		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], title: 2026 } ];
-
-		const [ charts ] = mergeSectionsWithDefaults( undefined );
-
-		expect( charts.title ).toBe( '2026' );
-	} );
-
-	it( 'keeps a default section keyed by a number', () => {
-		// The key is matched by strict equality and round trips through the
-		// stored JSON, so a number ties the section back to its default.
-		mockDefaultSections = [ { ...DEFAULT_SECTIONS[ 1 ], key: 42 } ];
-
-		const sections = mergeSectionsWithDefaults( [
-			{ key: 42, title: 'My charts' },
-		] );
+	it( 'hides move up for first and move down for last visible section', async () => {
+		renderDashboard();

-		expect( sections ).toHaveLength( 1 );
-		expect( sections[ 0 ] ).toMatchObject( {
-			key: 42,
-			title: 'My charts',
-		} );
-	} );
-} );
+		await openSectionOptions( 'Performance' );
+		expect(
+			screen.queryByRole( 'menuitem', { name: /Move up/ } )
+		).not.toBeInTheDocument();
+		expect(
+			screen.getByRole( 'menuitem', { name: /Move down/ } )
+		).toBeInTheDocument();

-describe( 'CustomizableDashboard', () => {
-	const updateUserPreferences = jest.fn();
-
-	const renderDashboard = ( dashboardSections ) => {
-		// The stored preference is JSON parsed on every render, so the dashboard
-		// gets a new reference each time and the repair has to guard itself.
-		useUserPreferences.mockImplementation( () => ( {
-			updateUserPreferences,
-			dashboard_sections: Array.isArray( dashboardSections )
-				? [ ...dashboardSections ]
-				: dashboardSections,
-		} ) );
-
-		return render(
-			<CustomizableDashboard path="/analytics/overview" query={ {} } />
+		await userEvent.click(
+			screen.getByRole( 'button', {
+				name: 'Performance section options',
+			} )
 		);
-	};
-
-	beforeEach( () => {
-		updateUserPreferences.mockReset();
-	} );
-
-	it( 'renders the default sections when the stored preference is corrupted', () => {
-		const { getByText } = renderDashboard( [ null, null ] );
-
-		expect( getByText( 'Performance' ) ).toBeInTheDocument();
-		expect( getByText( 'Charts' ) ).toBeInTheDocument();
-	} );
-
-	it( 'renders a registered section that is visible by a truthy value', () => {
-		// An extension section registered the way the released dashboard
-		// accepted it, on a store that never customized the dashboard.
-		mockDefaultSections = [
-			...DEFAULT_SECTIONS,
-			{
-				key: 'my-extension',
-				component: () => null,
-				title: 'Mine',
-				isVisible: 1,
-				hiddenBlocks: [],
-			},
-		];
-
-		const { getByText } = renderDashboard( undefined );
-
-		expect( getByText( 'Mine' ) ).toBeInTheDocument();
-	} );
-
-	it( 'offers a hidden section the filter registered without an icon', () => {
-		// `Icon` clones the icon it is handed, so anything but a React element
-		// throws where the merchant goes to bring the section back.
-		mockDefaultSections = [
-			...DEFAULT_SECTIONS,
-			{
-				key: 'my-extension',
-				component: () => null,
-				title: 'Mine',
-				isVisible: 0,
-				hiddenBlocks: [],
-			},
-		];
-
-		const { getByTitle } = renderDashboard( undefined );
-		fireEvent.click( getByTitle( 'Add more sections' ) );
-
-		expect( getByTitle( 'Add Mine section' ) ).toBeInTheDocument();
-	} );
-
-	it( 'renders the icon of a hidden section that provides one', () => {
-		mockDefaultSections = [
-			...DEFAULT_SECTIONS,
-			{
-				key: 'my-extension',
-				component: () => null,
-				title: 'Mine',
-				isVisible: false,
-				icon: <svg />,
-				hiddenBlocks: [],
-			},
-		];
-
-		// The dropdown renders in a popover, so it lands outside `container`.
-		const { baseElement, getByTitle } = renderDashboard( undefined );
-		fireEvent.click( getByTitle( 'Add more sections' ) );
-
+		await openSectionOptions( 'Leaderboards' );
 		expect(
-			baseElement.querySelector( '.my-extension__icon' )
+			screen.getByRole( 'menuitem', { name: /Move up/ } )
 		).toBeInTheDocument();
+		expect(
+			screen.queryByRole( 'menuitem', { name: /Move down/ } )
+		).not.toBeInTheDocument();
 	} );

-	it( 'repairs a corrupted preference once, without the React nodes', () => {
-		const { rerender } = renderDashboard( [ null, null ] );
+	it( 'moves the first section down and saves the ordered preference payload', async () => {
+		renderDashboard();
+
+		await invokeSectionAction( 'Performance', 'Move down' );

-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
+		expect( getLastPreferencePayload() ).toEqual( {
 			dashboard_sections: [
-				{
-					key: 'store-performance',
-					title: 'Performance',
-					isVisible: true,
-					hiddenBlocks: [ 'taxes/order_tax' ],
-				},
-				{
-					key: 'charts',
-					title: 'Charts',
-					isVisible: true,
-					hiddenBlocks: [ 'coupons_amount' ],
-				},
+				{ key: 'charts', isVisible: true },
+				{ key: 'store-performance', isVisible: true },
+				{ key: 'leaderboards', isVisible: true },
 			],
 		} );
-
-		// The preference is re-read on every render, so the repair must not loop.
-		rerender(
-			<CustomizableDashboard path="/analytics/overview" query={ {} } />
-		);
-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
+		expect( await getVisibleSectionTitles() ).toEqual( [
+			'Charts',
+			'Performance',
+			'Leaderboards',
+		] );
 	} );

-	it( 'repairs a preference stored as a raw string', () => {
-		renderDashboard( '[,,]' );
+	it( 'moves the second section up and saves the ordered preference payload', async () => {
+		renderDashboard();

-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
-	} );
+		await invokeSectionAction( 'Charts', 'Move up' );

-	it( 'repairs a preference holding a corrupted hiddenBlocks', () => {
-		renderDashboard( [
-			{ key: 'charts', isVisible: true, hiddenBlocks: null },
-		] );
-
-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				expect.objectContaining( {
-					key: 'charts',
-					hiddenBlocks: [ 'coupons_amount' ],
-				} ),
-			] ),
+		expect( getLastPreferencePayload() ).toEqual( {
+			dashboard_sections: [
+				{ key: 'charts', isVisible: true },
+				{ key: 'store-performance', isVisible: true },
+				{ key: 'leaderboards', isVisible: true },
+			],
 		} );
+		expect( await getVisibleSectionTitles() ).toEqual( [
+			'Charts',
+			'Performance',
+			'Leaderboards',
+		] );
 	} );

-	it( 'repairs a preference holding a corrupted title', () => {
-		renderDashboard( [ { key: 'charts', isVisible: true, title: {} } ] );
+	it( 'removes Performance and saves it hidden at the end', async () => {
+		renderDashboard();

-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				expect.objectContaining( { key: 'charts', title: 'Charts' } ),
-			] ),
-		} );
-	} );
+		await invokeSectionAction( 'Performance', 'Remove section' );

-	it( 'repairs a preference holding a corrupted isVisible', () => {
-		renderDashboard( [ { key: 'charts', isVisible: 0 } ] );
-
-		expect( updateUserPreferences ).toHaveBeenCalledTimes( 1 );
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				expect.objectContaining( { key: 'charts', isVisible: true } ),
-			] ),
+		expect( getLastPreferencePayload() ).toEqual( {
+			dashboard_sections: [
+				{ key: 'charts', isVisible: true },
+				{ key: 'leaderboards', isVisible: true },
+				{ key: 'store-performance', isVisible: false },
+			],
 		} );
-	} );
-
-	it( 'keeps a stored section the dashboard does not know about', () => {
-		// A section registered by an extension that is currently deactivated.
-		renderDashboard( [
-			null,
-			{ key: 'my-extension', title: 'Mine', isVisible: false },
+		expect( await getVisibleSectionTitles() ).toEqual( [
+			'Charts',
+			'Leaderboards',
 		] );
-
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				{ key: 'my-extension', title: 'Mine', isVisible: false },
-			] ),
-		} );
 	} );

-	it( 'keeps an unknown section that holds a corrupted field', () => {
-		// There is no default to patch the field up from, so it is dropped and
-		// the rest of the entry survives.
+	it( 'adds hidden Performance back and saves it visible at the end', async () => {
 		renderDashboard( [
-			null,
-			{
-				key: 'my-extension',
-				title: 'Mine',
-				isVisible: false,
-				hiddenBlocks: null,
-			},
+			{ key: 'charts', isVisible: true },
+			{ key: 'leaderboards', isVisible: true },
+			{ key: 'store-performance', isVisible: false },
 		] );

-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				{ key: 'my-extension', title: 'Mine', isVisible: false },
-			] ),
+		await userEvent.click(
+			await screen.findByRole( 'button', { name: 'Add more sections' } )
+		);
+		const choices = await screen.findByRole( 'heading', {
+			name: 'Dashboard Sections',
 		} );
-	} );
-
-	it( 'does not store anything when there is nothing usable to fall back to', () => {
-		// Storing an empty list would only be repaired again on the next visit.
-		mockDefaultSections = [];
-
-		renderDashboard( [ null, null ] );
-
-		expect( updateUserPreferences ).not.toHaveBeenCalled();
-	} );
-
-	it( 'leaves a well formed preference alone', () => {
-		renderDashboard( [ { key: 'charts', isVisible: true } ] );
-
-		expect( updateUserPreferences ).not.toHaveBeenCalled();
-	} );
+		const addPerformance = within( choices.parentElement ).getByRole(
+			'button',
+			{ name: 'Performance' }
+		);

-	it( 'never stores the React nodes when a section is customized', () => {
-		const { getByTitle } = renderDashboard( [
-			{ key: 'charts', title: 'Charts', isVisible: true },
-			{
-				key: 'store-performance',
-				title: 'Performance',
-				isVisible: true,
-			},
-		] );
+		// The button's own label is the bare section title, so the tooltip is
+		// the only place it says what clicking it does.
+		expect( addPerformance ).toHaveAttribute(
+			'title',
+			'Add Performance section'
+		);

-		fireEvent.click( getByTitle( 'Hide Charts' ) );
+		await userEvent.click( addPerformance );
+		await screen.findByRole( 'heading', { name: 'Performance' } );

-		const [ [ { dashboard_sections: stored } ] ] =
-			updateUserPreferences.mock.calls;
-		stored.forEach( ( section ) => {
-			expect( section ).not.toHaveProperty( 'icon' );
-			expect( section ).not.toHaveProperty( 'component' );
+		expect( getLastPreferencePayload() ).toEqual( {
+			dashboard_sections: [
+				{ key: 'charts', isVisible: true },
+				{ key: 'leaderboards', isVisible: true },
+				{ key: 'store-performance', isVisible: true },
+			],
 		} );
-	} );
-
-	it( 'sanitizes a value a section component hands back', () => {
-		// The update callbacks are passed to third party section components.
-		const { getByTitle } = renderDashboard( [
-			{ key: 'charts', title: 'Charts', isVisible: true },
+		expect( await getVisibleSectionTitles() ).toEqual( [
+			'Charts',
+			'Leaderboards',
+			'Performance',
 		] );
-
-		fireEvent.click( getByTitle( 'Rename Charts' ) );
-
-		expect( updateUserPreferences ).toHaveBeenCalledWith( {
-			dashboard_sections: expect.arrayContaining( [
-				{
-					key: 'charts',
-					isVisible: true,
-					hiddenBlocks: [ 'coupons_amount' ],
-				},
-			] ),
-		} );
-	} );
-
-	it( 'does not store anything when the dashboard was never customized', () => {
-		renderDashboard( '' );
-
-		expect( updateUserPreferences ).not.toHaveBeenCalled();
-	} );
-
-	it( 'does not store anything when the stored preference is empty', () => {
-		// An empty list means the defaults are in use, same as no preference.
-		renderDashboard( [] );
-
-		expect( updateUserPreferences ).not.toHaveBeenCalled();
 	} );
 } );
diff --git a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-overview.spec.ts b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-overview.spec.ts
index 9c75a6fbb86..163f798d74b 100644
--- a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-overview.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-overview.spec.ts
@@ -19,9 +19,7 @@ const EXPECTED_SECTION_HEADERS = [ 'Performance', 'Charts', 'Leaderboards' ];

 let userId: number;
 let headings_sections: Locator;
-let heading_performance: Locator;
 let buttons_ellipsis: Locator;
-let menuitem_moveUp: Locator;
 let menuitem_moveDown: Locator;
 let page: Page;

@@ -50,12 +48,15 @@ const headers = basicAuthHeaders( sectionsUser );

 // Call this before the click that triggers the save, and await the returned
 // promise after it — registering the listener after the click can miss a
-// response that already arrived.
+// response that already arrived. It matches the write that carries the section
+// layout: other preference writes, such as the metrics tour being dismissed,
+// go to the same endpoint and would otherwise satisfy it too early.
 const waitForUserPrefsSave = () =>
 	page.waitForResponse(
 		( res ) =>
 			res.url().includes( `/users/${ userId }` ) &&
-			res.request().method() !== 'GET'
+			res.request().method() !== 'GET' &&
+			!! res.request().postData()?.includes( 'dashboard_sections' )
 	);

 const expectSaved = ( response: PlaywrightResponse ) =>
@@ -64,44 +65,6 @@ const expectSaved = ( response: PlaywrightResponse ) =>
 		`${ response.status() } ${ response.url() }`
 	).toBeTruthy();

-const hidePerformanceSection = async () => {
-	const response =
-		await test.step( `Send POST request to hide Performance section`, async () => {
-			const pageRequest = page.request;
-			const url = `./wp-json/wp/v2/users/${ userId }`;
-			const params = { _locale: 'user' };
-			const dashboard_sections = JSON.stringify( [
-				{ key: 'store-performance', isVisible: false },
-			] );
-			const data = {
-				id: userId,
-				woocommerce_meta: {
-					dashboard_sections,
-				},
-			};
-
-			return await pageRequest.post( url, {
-				data,
-				params,
-				headers,
-			} );
-		} );
-
-	await test.step( `Assert response status is OK`, async () => {
-		expect( response.ok() ).toBeTruthy();
-	} );
-
-	await test.step( `Inspect the response payload to verify that Performance section was successfully hidden`, async () => {
-		const { woocommerce_meta } = await response.json();
-		const { dashboard_sections } = woocommerce_meta;
-		const sections = JSON.parse( dashboard_sections );
-		const performanceSection = sections.find(
-			( { key } ) => key === 'store-performance'
-		);
-		expect( performanceSection.isVisible ).toBeFalsy();
-	} );
-};
-
 const resetSections = async () => {
 	const response =
 		await test.step( `Send POST request to reset all sections`, async () => {
@@ -172,6 +135,28 @@ test.describe(
 				} );
 			} );

+			// The shared admin has the metrics tour marked as seen in site setup.
+			// This administrator is new, so the tour would show and its dismissal
+			// would write preferences alongside the section layout this spec saves.
+			await test.step( `Mark the metrics tour as seen`, async () => {
+				const response = await page.request.post(
+					`./wp-json/wp/v2/users/${ userId }`,
+					{
+						data: {
+							id: userId,
+							woocommerce_meta: {
+								dashboard_performance_tour_shown: 'yes',
+							},
+						},
+						headers,
+					}
+				);
+				expect(
+					response.ok(),
+					`${ response.status() } ${ await response.text() }`
+				).toBeTruthy();
+			} );
+
 			await resetSections();

 			await test.step( `Initialize locators`, async () => {
@@ -183,18 +168,10 @@ test.describe(
 					name: pattern,
 				} );

-				heading_performance = page.getByRole( 'heading', {
-					name: 'Performance',
-				} );
-
 				buttons_ellipsis = page.getByRole( 'button', {
 					name: 'Choose which',
 				} );

-				menuitem_moveUp = page.getByRole( 'menuitem', {
-					name: 'Move up',
-				} );
-
 				menuitem_moveDown = page.getByRole( 'menuitem', {
 					name: 'Move down',
 				} );
@@ -221,124 +198,36 @@ test.describe(
 			await page.close();
 		} );

-		test( 'a user should see 3 sections by default - Performance, Charts, and Leaderboards', async () => {
-			for ( const expectedSection of EXPECTED_SECTION_HEADERS ) {
-				await test.step( `Assert that the "${ expectedSection }" section is visible`, async () => {
-					await expect(
-						headings_sections.filter( { hasText: expectedSection } )
-					).toBeVisible();
-				} );
-			}
-		} );
-
-		test.describe(
-			'moving sections',
-			{ tag: [ tags.COULD_BE_LOWER_LEVEL_TEST ] },
-			() => {
-				test( 'should not display move up for the top, or move down for the bottom section', async () => {
-					await test.step( `Check the top section`, async () => {
-						await buttons_ellipsis.first().click();
-						await expect( menuitem_moveUp ).toBeHidden();
-						await expect( menuitem_moveDown ).toBeVisible();
-						await page.keyboard.press( 'Escape' );
-					} );
-
-					await test.step( `Check the bottom section`, async () => {
-						await expect( buttons_ellipsis.nth( 2 ) ).toBeVisible();
-						await buttons_ellipsis.last().click();
-						await expect( menuitem_moveDown ).toBeHidden();
-						await expect( menuitem_moveUp ).toBeVisible();
-						await page.keyboard.press( 'Escape' );
-					} );
-				} );
+		test( 'persists reordered Analytics sections after reload', async () => {
+			await test.step( `Assert the default sections and their real controls`, async () => {
+				await expect( headings_sections ).toHaveText(
+					EXPECTED_SECTION_HEADERS
+				);
+				await expect( buttons_ellipsis ).toHaveCount( 3 );

-				test( 'should allow a user to move a section down', async () => {
-					const firstSection = await headings_sections
-						.first()
-						.innerText();
-					const secondSection = await headings_sections
-						.nth( 1 )
-						.innerText();
-
-					await test.step( `Move first section down`, async () => {
-						await buttons_ellipsis.first().click();
-						const savePromise = waitForUserPrefsSave();
-						await menuitem_moveDown.click();
-						expectSaved( await savePromise );
-					} );
-
-					await test.step( `Expect the second section to become first, and first becomes second.`, async () => {
-						await expect( headings_sections.first() ).toHaveText(
-							secondSection
-						);
-
-						await expect( headings_sections.nth( 1 ) ).toHaveText(
-							firstSection
-						);
-					} );
-				} );
+				for ( const button of await buttons_ellipsis.all() ) {
+					await expect( button ).toBeVisible();
+				}
+			} );

-				test( 'should allow a user to move a section up', async () => {
-					const firstSection = await headings_sections
-						.first()
-						.innerText();
-					const secondSection = await headings_sections
-						.nth( 1 )
-						.innerText();
-
-					await test.step( `Move second section up`, async () => {
-						await buttons_ellipsis.nth( 1 ).click();
-						const savePromise = waitForUserPrefsSave();
-						await menuitem_moveUp.click();
-						expectSaved( await savePromise );
-					} );
-
-					await test.step( `Expect second section becomes first section, first becomes second`, async () => {
-						await expect( headings_sections.first() ).toHaveText(
-							secondSection
-						);
-						await expect( headings_sections.nth( 1 ) ).toHaveText(
-							firstSection
-						);
-					} );
-				} );
-			}
-		);
-
-		test( 'should allow a user to remove a section', async () => {
-			await test.step( `Remove the Performance section`, async () => {
-				await page
-					.getByRole( 'button', {
-						name: 'Choose which analytics to display and the section name',
-					} )
-					.click();
+			await test.step( `Move Performance below Charts and save`, async () => {
+				await buttons_ellipsis.first().click();
 				const savePromise = waitForUserPrefsSave();
-				await page
-					.getByRole( 'menuitem', { name: 'Remove section' } )
-					.click();
+				await menuitem_moveDown.click();
 				expectSaved( await savePromise );
 			} );

-			await test.step( `Expect the Performance section to be hidden`, async () => {
-				await expect( headings_sections ).toHaveCount( 2 );
-				await expect( heading_performance ).toBeHidden();
-			} );
-		} );
+			const reorderedSections = [
+				'Charts',
+				'Performance',
+				'Leaderboards',
+			];
+			await expect( headings_sections ).toHaveText( reorderedSections );

-		test( 'should allow a user to add a section back in', async () => {
-			await hidePerformanceSection();
 			await page.reload();

-			await test.step( `Add the Performance section back in.`, async () => {
-				await page.getByTitle( 'Add more sections' ).click();
-				const savePromise = waitForUserPrefsSave();
-				await page.getByTitle( 'Add Performance section' ).click();
-				expectSaved( await savePromise );
-			} );
-
-			await test.step( `Expect the Performance section to be added back.`, async () => {
-				await expect( heading_performance ).toBeVisible();
-			} );
+			await expect( headings_sections ).toHaveText( reorderedSections );
+			await expect( buttons_ellipsis ).toHaveCount( 3 );
 		} );
 	}
 );
diff --git a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-settings.spec.ts b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-settings.spec.ts
index d4ec292b1cc..019acfe9507 100644
--- a/plugins/woocommerce/tests/e2e/tests/analytics/analytics-settings.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/analytics/analytics-settings.spec.ts
@@ -34,172 +34,110 @@ test.describe(
 			} );
 		} );

-		test.afterAll( async () => {
-			await page.close();
-		} );
-
-		test( 'should show Immediate mode by default when option is not set', async ( {
-			baseURL,
-		} ) => {
-			// Delete the option to simulate a new installation
-			await deleteOption(
-				request,
-				baseURL,
-				'woocommerce_analytics_scheduled_import'
-			);
-
-			// Reload the page
-			await page.reload();
-
-			// Verify "Immediately" is selected
-			await expect(
-				page.getByRole( 'radio', {
-					name: /Immediately/i,
-				} )
-			).toBeChecked();
-		} );
-
-		test( 'should switch from scheduled to immediate mode with confirmation modal - cancel flow', async ( {
-			baseURL,
-		} ) => {
-			// Set to scheduled mode
-			await setOption(
-				request,
-				baseURL,
-				'woocommerce_analytics_scheduled_import',
-				'yes'
-			);
-
-			// Reload the page
-			await page.reload();
-
-			// Verify "Scheduled (recommended)" is selected
-			await expect(
-				page.getByRole( 'radio', {
-					name: /Scheduled \(recommended\)/i,
-				} )
-			).toBeChecked();
-
-			// Click "Immediately" radio option
-			await page.getByRole( 'radio', { name: /Immediately/i } ).click();
-
-			// Verify confirmation modal appears
-			await expect(
-				page.getByRole( 'heading', { name: 'Are you sure?' } )
-			).toBeVisible();
-
-			// Click "Cancel" button
-			await page
-				.getByRole( 'button', { name: /Cancel/i, exact: false } )
-				.click();
-
-			// Verify modal closes
-			await expect(
-				page.locator(
-					'.woocommerce-analytics-import-mode-confirmation-modal'
-				)
-			).toBeHidden();
-
-			// Verify "Scheduled (recommended)" is still selected on page
-			await expect(
-				page.getByRole( 'radio', {
-					name: /Scheduled \(recommended\)/i,
-				} )
-			).toBeChecked();
-		} );
-
-		test( 'should switch from scheduled to immediate mode with confirmation modal - confirm flow', async ( {
-			baseURL,
-		} ) => {
-			// Set to scheduled mode
-			await setOption(
-				request,
-				baseURL,
-				'woocommerce_analytics_scheduled_import',
-				'yes'
-			);
-
-			// Reload the page
-			await page.reload();
-
-			// Click "Immediately" radio option
-			await page.getByRole( 'radio', { name: /Immediately/i } ).click();
-
-			// Verify confirmation modal appears
-			await expect(
-				page.getByRole( 'heading', { name: 'Are you sure?' } )
-			).toBeVisible();
-
-			// Click "Confirm" button
-			await page
-				.getByRole( 'button', { name: /Confirm/i, exact: false } )
-				.click();
-
-			// Click "Save settings" button
-			await page.getByRole( 'button', { name: 'Save settings' } ).click();
-
-			// Verify success message
-			await expect(
-				page
-					.getByText( 'Your settings have been successfully saved.' )
-					.first()
-			).toBeVisible();
-
-			// Refresh page and verify "Immediately" is selected
-			await page.reload();
-			await expect(
-				page.getByRole( 'radio', { name: /Immediately/i } )
-			).toBeChecked();
+		test.afterAll( async ( { baseURL } ) => {
+			const cleanupErrors: unknown[] = [];
+
+			try {
+				await setOption(
+					request,
+					baseURL,
+					'woocommerce_analytics_scheduled_import',
+					'yes'
+				);
+			} catch ( error ) {
+				cleanupErrors.push( error );
+			}
+
+			try {
+				await page.close();
+			} catch ( error ) {
+				cleanupErrors.push( error );
+			}
+
+			if ( cleanupErrors.length ) {
+				throw new AggregateError(
+					cleanupErrors,
+					'Failed to restore Analytics Settings test state.'
+				);
+			}
 		} );

-		test( 'should switch from immediate to scheduled mode without confirmation modal', async ( {
+		test( 'manages scheduled import mode confirmation and persistence', async ( {
 			baseURL,
 		} ) => {
-			// Set to immediate mode
-			await setOption(
-				request,
-				baseURL,
-				'woocommerce_analytics_scheduled_import',
-				'no'
-			);
-
-			// Reload the page
-			await page.reload();
-
-			// Verify "Immediately" is selected
-			await expect(
-				page.getByRole( 'radio', { name: /Immediately/i } )
-			).toBeChecked();
-
-			// Click "Scheduled (recommended)" radio option
-			await page
-				.getByRole( 'radio', {
-					name: /Scheduled \(recommended\)/i,
-				} )
-				.click();
-
-			// Verify NO modal appears (no warning when switching back to recommended mode)
-			await expect(
-				page.getByRole( 'heading', { name: 'Are you sure?' } )
-			).toBeHidden();
+			const immediately = page.getByRole( 'radio', {
+				name: /Immediately/i,
+			} );
+			const scheduled = page.getByRole( 'radio', {
+				name: /Scheduled \(recommended\)/i,
+			} );
+			const confirmationHeading = page.getByRole( 'heading', {
+				name: 'Are you sure?',
+			} );
+			const successNotice = page
+				.getByText( 'Your settings have been successfully saved.' )
+				.first();
+
+			await test.step( 'Use Immediate when the option is absent', async () => {
+				await deleteOption(
+					request,
+					baseURL,
+					'woocommerce_analytics_scheduled_import'
+				);
+				await page.reload();
+				await expect( immediately ).toBeChecked();
+			} );

-			// Click "Save settings" button
-			await page.getByRole( 'button', { name: 'Save settings' } ).click();
+			await test.step( 'Cancel a scheduled to immediate change', async () => {
+				await setOption(
+					request,
+					baseURL,
+					'woocommerce_analytics_scheduled_import',
+					'yes'
+				);
+				await page.reload();
+				await expect( scheduled ).toBeChecked();
+
+				await immediately.click();
+				await expect( confirmationHeading ).toBeVisible();
+				await page
+					.getByRole( 'button', { name: /Cancel/i, exact: false } )
+					.click();
+				await expect( confirmationHeading ).toBeHidden();
+				await expect( scheduled ).toBeChecked();
+			} );

-			// Verify success message
-			await expect(
-				page
-					.getByText( 'Your settings have been successfully saved.' )
-					.first()
-			).toBeVisible();
+			await test.step( 'Confirm and persist immediate mode', async () => {
+				await immediately.click();
+				await expect( confirmationHeading ).toBeVisible();
+				await page
+					.getByRole( 'button', {
+						name: /Confirm/i,
+						exact: false,
+					} )
+					.click();
+				await page
+					.getByRole( 'button', { name: 'Save settings' } )
+					.click();
+				await expect( successNotice ).toBeVisible();
+				await page.reload();
+				await expect( immediately ).toBeChecked();
+			} );

-			// Refresh page and verify "Scheduled (recommended)" is selected
-			await page.reload();
-			await expect(
-				page.getByRole( 'radio', {
-					name: /Scheduled \(recommended\)/i,
-				} )
-			).toBeChecked();
+			await test.step( 'Return to scheduled mode without a warning', async () => {
+				await scheduled.click();
+				// Wait for the click to land before asserting the absence of the
+				// modal: `toBeHidden` resolves on its first poll, so on its own it
+				// would pass on the tick before a modal could appear.
+				await expect( scheduled ).toBeChecked();
+				await expect( confirmationHeading ).toBeHidden();
+				await page
+					.getByRole( 'button', { name: 'Save settings' } )
+					.click();
+				await expect( successNotice ).toBeVisible();
+				await page.reload();
+				await expect( scheduled ).toBeChecked();
+			} );
 		} );
 	}
 );
diff --git a/plugins/woocommerce/tests/e2e/tests/analytics/analytics.spec.ts b/plugins/woocommerce/tests/e2e/tests/analytics/analytics.spec.ts
index 4c51a74703f..70eca700b37 100644
--- a/plugins/woocommerce/tests/e2e/tests/analytics/analytics.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/analytics/analytics.spec.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { request as apiRequest, type Page } from '@playwright/test';
+import { type Page } from '@playwright/test';
 import {
 	WC_ADMIN_API_PATH,
 	WC_API_PATH,
@@ -12,7 +12,6 @@ import {
  */
 import { expect, tags, test as baseTest, locks } from '../../fixtures/fixtures';
 import { ADMIN_STATE_PATH } from '../../playwright.config';
-import { setOption } from '../../utils/options';

 /**
  * Escape a string for literal use inside a RegExp.
@@ -20,6 +19,13 @@ import { setOption } from '../../utils/options';
 const escapeRegExp = ( value: string ) =>
 	value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );

+/**
+ * Format a date as `YYYY-MM-DD` in UTC. The e2e store keeps WordPress's
+ * default UTC timezone, so this matches the dates Analytics puts in its
+ * report requests.
+ */
+const toUtcDate = ( date: Date ) => date.toISOString().slice( 0, 10 );
+
 /**
  * Locate an analytics summary tile ("performance indicator") by its label and
  * value, ignoring the volatile delta text that follows (e.g.
@@ -332,7 +338,7 @@ test.beforeAll( async ( { request, restApi } ) => {
 						orders: stats.data?.totals?.orders_count,
 						itemsSold: stats.data?.totals?.items_sold,
 					};
-				} catch ( e ) {
+				} catch {
 					return { orders: undefined, itemsSold: undefined };
 				}
 			},
@@ -366,42 +372,6 @@ test.afterAll( async ( { restApi } ) => {
 	);
 } );

-test(
-	'renders the overview performance indicators',
-	{
-		tag: [ tags.PAYMENTS, tags.SERVICES ],
-		lock: locks.ANALYTICS_IMPORT_MODE,
-	},
-	async ( { page } ) => {
-		await page.goto(
-			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
-		);
-
-		// 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();
-		}
-	}
-);
-
 test(
 	'downloads revenue report as CSV',
 	{
@@ -424,7 +394,7 @@ test(
 			await expect( page.locator( '.components-snackbar' ) ).toHaveText(
 				'Your Revenue Report will be emailed to you.'
 			);
-		} catch ( e ) {
+		} catch {
 			const downloadPromise = page.waitForEvent( 'download' );
 			await page.getByRole( 'button', { name: 'Download' } ).click();
 			const download = await downloadPromise;
@@ -462,7 +432,61 @@ test(
 		// 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();
+
+		// Update fires the summary, the previous-year summary and the chart, and
+		// a Month to date response can still be in flight. Matching on last
+		// month's exact range picks only a request for the range just selected.
+		const now = new Date();
+		const expectedAfter = `${ toUtcDate(
+			new Date(
+				Date.UTC( now.getUTCFullYear(), now.getUTCMonth() - 1, 1 )
+			)
+		) }T00:00:00`;
+		const expectedBefore = `${ toUtcDate(
+			new Date( Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), 0 ) )
+		) }T23:59:59`;
+
+		const statsResponsePromise = page.waitForResponse(
+			( response ) => {
+				const requestUrl = new URL( response.url() );
+
+				return (
+					requestUrl.pathname.endsWith(
+						'/wc-analytics/reports/products/stats'
+					) &&
+					requestUrl.searchParams.get( 'products' ) ===
+						String( variableProductId ) &&
+					requestUrl.searchParams.get( 'after' ) === expectedAfter &&
+					requestUrl.searchParams.get( 'before' ) === expectedBefore
+				);
+			},
+			{ timeout: 15_000 }
+		);
 		await page.getByRole( 'button', { name: 'Update' } ).click();
+		const [ statsResponse ] = await Promise.all( [
+			statsResponsePromise,
+			expect( page ).toHaveURL(
+				( url ) => {
+					const query = url.searchParams;
+
+					return (
+						query.get( 'path' ) === '/analytics/products' &&
+						query.get( 'filter' ) === 'single_product' &&
+						query.get( 'products' ) ===
+							String( variableProductId ) &&
+						query.get( 'period' ) === 'last_month' &&
+						query.get( 'compare' ) === 'previous_year'
+					);
+				},
+				{ timeout: 5_000 }
+			),
+		] );
+		expect( statsResponse.ok() ).toBeTruthy();
+
+		expect(
+			new URL( statsResponse.url() ).searchParams.get( 'interval' )
+		).toBe( 'day' );
+
 		await expect( summaryTile( page, 'Items sold', '0' ) ).toBeVisible();
 		await expect( summaryTile( page, 'Net sales', '$0.00' ) ).toBeVisible();
 	}
@@ -481,10 +505,9 @@ test(

 		await expect( page.getByText( 'Month to date' ).first() ).toBeVisible();

-		// 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.
+		// A range that predates every spec's orders keeps the totals at a stable
+		// zero whatever else the shared serial store holds, while still driving
+		// the custom-range UI end to end.
 		await page.getByRole( 'button', { name: 'Month to date' } ).click();
 		await page.getByText( 'Custom', { exact: true } ).click();
 		await page
@@ -520,12 +543,8 @@ test(
 		lock: locks.ANALYTICS_IMPORT_MODE,
 	},
 	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.
+		// Every one of this spec's ten orders contains productIds[ 0 ] and no
+		// other order in the shared store does, so these totals are isolated.
 		await page.goto(
 			`wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Forders&filter=advanced&product_includes=${ productIds[ 0 ] }`
 		);
@@ -641,86 +660,143 @@ test(
 	}
 );

-// Overview manual-import trigger.
-test.describe(
-	'manual import trigger',
+test(
+	'shows the import status bar and runs its manual trigger in scheduled mode',
 	{
 		tag: [ tags.PAYMENTS, tags.SERVICES ],
 		lock: locks.ANALYTICS_IMPORT_MODE,
 	},
-	() => {
-		test.beforeEach( async ( { page } ) => {
-			await page.goto(
-				'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
-			);
+	async ( { page, request, restApi } ) => {
+		// Scheduled mode is the only mode that offers the manual trigger. The
+		// beforeAll forced immediate mode so the report data would import per order.
+		await restApi.post(
+			'wc-analytics/settings/wc_admin/woocommerce_analytics_scheduled_import',
+			{ value: 'yes' }
+		);
+
+		await page.goto(
+			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
+		);
+
+		// Turning scheduled mode on schedules a recurring batch import that is due
+		// immediately, and wp-env has no cron to run it. Until it runs, the status
+		// bar renders its busy state instead of the trigger. So drain the Action
+		// Scheduler queue through the same `process-waiting-actions` mu-plugin this
+		// spec's data setup uses, then re-read the page. The reload races the drain,
+		// which is why this polls rather than reading once. Both inner assertions
+		// are short: a long one would spend the whole budget on a single attempt
+		// instead of draining the queue again.
+		await expect( async () => {
+			await request.get( '?process-waiting-actions' );
+			await page.reload();
+			await expect( page.getByText( 'Data status' ) ).toBeVisible( {
+				timeout: 2_000,
+			} );
+			await expect(
+				page.getByRole( 'button', {
+					name: 'Manually trigger analytics data import',
+				} )
+			).toBeVisible( { timeout: 2_000 } );
+		} ).toPass( {
+			timeout: 30_000,
+			intervals: [ 1_000, 2_000, 3_000 ],
 		} );

-		test( 'shows the manual update trigger in scheduled mode', async ( {
-			page,
-			baseURL,
-		} ) => {
-			// Switch to scheduled mode, where the manual "Update now" trigger is
-			// offered.
-			await setOption(
-				apiRequest,
-				baseURL,
-				'woocommerce_analytics_scheduled_import',
-				'yes'
-			);
+		// Clicking it is the part no other layer owns. `import-status-bar.test.tsx`
+		// mocks `../use-import-status`, so its click reaches a jest.fn() and its
+		// busy case is handed `isTriggeringImport: true` as a prop rather than
+		// reaching that state as a consequence. Break the button-to-hook wiring
+		// and everything below the browser stays green.
+		const triggerResponse = page.waitForResponse(
+			( response ) =>
+				response.url().includes( '/wc-analytics/imports/trigger' ) &&
+				response.ok()
+		);

-			// Enabling scheduled mode schedules a recurring batch import whose
-			// first run is "now" (OrdersScheduler::schedule_recurring_batch_processor
-			// → as_schedule_recurring_action( time(), … )). Until that batch runs,
-			// the status bar renders its busy "in progress" state instead of the
-			// "Update now" trigger, and wp-env has no cron to run it. So drain the
-			// Action Scheduler queue via ?process-waiting-actions — exactly as this
-			// spec's data setup does — until the batch clears and the button
-			// settles to idle. Poll because the reload races the drain.
-			const updateButton = page.getByRole( 'button', {
+		await page
+			.getByRole( 'button', {
 				name: 'Manually trigger analytics data import',
-			} );
-			await expect( async () => {
-				await page.request.get( '?process-waiting-actions' );
-				await page.reload();
-				await expect( page.getByText( 'Data status' ) ).toBeVisible();
-				await expect( updateButton ).toBeVisible( { timeout: 2_000 } );
-			} ).toPass( {
-				timeout: 30_000,
-				intervals: [ 1_000, 2_000, 3_000 ],
-			} );
+			} )
+			.click();

-			// Clicking it fires the import and flips the button to a busy state.
-			const responsePromise = page.waitForResponse(
-				( response ) =>
-					response
-						.url()
-						.includes( '/wc-analytics/imports/trigger' ) &&
-					response.ok()
-			);
-			await updateButton.click();
+		await triggerResponse;

-			const busyButton = page.getByRole( 'button', {
+		await expect(
+			page.getByRole( 'button', {
 				name: 'Analytics data import in progress',
-			} );
-			await expect( busyButton ).toBeDisabled();
-			await responsePromise;
-		} );
+			} )
+		).toBeDisabled();

-		test( 'hides the manual update trigger in immediate mode', async ( {
-			page,
-			baseURL,
-		} ) => {
-			// In immediate mode there is no batch import to trigger, so the
-			// status bar (and its button) are not rendered.
-			await setOption(
-				apiRequest,
-				baseURL,
-				'woocommerce_analytics_scheduled_import',
-				'no'
-			);
-			await page.reload();
+		// Leave the option as this file's beforeAll set it. The afterAll restores
+		// the install default either way; this is for whatever is appended here next.
+		await restApi.post(
+			'wc-analytics/settings/wc_admin/woocommerce_analytics_scheduled_import',
+			{ value: 'no' }
+		);
+	}
+);

-			await expect( page.getByText( 'Data status' ) ).toBeHidden();
+test(
+	'loads the overview performance indicators from their own endpoint',
+	{
+		tag: [ tags.PAYMENTS, tags.SERVICES ],
+		lock: locks.ANALYTICS_IMPORT_MODE,
+	},
+	async ( { page } ) => {
+		// `performance-indicators` has no browser owner otherwise. The Jest
+		// suite mocks `@woocommerce/date` wholesale and answers from a fixture
+		// keyed on its own `appendTimestamp` stub, so a renamed endpoint or a
+		// broken date boundary cannot turn it red. Month to date runs from the
+		// first of the month up to now. The tiles send `before` as the current
+		// time and can ask again up to 23:59:59 on a re-render, so require any
+		// `before` today that is no earlier than the moment this test started.
+		const now = new Date();
+		const today = toUtcDate( now );
+		const expectedAfter = `${ toUtcDate(
+			new Date( Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), 1 ) )
+		) }T00:00:00`;
+		const startedAt = now.toISOString().slice( 0, 19 );
+
+		const indicatorsResponse = page.waitForResponse( ( response ) => {
+			const requestUrl = new URL( response.url() );
+			const before = requestUrl.searchParams.get( 'before' ) ?? '';
+
+			return (
+				requestUrl.pathname.endsWith(
+					'/wc-analytics/reports/performance-indicators'
+				) &&
+				requestUrl.searchParams.get( 'after' ) === expectedAfter &&
+				before.startsWith( `${ today }T` ) &&
+				before >= startedAt &&
+				response.ok()
+			);
 		} );
+
+		await page.goto(
+			'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview'
+		);
+
+		await indicatorsResponse;
+
+		// Each default indicator by label with some value beside it, rather than
+		// a store-wide figure: the shared store makes exact totals flaky across
+		// parallel specs.
+		//
+		// 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` the indicator picker also contributes 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();
+		}
 	}
 );