Commit e38c4f8ba4e for woocommerce

commit e38c4f8ba4e5292f209a34ebd05c3866716e9ebf
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Wed Sep 16 13:16:20 2026 +0300

    [tests] Demote settings persistence to PHPUnit and add a WooCommerce.com screen canary (#68644)

    * test(settings): Move settings persistence below E2E

    Three browser titles across two settings screens asserted that a form
    saves. can update settings filled the general screen's store address,
    currency, price-format and selling-location fields, saved, and reloaded
    to check each value came back. settings-woo-com.spec.ts did the same
    for the two WooCommerce.com checkboxes, one title each.

    Most of that is a server decision and does not need a browser.
    class-wc-settings-general-test.php gains
    test_save_persists_general_setting_values, which posts the same fields
    and reads the options back.
    class-wc-settings-advanced-test.php gains
    test_save_persists_woocommerce_com_checkbox_options, provider-driven
    over both checkboxes.

    settings-general.spec.ts keeps one title and rewrites it, which is the
    larger part of this diff and not a tidy-up. It still asserts the client
    state no PHP test can see - Save changes disabled, enabled by a change,
    disabled again after saving - and it now also asserts that each
    selling-location mode exposes only its own conditional country row.
    Then it saves, reloads, and checks the three settings it changed came
    back. That last part is a browser assertion that a form persists, which
    is the category this commit otherwise moves down; it is kept because
    those three settings are what drive the conditional rows the title
    exists to check, and re-reading them after a reload is how the title
    proves the form was rebuilt from stored state rather than from memory.

    So the browser coverage that actually goes away is the rest of the
    general screen: store address, city and postcode, currency, currency
    position, the price separators and the decimal count. Their values are
    now asserted as options rather than as re-rendered input values, and
    the select2-enhanced country control the old title drove is no longer
    exercised anywhere.

    A canary is added, because deleting settings-woo-com.spec.ts removed
    the only test that opens the WooCommerce.com section. Both label
    strings do survive elsewhere: the API settings CRUD test asserts them
    as the REST description field, from the same desc in
    class-wc-settings-advanced.php, so a rename would fail there too. What
    nothing covers after the deletion is that the section renders, and that
    each label is associated with its checkbox. The canary resolves both
    controls by role and accessible name and asserts nothing else; it saves
    nothing, since persistence is the PHPUnit test's job.

    Two defects in the new Advanced test are fixed here rather than
    carried. It seeded both checkboxes to no and then asserted the peer was
    still no, which could not fail; the peer now starts at yes, so the
    assertion proves the save cleared it. And it detached the two tracking
    callbacks but left Notes::possibly_delete_marketing_notes attached,
    which deletes every marketing note when the suggestions option changes
    and which no teardown can undo; it is now detached and restored the
    same way. The test also restores each option's autoload flag through
    raw SQL and reads back past the object cache, matching what the General
    test in this commit already did.

    Consolidates two mega-branch commits, subjects verbatim:
    - test(settings): Move general persistence below E2E
    - test(settings): Move WooCommerce.com options below E2E

    Refs TESTOPS-288
    Refs #68046

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

    * test(settings): Stop the settings save tests restoring base-class state

    Both new save tests snapshotted every option they were about to write,
    straight out of the options table with $wpdb, and wrote the rows back
    in a finally block along with $_POST and a handful of wp_cache_delete
    calls. tear_down() rolls the transaction back before anything else
    runs, and clean_up_global_scope() empties the request globals and
    flushes the object cache before the next test reads either.

    The Advanced test also re-attached the tracking and marketing-note
    callbacks it had detached. _restore_hooks() rebuilds $wp_filter from
    the suite's baseline every teardown, so the re-attach was redundant;
    the detach stays, because it has to hold for the duration of the test.

    $GLOBALS['current_section'] is the one thing here the base class does
    not own, so both tests still restore it.

    get_raw_option_state() and restore_raw_option_state() were added by
    this PR and had no other caller, so they go too. That is where the
    assertion count moves: 31 tests and 79 assertions before, 31 tests and
    55 after. The 24 that went were restore_raw_option_state() asserting
    its own writes succeeded, one per option it put back -- none of them
    about the settings behaviour under test.

    Refs TESTOPS-288

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

    * test(settings): Assert the persisted General values, not the option cache

    Two problems in the same loop, both of which made the save path less
    covered than it looks.

    The reads did not reach the database. get_option() serves autoloaded
    options out of the 'alloptions' blob and only falls through to the
    per-option key when the option is missing from it, so deleting just the
    per-option key read back whatever update_option() had cached. The
    raw $wpdb helpers this test replaced were reading the row precisely to
    avoid that. Deleting 'alloptions' as well makes the check real -- and it
    immediately failed: save() puts the number of decimals through absint(),
    so the cache held int 1 while the row held the string '1'. The
    expectation was pinned to the in-memory representation. It now expects
    '1', which is what is actually stored.

    Three of the posted values were also the install defaults --
    default_customer_address 'base', enable_coupons 'yes',
    calc_discounts_sequentially 'no'. update_option() returns early on an
    unchanged value, so dropping any of those three from get_settings()
    entirely would have left this test green. They now post values that
    differ from the defaults, so the assertions depend on the save actually
    running.

    Suite unchanged at 15096 tests and 58694 assertions.

    Refs TESTOPS-288

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

    * test(settings): Add a testdox to the General settings save test

    Every other test method in this batch describes itself with @testdox;
    this one had a plain docblock sentence instead.

    Refs TESTOPS-288

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

    * test(settings): Stop detaching the marketing notes callback

    The WooCommerce.com checkbox test detached
    Notes::possibly_delete_marketing_notes before saving
    woocommerce_show_marketplace_suggestions. The notes that callback
    deletes are rows inside the per-test transaction, so the rollback puts
    them back and the detach bought nothing.

    The tracking callbacks stay detached: their side effects reach past the
    database.

    Refs TESTOPS-288

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

    * test(settings): Clear alloptions before reading back tracking options

    The WooCommerce.com checkbox test deletes each option's own cache
    key so that get_option() rereads the saved value. That only works
    for options that are not autoloaded. woocommerce_allow_tracking is
    registered with autoload => true, and get_option() answers
    autoloaded options from the alloptions cache, so its delete did
    nothing and the read came from the cache the save had just
    refreshed.

    Delete alloptions as well, so both options are read back from the
    database. CodeRabbit raised this in its review of this PR.

    Refs TESTOPS-288

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

    * test(settings): Post the checkbox values a browser submits

    The General and WooCommerce.com save tests stand in for the browser
    specs this PR removes, but neither posted what a browser sends. A
    ticked checkbox renders value="1", and an unticked one is left out of
    the request entirely. The tests posted 'yes' and 'no' instead.

    save_fields accepts both shapes, so the tests could not see the
    browser's shape break. Three mutations that break real saves left both
    tests green: ignoring '1', saving a missing checkbox as 'yes', and
    skipping a missing checkbox without writing it.

    Post '1' for ticked boxes and leave the unticked coupons box out of the
    request. All three mutations now fail. CodeRabbit asked for a browser
    save round-trip on the WooCommerce.com canary; this takes the part of
    that concern PHPUnit can carry, while the shared browser save path
    stays covered by the tax and downloadable product settings specs.

    Refs TESTOPS-288

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

    * test(settings): Check the WooCommerce.com screen renders Save changes

    CodeRabbit asked the WooCommerce.com canary to save both checkboxes
    through the browser. The canary stays render-only: the checkbox save
    path is shared with every classic settings screen, and the tax and
    downloadable product settings specs already drive it end to end.

    One part of submitting this form is decided per section, though. A
    section can set $hide_save_button, as Blueprint does, and the settings
    UI mount does the same for any section it takes over. If that happened
    on the WooCommerce.com section, the canary and the PHPUnit save tests
    would all stay green while nobody could save the privacy opt-ins.

    Assert that Save changes renders. Hiding it for woocommerce_com in
    WC_Settings_Advanced::output() fails the canary at that assertion,
    after both checkbox assertions pass, and restoring the file passes it
    again.

    Refs TESTOPS-288

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

    ---------

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

diff --git a/plugins/woocommerce/changelog/testops-288-settings-persistence b/plugins/woocommerce/changelog/testops-288-settings-persistence
new file mode 100644
index 00000000000..26d390d8f59
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-settings-persistence
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move general and WooCommerce.com settings persistence from E2E to PHPUnit.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/settings/settings-general.spec.ts b/plugins/woocommerce/tests/e2e/tests/settings/settings-general.spec.ts
index cc5947f886f..1e25dcd63b4 100644
--- a/plugins/woocommerce/tests/e2e/tests/settings/settings-general.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/settings/settings-general.spec.ts
@@ -11,56 +11,33 @@ import { ADMIN_STATE_PATH } from '../../playwright.config';

 test.describe( 'WooCommerce General Settings', { tag: tags.SERVICES }, () => {
 	test.use( { storageState: ADMIN_STATE_PATH } );
+	const persistedSettingIds = [
+		'woocommerce_allowed_countries',
+		'woocommerce_specific_allowed_countries',
+		'woocommerce_default_country',
+	];
+	const originalSettingValues = new Map< string, string | string[] >();
+
+	test.beforeAll( async ( { restApi } ) => {
+		for ( const settingId of persistedSettingIds ) {
+			const response = await restApi.get(
+				`${ WC_API_PATH }/settings/general/${ settingId }`
+			);
+			originalSettingValues.set( settingId, response.data.value );
+		}
+	} );

 	test.afterAll( async ( { restApi } ) => {
-		await restApi.post( `${ WC_API_PATH }/settings/general/batch`, {
-			update: [
-				{
-					id: 'woocommerce_store_address',
-					value: 'addr 1',
-				},
-				{
-					id: 'woocommerce_store_city',
-					value: 'San Francisco',
-				},
-				{
-					id: 'woocommerce_default_country',
-					value: 'US:CA',
-				},
-				{
-					id: 'woocommerce_store_postcode',
-					value: '94107',
-				},
-				{
-					id: 'woocommerce_currency_pos',
-					value: 'left',
-				},
-				{
-					id: 'woocommerce_price_thousand_sep',
-					value: ',',
-				},
-				{
-					id: 'woocommerce_price_decimal_sep',
-					value: '.',
-				},
-				{
-					id: 'woocommerce_price_num_decimals',
-					value: '2',
-				},
-			],
-		} );
-		await restApi.put(
-			`${ WC_API_PATH }/settings/general/woocommerce_allowed_countries`,
-			{
-				value: 'all',
-			}
-		);
-		await restApi.put(
-			`${ WC_API_PATH }/settings/general/woocommerce_currency`,
-			{
-				value: 'USD',
-			}
+		const update = Array.from(
+			originalSettingValues,
+			( [ id, value ] ) => ( { id, value } )
 		);
+
+		if ( update.length ) {
+			await restApi.post( `${ WC_API_PATH }/settings/general/batch`, {
+				update,
+			} );
+		}
 	} );

 	test(
@@ -81,102 +58,61 @@ test.describe( 'WooCommerce General Settings', { tag: tags.SERVICES }, () => {
 				page.getByRole( 'button', { name: 'Save changes' } )
 			).toBeDisabled();

-			// Change the base location
+			const allExceptCountriesRow = page.locator( 'tr' ).filter( {
+				has: page.locator( '#woocommerce_all_except_countries' ),
+			} );
+			const specificCountriesRow = page.locator( 'tr' ).filter( {
+				has: page.locator( '#woocommerce_specific_allowed_countries' ),
+			} );
+
+			// Changing the selling location marks the form as dirty and exposes
+			// only the conditional country control for the selected mode.
 			await page
-				.locator( 'select[name="woocommerce_default_country"]' )
-				.selectOption( 'US:NC' );
+				.locator( '#woocommerce_allowed_countries' )
+				.selectOption( 'all_except' );
+			await expect( allExceptCountriesRow ).toBeVisible();
+			await expect( specificCountriesRow ).toBeHidden();

 			// See the Save changes button is now enabled.
 			await expect( page.locator( 'text=Save changes' ) ).toBeEnabled();
-		}
-	);
-
-	test( 'can update settings', async ( { page } ) => {
-		await page.goto( 'wp-admin/admin.php?page=wc-settings' );
-
-		// make sure the general tab is active
-		await expect( page.locator( 'a.nav-tab-active' ) ).toContainText(
-			'General'
-		);
-
-		// Set selling location to something different so we can save.
-		await page
-			.locator( '#woocommerce_allowed_countries' )
-			.selectOption( 'all_except' );

-		// Set the new store address
-		await page.locator( '#woocommerce_store_address' ).fill( '5th Avenue' );
-		await page.locator( '#woocommerce_store_city' ).fill( 'New York' );
-		await page.locator( '#woocommerce_store_postcode' ).fill( '10010' );
-		await page
-			.locator( 'select[name="woocommerce_currency"]' )
-			.selectOption( 'CAD' );
-
-		// Set selling location to all countries first so we can
-		// choose California as base location.
-		await page
-			.locator( '#woocommerce_allowed_countries' )
-			.selectOption( 'all' );
-
-		// Set selling location to specific countries first, so we can choose U.S as base location (without state).
-		// This will makes specific countries option appears.
-		await page
-			.locator( '#woocommerce_allowed_countries' )
-			.selectOption( 'specific' );
-		await page
-			.locator(
-				'select[data-placeholder="Choose countries / regions…"] >> nth=1'
-			)
-			.selectOption( 'US' );
-		await page
-			.locator( 'select[name="woocommerce_default_country"]' )
-			.selectOption( 'US:NY' );
-
-		// Set currency position left with space
-		await page
-			.locator( 'select[name="woocommerce_currency_pos"]' )
-			.selectOption( 'left_space' );
+			await page
+				.locator( '#woocommerce_allowed_countries' )
+				.selectOption( 'all' );
+			await expect( allExceptCountriesRow ).toBeHidden();
+			await expect( specificCountriesRow ).toBeHidden();

-		// Set currency options
-		await page.locator( '#woocommerce_price_thousand_sep' ).fill( '.' );
-		await page.locator( '#woocommerce_price_decimal_sep' ).fill( ',' );
-		await page.locator( '#woocommerce_price_num_decimals' ).fill( '1' );
+			await page
+				.locator( '#woocommerce_allowed_countries' )
+				.selectOption( 'specific' );
+			await expect( allExceptCountriesRow ).toBeHidden();
+			await expect( specificCountriesRow ).toBeVisible();
+			await page
+				.locator( '#woocommerce_specific_allowed_countries' )
+				.selectOption( 'US' );

-		// Save settings and verify the changes
-		await page.getByRole( 'button', { name: 'Save changes' } ).click();
-		await expect( page.locator( 'div.updated.inline' ) ).toContainText(
-			'Your settings have been saved.'
-		);
+			// Change the base location and persist the assembled form.
+			await page
+				.locator( 'select[name="woocommerce_default_country"]' )
+				.selectOption( 'US:NY' );
+			await page.getByRole( 'button', { name: 'Save changes' } ).click();
+			await expect( page.locator( 'div.updated.inline' ) ).toContainText(
+				'Your settings have been saved.'
+			);

-		await expect(
-			page.locator( '#woocommerce_store_address' )
-		).toHaveValue( '5th Avenue' );
-		await expect( page.locator( '#woocommerce_store_city' ) ).toHaveValue(
-			'New York'
-		);
-		await expect(
-			page.locator( '#woocommerce_store_postcode' )
-		).toHaveValue( '10010' );
-		await expect(
-			page.locator( 'select[name="woocommerce_default_country"]' )
-		).toHaveValue( 'US:NY' );
-		await expect(
-			page.locator( 'select[name="woocommerce_currency"]' )
-		).toHaveValue( 'CAD' );
-		await expect(
-			page.locator( '#woocommerce_allowed_countries' )
-		).toHaveValue( 'specific' );
-		await expect(
-			page.locator( '#woocommerce_price_thousand_sep' )
-		).toHaveValue( '.' );
-		await expect( page.locator( '#woocommerce_currency_pos' ) ).toHaveValue(
-			'left_space'
-		);
-		await expect(
-			page.locator( '#woocommerce_price_decimal_sep' )
-		).toHaveValue( ',' );
-		await expect(
-			page.locator( '#woocommerce_price_num_decimals' )
-		).toHaveValue( '1' );
-	} );
+			await page.reload();
+			await expect(
+				page.locator( '#woocommerce_allowed_countries' )
+			).toHaveValue( 'specific' );
+			await expect(
+				page.locator( '#woocommerce_specific_allowed_countries' )
+			).toHaveValues( [ 'US' ] );
+			await expect(
+				page.locator( 'select[name="woocommerce_default_country"]' )
+			).toHaveValue( 'US:NY' );
+			await expect(
+				page.getByRole( 'button', { name: 'Save changes' } )
+			).toBeDisabled();
+		}
+	);
 } );
diff --git a/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com-screen.spec.ts b/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com-screen.spec.ts
new file mode 100644
index 00000000000..dcb741b2008
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com-screen.spec.ts
@@ -0,0 +1,56 @@
+/**
+ * Internal dependencies
+ */
+import { tags, test, expect } from '../../fixtures/fixtures';
+import { ADMIN_STATE_PATH } from '../../playwright.config';
+
+/**
+ * Canary for the WooCommerce.com settings screen.
+ *
+ * The two persistence tests that used to live here moved to
+ * WC_Settings_Advanced_Test::test_save_persists_woocommerce_com_checkbox_options, which asserts
+ * what the server does with the posted values. Nothing else in the suite opens this section.
+ *
+ * The label strings themselves are not unowned: tests/e2e/tests/api-tests/settings/settings-crud.test.ts
+ * asserts both as the REST description field, from the same `desc` in class-wc-settings-advanced.php,
+ * so a rename would fail there too. What no other test covers is that this section renders at all,
+ * and that each label is associated with its checkbox — which is what getByRole resolves by
+ * accessible name, and what makes a privacy control reachable by assistive technology. The API test
+ * reads a JSON field and renders nothing.
+ *
+ * It also asserts the Save changes button renders, because a section can hide it through
+ * `$hide_save_button` and the browser save tests on other screens would not notice.
+ *
+ * It deliberately does not save anything: persistence is the PHPUnit test's job.
+ */
+test.describe(
+	'WooCommerce.com Settings screen',
+	{
+		tag: [ tags.SERVICES, tags.SKIP_ON_WPCOM ],
+	},
+	() => {
+		test.use( { storageState: ADMIN_STATE_PATH } );
+
+		test( 'renders both opt-in controls with their accessible labels', async ( {
+			page,
+		} ) => {
+			await page.goto(
+				'wp-admin/admin.php?page=wc-settings&tab=advanced&section=woocommerce_com'
+			);
+
+			await expect(
+				page.getByRole( 'checkbox', {
+					name: 'Allow usage of WooCommerce to be tracked',
+				} )
+			).toBeVisible();
+			await expect(
+				page.getByRole( 'checkbox', {
+					name: 'Display suggestions within WooCommerce',
+				} )
+			).toBeVisible();
+			await expect(
+				page.getByRole( 'button', { name: 'Save changes' } )
+			).toBeVisible();
+		} );
+	}
+);
diff --git a/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com.spec.ts b/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com.spec.ts
deleted file mode 100644
index d239dc8ce7c..00000000000
--- a/plugins/woocommerce/tests/e2e/tests/settings/settings-woo-com.spec.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * External dependencies
- */
-import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { tags, test, expect } from '../../fixtures/fixtures';
-import { ADMIN_STATE_PATH } from '../../playwright.config';
-
-test.describe(
-	'WooCommerce woo.com Settings',
-	{
-		tag: [ tags.SERVICES, tags.SKIP_ON_WPCOM ],
-	},
-	() => {
-		test.use( { storageState: ADMIN_STATE_PATH } );
-
-		test.beforeAll( async ( { restApi } ) => {
-			await restApi.put(
-				`${ WC_API_PATH }/settings/advanced/woocommerce_allow_tracking`,
-				{
-					value: 'no',
-				}
-			);
-			await restApi.put(
-				`${ WC_API_PATH }/settings/advanced/woocommerce_show_marketplace_suggestions`,
-				{
-					value: 'no',
-				}
-			);
-		} );
-
-		test( 'can enable analytics tracking', async ( { page } ) => {
-			await page.goto(
-				'wp-admin/admin.php?page=wc-settings&tab=advanced&section=woocommerce_com'
-			);
-
-			// enable analytics tracking
-			await page
-				.getByLabel( 'Allow usage of WooCommerce to be tracked' )
-				.check();
-			await page.getByRole( 'button', { name: 'Save changes' } ).click();
-
-			// confirm setting saved
-			await expect( page.locator( 'div.updated.inline' ) ).toContainText(
-				'Your settings have been saved.'
-			);
-			await expect(
-				page.getByLabel( 'Allow usage of WooCommerce to be tracked' )
-			).toBeChecked();
-		} );
-
-		test( 'can enable marketplace suggestions', async ( { page } ) => {
-			await page.goto(
-				'wp-admin/admin.php?page=wc-settings&tab=advanced&section=woocommerce_com'
-			);
-
-			// enable marketplace suggestions
-			await page
-				.getByLabel( 'Display suggestions within WooCommerce' )
-				.check();
-			await page.getByRole( 'button', { name: 'Save changes' } ).click();
-
-			// confirm setting saved
-			await expect( page.locator( 'div.updated.inline' ) ).toContainText(
-				'Your settings have been saved.'
-			);
-			await expect(
-				page.getByLabel( 'Display suggestions within WooCommerce' )
-			).toBeChecked();
-		} );
-	}
-);
diff --git a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-advanced-test.php b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-advanced-test.php
index 0cbcc64fd14..e480e9fee31 100644
--- a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-advanced-test.php
+++ b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-advanced-test.php
@@ -140,6 +140,71 @@ class WC_Settings_Advanced_Test extends WC_Settings_Unit_Test_Case {
 		$this->assertEquals( $expected, $setting_ids_and_types );
 	}

+	/**
+	 * @testdox save should persist the selected WooCommerce.com checkbox without enabling its peer.
+	 *
+	 * @dataProvider woocommerce_com_checkbox_options_provider
+	 *
+	 * @param string $selected_option_id Selected checkbox option ID.
+	 * @param string $peer_option_id     Peer checkbox option ID.
+	 */
+	public function test_save_persists_woocommerce_com_checkbox_options( $selected_option_id, $peer_option_id ): void {
+		$had_current_section      = array_key_exists( 'current_section', $GLOBALS );
+		$original_current_section = $had_current_section ? $GLOBALS['current_section'] : null;
+
+		// Detach the tracking callbacks that react to woocommerce_allow_tracking changing:
+		// they have side effects the rollback cannot undo. `_restore_hooks()` puts them back
+		// after the test. The marketing-notes callback stays attached, because the notes it
+		// deletes are rows the rollback restores.
+		foreach ( array( 'get_tracking_history', 'handle_tracking_setting_change' ) as $method ) {
+			remove_action( 'update_option_woocommerce_allow_tracking', array( WC(), $method ), 10 );
+		}
+
+		try {
+			// The peer starts at 'yes' on purpose. Seeding both to 'no' would make the peer
+			// assertion below unfalsifiable: its expected post-state would equal its pre-state, so
+			// it would pass whether or not the save touched it. An unchecked box posts nothing and
+			// WC_Admin_Settings::save_fields writes 'no' for it, which is what is being asserted.
+			update_option( $selected_option_id, 'no' );
+			update_option( $peer_option_id, 'yes' );
+
+			// Post '1', the value a ticked checkbox submits. save_fields also accepts 'yes', which no
+			// browser sends, so posting 'yes' would not notice the '1' branch breaking.
+			$GLOBALS['current_section'] = 'woocommerce_com';
+			$_POST                      = array( $selected_option_id => '1' );
+
+			$sut = new WC_Settings_Advanced();
+			$sut->save();
+
+			// woocommerce_allow_tracking is autoloaded, and get_option() answers autoloaded options
+			// from `alloptions`, so clear that too for both values to be read from the database.
+			wp_cache_delete( $selected_option_id, 'options' );
+			wp_cache_delete( $peer_option_id, 'options' );
+			wp_cache_delete( 'alloptions', 'options' );
+
+			$this->assertSame( 'yes', get_option( $selected_option_id ) );
+			$this->assertSame( 'no', get_option( $peer_option_id ) );
+		} finally {
+			if ( $had_current_section ) {
+				$GLOBALS['current_section'] = $original_current_section;
+			} else {
+				unset( $GLOBALS['current_section'] );
+			}
+		}
+	}
+
+	/**
+	 * Provides WooCommerce.com checkbox options and their peers.
+	 *
+	 * @return array<string, array{string, string}>
+	 */
+	public function woocommerce_com_checkbox_options_provider(): array {
+		return array(
+			'analytics tracking'      => array( 'woocommerce_allow_tracking', 'woocommerce_show_marketplace_suggestions' ),
+			'marketplace suggestions' => array( 'woocommerce_show_marketplace_suggestions', 'woocommerce_allow_tracking' ),
+		);
+	}
+
 	/**
 	 * @testdox output method should invoke the output method of the appropriate class depending on the section.
 	 *
diff --git a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-general-test.php b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-general-test.php
index 002964abced..c8d23d7bc80 100644
--- a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-general-test.php
+++ b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-general-test.php
@@ -14,6 +14,86 @@ require_once __DIR__ . '/class-wc-settings-unit-test-case.php';
  */
 class WC_Settings_General_Test extends WC_Settings_Unit_Test_Case {

+	/**
+	 * @testdox The real General settings save path persists the form values.
+	 */
+	public function test_save_persists_general_setting_values() {
+		$sut                      = new WC_Settings_General();
+		$had_current_section      = array_key_exists( 'current_section', $GLOBALS );
+		$original_current_section = $had_current_section ? $GLOBALS['current_section'] : null;
+
+		try {
+			// Post what a browser submits: '1' for a ticked checkbox and nothing for an
+			// unticked one, so woocommerce_enable_coupons is left out on purpose.
+			$_POST                      = array(
+				'woocommerce_store_address'               => '5th Avenue',
+				'woocommerce_store_address_2'             => 'Suite 4',
+				'woocommerce_store_city'                  => 'New York',
+				'woocommerce_default_country'             => 'US:NY',
+				'woocommerce_store_postcode'              => '10010',
+				'woocommerce_allowed_countries'           => 'specific',
+				'woocommerce_all_except_countries'        => array( 'CA', 'FR' ),
+				'woocommerce_specific_allowed_countries'  => array( 'US', 'CA' ),
+				'woocommerce_ship_to_countries'           => 'specific',
+				'woocommerce_specific_ship_to_countries'  => array( 'US' ),
+				'woocommerce_default_customer_address'    => 'geolocation',
+				'woocommerce_calc_taxes'                  => '1',
+				'woocommerce_calc_discounts_sequentially' => '1',
+				'woocommerce_currency'                    => 'CAD',
+				'woocommerce_currency_pos'                => 'left_space',
+				'woocommerce_price_thousand_sep'          => '.',
+				'woocommerce_price_decimal_sep'           => ',',
+				'woocommerce_price_num_decimals'          => '1',
+			);
+			$GLOBALS['current_section'] = '';
+
+			$sut->save();
+
+			$expected_values = array(
+				'woocommerce_store_address'               => '5th Avenue',
+				'woocommerce_store_address_2'             => 'Suite 4',
+				'woocommerce_store_city'                  => 'New York',
+				'woocommerce_default_country'             => 'US:NY',
+				'woocommerce_store_postcode'              => '10010',
+				'woocommerce_allowed_countries'           => 'specific',
+				'woocommerce_all_except_countries'        => array( 'CA', 'FR' ),
+				'woocommerce_specific_allowed_countries'  => array( 'US', 'CA' ),
+				'woocommerce_ship_to_countries'           => 'specific',
+				'woocommerce_specific_ship_to_countries'  => array( 'US' ),
+				'woocommerce_default_customer_address'    => 'geolocation',
+				'woocommerce_calc_taxes'                  => 'yes',
+				'woocommerce_enable_coupons'              => 'no',
+				'woocommerce_calc_discounts_sequentially' => 'yes',
+				'woocommerce_currency'                    => 'CAD',
+				'woocommerce_currency_pos'                => 'left_space',
+				'woocommerce_price_thousand_sep'          => '.',
+				'woocommerce_price_decimal_sep'           => ',',
+				// A string, not an int. save() runs the value through absint(), so
+				// update_option() leaves an int in the cache while the row holds '1'.
+				// Asserting the int only passes on a cache hit.
+				'woocommerce_price_num_decimals'          => '1',
+			);
+
+			foreach ( $expected_values as $option_name => $expected_value ) {
+				// Autoloaded options are served from the 'alloptions' blob, so deleting
+				// the per-option key alone still reads back what update_option() cached
+				// rather than what was written. Drop both to reach the row.
+				wp_cache_delete( $option_name, 'options' );
+				wp_cache_delete( 'alloptions', 'options' );
+				$this->assertSame( $expected_value, get_option( $option_name ), "Unexpected persisted value for {$option_name}." );
+			}
+		} finally {
+			// The option rows and $_POST go back with the rollback and the next
+			// clean_up_global_scope(); current_section is the one the base class
+			// does not touch.
+			if ( $had_current_section ) {
+				$GLOBALS['current_section'] = $original_current_section;
+			} else {
+				unset( $GLOBALS['current_section'] );
+			}
+		}
+	}
+
 	/**
 	 * Test for get_settings (triggers the woocommerce_general_settings filter).
 	 */