Commit 17117d62d7a for woocommerce

commit 17117d62d7af18e8575c76badac66275292363dc
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Oct 5 16:46:03 2026 +0300

    [tests] Cover the email settings preview, palette and font contracts and the editor update banner, and remove unobserved email test-helper code (#68860)

    * test(admin): Assert the email update banner, preview controls and palette fill wiring

    The email editor update banner, the email settings preview fill and the
    color palette fill each carry a wiring decision that no Jest test read:
    which hook callback the banner's dismiss control reaches, whether a
    single previewable email hides the type select, and which PluginArea
    scope the palette fill registers into.

    Cover all three. The banner test drives the dismiss button and pins
    `dismiss` against `autoDismiss`, the two callbacks differing only in
    whether the `_dismissed` Tracks event fires. The preview fill test mounts
    the fill the way the settings page does and asserts the select is absent
    for one email type and present for two, with the preview iframe URL as
    the anchor. The palette test pins the registered scope, and a new
    settings-slots test pins the same literal on the PluginArea side so a
    rename on either side fails.

    Refs TESTOPS-288

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

    * test(settings): Pin the email preview hook, palette title and font list

    Three email settings contracts were asserted against the code that
    produces them, or not at all. The per-email preview markup was rendered
    by calling the method directly, so renaming the action it hangs off left
    the test green. The color palette row's title was never read. The font
    select was compared against EmailFont::$font, so removing a font shrank
    both sides of the assertion.

    Assert the action name and priority next to the rendered mount, add a
    color palette row test that pins the title and the heading it renders,
    and spell out the shipped font names and stacks as literals.

    Refs TESTOPS-288

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

    * test(e2e): Assert the email editor preview opens with preview=true

    The preview popup only renders the unsaved post because the editor
    rewrites the preview link to drop preview_nonce while keeping
    preview=true. The title checked the popup body, which also matches the
    published post, so losing preview=true stayed green.

    Assert the popup URL alongside the body text.

    Refs TESTOPS-288

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

    * test(email-editor): Load WC_Emails before mocking WC_Email in the applier tests

    Both applier test files build WC_Email mocks with getMockBuilder(), which
    reflects on the class. Running either file on its own errored out of
    every test with "Cannot call constructor", because nothing had loaded
    WC_Emails yet and the first mock attempt pulled the email classes in
    mid-reflection. In a full suite run a sibling file happened to load them
    first, so the failure only showed when the file ran alone.

    Boot WC_Emails in setUp, as the sync tracker and backfill tests already
    do. Run alone, the auto applier goes from 25 errors to 0 across 27 tests
    and the selective applier from 23 to 0 across 25.

    Refs TESTOPS-288

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

    * test(e2e): Remove the unobserved email template sync helper classes and exports

    No retained spec drove the third-party email fixture, the opted-in and
    transactional list overrides, or the Tracks recorder: their TypeScript
    entry points had no callers, so the PHP behind them could never turn on.
    The same held for the backfill trigger, the health probe, the direct
    seed and the template reset helper.

    Delete them. The Tracks recorder took the /tracks routes with it, since
    its log can no longer be written and the cleanup helper's drain assertion
    could only ever see an empty log; that helper is now cleanup only, so it
    is named for what it does. The template HTML override, the sweep trigger
    and the seeding and option endpoints stay.

    Refs TESTOPS-288

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

    * test(email-editor): Cover the preview link rewrite in the package integration suite

    update_preview_post_link strips preview_nonce and keeps preview=true, but
    no test read either. The browser title cannot: without an autosave the
    editor client builds the preview URL from the post link and appends
    preview=true itself, so the popup URL looks right even when this filter
    drops it.

    Assert the rewrite through the preview_post_link hook the editor
    registers, for an email post, a post of another type, and a call with no
    post.

    Refs TESTOPS-288

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

    * test(email-editor): Narrow the factory post id before passing it to get_post

    The WordPress post factory's create() is typed int|WP_Error, and PHPStan
    rejects passing that union to get_post(). The email-post helper in the same
    class already asserts the id is an int before returning it; the plain-post
    case now does the same, which both narrows the type for PHPStan and fails
    loudly if the fixture insert ever errors.

    Refs TESTOPS-288

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

    * test(e2e): Clear every email fixture option even when one clear fails

    resetFixtureState() awaited its four cleanup calls in sequence, so the
    first rejected request skipped the rest. The afterEach that calls it
    catches the error and carries on, which left the skipped options set
    for every test that followed in the same worker.

    Run each call in its own try/catch, collect the errors, and throw one
    AggregateError once all four have been attempted. The caller already
    collects cleanup errors the same way, so a failure is still reported.

    Exercised with stubs where the second and fourth calls reject: all four
    run, and the AggregateError carries both errors.

    Refs #68860

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

    * test(email-editor): Find the update banner by its role, not its class

    The portal, body-fallback and dismiss tests located the banner with
    `.wc-update-banner`. The banner renders `role="status"`, so the tests
    can find it the way assistive technology does and stop depending on a
    class name.

    The portal test looks for the status region inside the canvas target.
    The fallback test asserts the region's parent is document.body, which
    the class query never checked. The dismiss test drops its class check:
    finding the dismiss button by name already proves a banner rendered.

    Proved by mutation: portalling to document.body always turns the portal
    test red, and removing the body fallback turns the fallback and dismiss
    tests red.

    Refs #68860

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

    * chore: Fix test-only changelog visibility

    Keep the email coverage and helper changes out of public release notes
    by using a Comment field instead of a changelog body.

    Refs #68860

    * test: Fix email preview query assertion

    Parse the preview URL and require the preview parameter to equal true.
    This prevents unrelated query names or values from satisfying the test.

    Refs #68860

    ---------

    Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

diff --git a/packages/php/email-editor/changelog/testops-288-restore-email-settings-and-editor-coverage b/packages/php/email-editor/changelog/testops-288-restore-email-settings-and-editor-coverage
new file mode 100644
index 00000000000..9f7eadd3be6
--- /dev/null
+++ b/packages/php/email-editor/changelog/testops-288-restore-email-settings-and-editor-coverage
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Cover update_preview_post_link in the package integration suite.
diff --git a/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php b/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
index f3aaffc9f0d..c161b2f3211 100644
--- a/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
@@ -100,6 +100,42 @@ class Email_Editor_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringEndsWith( 'single-email-post-template.php', $template );
 	}

+	/**
+	 * The preview link an email post opens keeps preview=true, which is what
+	 * makes WordPress serve the unsaved revision, and drops the nonce.
+	 */
+	public function testItStripsOnlyThePreviewNonceFromAnEmailPreviewLink(): void {
+		$post = get_post( $this->create_email_post( 'publish' ) );
+
+		$link = apply_filters(
+			'preview_post_link',
+			'http://example.com/?p=1&preview=true&preview_nonce=abc123',
+			$post
+		);
+
+		$this->assertSame( 'http://example.com/?p=1&preview=true', $link );
+	}
+
+	/**
+	 * A post of another type keeps the preview link WordPress built for it.
+	 */
+	public function testItLeavesThePreviewLinkOfANonEmailPostUntouched(): void {
+		$post_id = self::factory()->post->create( array( 'post_type' => 'post' ) );
+		$this->assertIsInt( $post_id );
+		$link = 'http://example.com/?p=1&preview=true&preview_nonce=abc123';
+
+		$this->assertSame( $link, apply_filters( 'preview_post_link', $link, get_post( $post_id ) ) );
+	}
+
+	/**
+	 * Without a post to inspect the preview link passes through unchanged.
+	 */
+	public function testItLeavesThePreviewLinkUntouchedWhenThereIsNoPost(): void {
+		$link = 'http://example.com/?p=1&preview=true&preview_nonce=abc123';
+
+		$this->assertSame( $link, apply_filters( 'preview_post_link', $link, null ) );
+	}
+
 	/**
 	 * Create an email post of the registered custom type.
 	 *
diff --git a/plugins/woocommerce/changelog/testops-288-restore-email-settings-and-editor-coverage b/plugins/woocommerce/changelog/testops-288-restore-email-settings-and-editor-coverage
new file mode 100644
index 00000000000..35d21c61e3f
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-restore-email-settings-and-editor-coverage
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Improve email settings and editor test coverage and helpers; no production change.
diff --git a/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-color-palette-slotfill.test.tsx b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-color-palette-slotfill.test.tsx
index 6f3d4f92aab..bf0cf9bdc2a 100644
--- a/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-color-palette-slotfill.test.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-color-palette-slotfill.test.tsx
@@ -70,6 +70,22 @@ describe( 'registerSettingsEmailColorPaletteFill', () => {
 		}
 	);

+	it( 'registers the fill in the scope the settings slot renders', () => {
+		renderMount( true, 'no' );
+
+		registerSettingsEmailColorPaletteFill();
+
+		expect( registerPluginMock ).toHaveBeenCalledTimes( 1 );
+		const [ name, settings ] = registerPluginMock.mock.calls[ 0 ];
+		expect( name ).toBe( 'woocommerce-admin-settings-email-color-palette' );
+		// settings-slots.js renders a PluginArea with this exact scope for the
+		// wc_settings_email_color_palette_slotfill mount, and its own test pins
+		// the same literal, so a rename on either side fails.
+		expect( settings.scope ).toBe(
+			'woocommerce-email-color-palette-settings'
+		);
+	} );
+
 	it( 'does not register the fill without the auto-sync input', () => {
 		document.body.innerHTML =
 			'<div id="wc_settings_email_color_palette_slotfill"></div>';
diff --git a/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-controls.test.tsx b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-controls.test.tsx
index fa77d04b261..8b366bb4805 100644
--- a/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-controls.test.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-email/__tests__/settings-email-preview-controls.test.tsx
@@ -4,20 +4,31 @@
 import { fireEvent, render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import apiFetch from '@wordpress/api-fetch';
+import { createSlotFill, SlotFillProvider } from '@wordpress/components';
+import { registerPlugin } from '@wordpress/plugins';

 /**
  * Internal dependencies
  */
+import { SETTINGS_SLOT_FILL_CONSTANT } from '~/settings/settings-slots';
 import { EmailPreviewHeader } from '../settings-email-preview-header';
 import { emailPreviewNonce } from '../settings-email-preview-nonce';
 import { EmailPreviewType } from '../settings-email-preview-type';
+import {
+	registerSettingsEmailPreviewFill,
+	type EmailTypes,
+} from '../settings-email-preview-slotfill';

 jest.mock( '@wordpress/api-fetch', () => jest.fn() );
+jest.mock( '@wordpress/plugins', () => ( {
+	registerPlugin: jest.fn(),
+} ) );
 jest.mock( '../settings-email-preview-nonce', () => ( {
 	emailPreviewNonce: jest.fn(),
 } ) );

 const apiFetchMock = apiFetch as unknown as jest.Mock;
+const registerPluginMock = registerPlugin as jest.Mock;
 const emailPreviewNonceMock = emailPreviewNonce as jest.MockedFunction<
 	typeof emailPreviewNonce
 >;
@@ -205,3 +216,76 @@ describe( 'Email preview header', () => {
 		);
 	} );
 } );
+
+describe( 'Email preview fill', () => {
+	const { Slot } = createSlotFill( SETTINGS_SLOT_FILL_CONSTANT );
+	const previewUrl =
+		'http://example.com/wp-admin/?preview_woocommerce_mail=true';
+
+	// Mount the fill the way WC_Settings_Emails does: print the slot element
+	// with its data attributes, register the plugin, then render the plugin's
+	// element into a matching slot.
+	const renderPreviewFill = ( emailTypes: EmailTypes ) => {
+		const mount = document.createElement( 'div' );
+		mount.id = 'wc_settings_email_preview_slotfill';
+		mount.setAttribute( 'data-preview-url', previewUrl );
+		mount.setAttribute( 'data-email-types', JSON.stringify( emailTypes ) );
+		mount.setAttribute( 'data-email-setting-ids', '[]' );
+		document.body.appendChild( mount );
+
+		registerSettingsEmailPreviewFill();
+
+		expect( registerPluginMock ).toHaveBeenCalledTimes( 1 );
+		const [ , settings ] = registerPluginMock.mock.calls[ 0 ];
+
+		return render(
+			<SlotFillProvider>
+				<Slot />
+				{ settings.render() }
+			</SlotFillProvider>
+		);
+	};
+
+	beforeEach( () => {
+		apiFetchMock.mockResolvedValue( { subject: 'Preview subject' } );
+		emailPreviewNonceMock.mockReturnValue( 'preview-nonce' );
+	} );
+
+	afterEach( () => {
+		document.body.innerHTML = '';
+		registerPluginMock.mockClear();
+		apiFetchMock.mockReset();
+		emailPreviewNonceMock.mockReset();
+	} );
+
+	it( 'previews the only email type without offering the type select', async () => {
+		renderPreviewFill( [
+			{ label: 'Reset password', value: resetPasswordType },
+		] );
+
+		expect(
+			await screen.findByTitle( 'Email preview frame' )
+		).toHaveAttribute(
+			'src',
+			`${ previewUrl }&type=${ resetPasswordType }`
+		);
+		expect( screen.queryByLabelText( 'Email preview type' ) ).toBeNull();
+	} );
+
+	it( 'offers the type select when more than one email type is previewable', async () => {
+		renderPreviewFill( [
+			{ label: 'Processing order', value: processingOrderType },
+			{ label: 'Reset password', value: resetPasswordType },
+		] );
+
+		expect(
+			await screen.findByTitle( 'Email preview frame' )
+		).toHaveAttribute(
+			'src',
+			`${ previewUrl }&type=${ processingOrderType }`
+		);
+		expect( screen.getByLabelText( 'Email preview type' ) ).toHaveValue(
+			processingOrderType
+		);
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/settings/__tests__/settings-slots.test.tsx b/plugins/woocommerce/client/admin/client/settings/__tests__/settings-slots.test.tsx
new file mode 100644
index 00000000000..1f8330b9f4f
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings/__tests__/settings-slots.test.tsx
@@ -0,0 +1,64 @@
+/**
+ * External dependencies
+ */
+import { act } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { possiblyRenderSettingsSlots } from '../settings-slots';
+
+// Stand in for the real PluginArea so the scope each mount is rendered with is
+// readable from the DOM.
+jest.mock( '@wordpress/plugins', () => ( {
+	PluginArea: ( { scope }: { scope: string } ) => (
+		<div data-testid="plugin-area" data-scope={ scope } />
+	),
+} ) );
+
+const renderSlot = async ( slotElementId: string ) => {
+	const mount = document.createElement( 'div' );
+	mount.id = slotElementId;
+	document.body.appendChild( mount );
+
+	// possiblyRenderSettingsSlots mounts its own React root, which Testing
+	// Library does not wrap for us.
+	// eslint-disable-next-line testing-library/no-unnecessary-act
+	await act( async () => {
+		possiblyRenderSettingsSlots();
+	} );
+
+	return mount;
+};
+
+describe( 'possiblyRenderSettingsSlots', () => {
+	afterEach( () => {
+		document.body.innerHTML = '';
+	} );
+
+	it( 'renders the email color palette mount in the palette fill scope', async () => {
+		const mount = await renderSlot(
+			'wc_settings_email_color_palette_slotfill'
+		);
+
+		const pluginArea = mount.querySelector( '[data-testid="plugin-area"]' );
+		// settings-email-color-palette-slotfill.tsx registers its plugin with
+		// this exact scope, and its own test pins the same literal, so a rename
+		// on either side fails.
+		expect( pluginArea?.getAttribute( 'data-scope' ) ).toBe(
+			'woocommerce-email-color-palette-settings'
+		);
+	} );
+
+	it( 'renders nothing into a mount whose id is not a settings slot', () => {
+		const mount = document.createElement( 'div' );
+		mount.id = 'wc_settings_unknown_slotfill';
+		document.body.appendChild( mount );
+
+		possiblyRenderSettingsSlots();
+
+		expect(
+			mount.querySelector( '[data-testid="plugin-area"]' )
+		).toBeNull();
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/update-banner-plugin.test.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/update-banner-plugin.test.tsx
index a9624e13152..77bf78d2f0a 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/update-banner-plugin.test.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/__tests__/update-banner-plugin.test.tsx
@@ -1,7 +1,8 @@
 /**
  * External dependencies
  */
-import { render } from '@testing-library/react';
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';

 /**
  * Internal dependencies
@@ -68,7 +69,7 @@ describe( 'UpdateBannerPlugin', () => {

 		render( <UpdateBannerPlugin /> );

-		expect( target.querySelector( '.wc-update-banner' ) ).not.toBeNull();
+		expect( within( target ).getByRole( 'status' ) ).toBeInTheDocument();
 	} );

 	it( 'falls back to document.body when no canvas target is present', () => {
@@ -79,8 +80,31 @@ describe( 'UpdateBannerPlugin', () => {

 		render( <UpdateBannerPlugin /> );

-		expect(
-			document.body.querySelector( '.wc-update-banner' )
-		).not.toBeNull();
+		expect( screen.getByRole( 'status' ).parentElement ).toBe(
+			document.body
+		);
+	} );
+
+	it( 'wires the banner dismiss control to dismiss, not autoDismiss', async () => {
+		const dismiss = jest.fn();
+		const autoDismiss = jest.fn();
+		useUpdateBannerMock.mockReturnValue( {
+			...baseHookReturn,
+			shouldRender: true,
+			dismiss,
+			autoDismiss,
+		} );
+
+		render( <UpdateBannerPlugin /> );
+
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Dismiss for this session' } )
+		);
+
+		// The two callbacks differ in one observable way: only `dismiss` reports
+		// the `_dismissed` Tracks event, so the × in the default banner has to
+		// reach `dismiss` and leave `autoDismiss` untouched.
+		expect( dismiss ).toHaveBeenCalledTimes( 1 );
+		expect( autoDismiss ).not.toHaveBeenCalled();
 	} );
 } );
diff --git a/plugins/woocommerce/tests/e2e/README.md b/plugins/woocommerce/tests/e2e/README.md
index e13bcf59d97..c51733c32ec 100644
--- a/plugins/woocommerce/tests/e2e/README.md
+++ b/plugins/woocommerce/tests/e2e/README.md
@@ -229,14 +229,12 @@ How a helper is wired up depends on when it needs to be active:

 Powers the `tests/email-editor/update-propagation/` suite (RSM-146). Exposes:

-- Option-driven filter overrides for `woocommerce_email_block_template_html`, `woocommerce_email_template_sync_opted_in_emails`, and `woocommerce_transactional_emails_for_block_editor`.
-- A server-side Tracks event recorder, controlled by option `wc_test_tracks_enabled`.
-- A fake `WC_Email` subclass (`fake_thirdparty`) gated by option `wc_test_fake_third_party_email_enabled` for third-party-email scope tests.
-- REST endpoints under `/wp-json/wc-email-test-helper/v1/` for seeding posts, triggering sweeps and backfill synchronously, draining the Tracks log, and writing typed option values.
+- An option-driven filter override for `woocommerce_email_block_template_html`.
+- REST endpoints under `/wp-json/wc-email-test-helper/v1/` for seeding posts, triggering sweeps synchronously, reading post content and canonical hashes, and writing typed option values.

 The plugin is dormant when its driving options are empty. It has a `WP_DEBUG` plus `X-Playwright` header safety rail to prevent accidental activation outside test contexts.

-If a test fails with `404` on `/wp-json/wc-email-test-helper/v1/health`, the plugin isn't loaded — run `pnpm env:e2e:restart`.
+If a test fails with `404` on a `/wp-json/wc-email-test-helper/v1/` route, the plugin isn't loaded — run `pnpm env:e2e:restart`.

 The PR-tier subset of these tests can be run locally with:

diff --git a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-fake-third-party-email.php b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-fake-third-party-email.php
deleted file mode 100644
index a4f000b45cb..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-fake-third-party-email.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-/**
- * Fake third-party email for E2E scenarios 15-16.
- *
- * Registered conditionally via the woocommerce_email_classes filter in the
- * main plugin file. Stays dormant unless wc_test_fake_third_party_email_enabled='yes'.
- *
- * @package WC_Email_Template_Sync_Test_Helper
- */
-
-declare( strict_types=1 );
-
-namespace WC_Email_Template_Sync_Test_Helper;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Minimal WC_Email subclass standing in for a real third-party transactional email.
- *
- * Real third-party plugins must subclass WC_Email; the registry, post generator, and
- * divergence detector all call methods on a live WC_Email instance, so filter-only
- * registration is insufficient. This fixture provides the minimum surface those classes
- * need (id, title, description, template paths, basic subject/heading defaults) so that
- * scenarios 15 (non-opted-in) and 16 (opted-in with explicit version) can exercise the
- * full sync pipeline.
- */
-class Fake_Third_Party_Email extends \WC_Email {
-
-	/**
-	 * Constructor.
-	 */
-	public function __construct() {
-		$this->id          = 'fake_thirdparty';
-		$this->title       = 'Fake third-party email (test fixture)';
-		$this->description = 'E2E fixture for RSM-146 scope tests. Do not enable in production.';
-
-		// Safe fallback to an existing core template so that any rendering paths that
-		// reach the file loader don't fail with a missing-template error.
-		$this->template_html  = 'emails/customer-new-account.php';
-		$this->template_plain = 'emails/plain/customer-new-account.php';
-		$this->customer_email = true;
-
-		parent::__construct();
-	}
-
-	/**
-	 * Default subject (overridable via merchant settings, but tests don't touch it).
-	 */
-	public function get_default_subject(): string {
-		return 'Fake third-party email subject';
-	}
-
-	/**
-	 * Default heading (overridable via merchant settings, but tests don't touch it).
-	 */
-	public function get_default_heading(): string {
-		return 'Fake third-party email heading';
-	}
-}
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-opted-in-overrides.php b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-opted-in-overrides.php
deleted file mode 100644
index 73a5c82bb5d..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-opted-in-overrides.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-/**
- * Opted-in and transactional-emails list filters for the WC Email Template Sync test helper plugin.
- *
- * @package WC_Email_Template_Sync_Test_Helper
- */
-
-declare( strict_types=1 );
-
-namespace WC_Email_Template_Sync_Test_Helper;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Filter wrappers that augment the sync registry's opted-in list and the block-editor
- * transactional-emails list with fixture-controlled values. Dormant when the driving
- * options are empty.
- */
-class Opted_In_Overrides {
-
-	public const OPTED_IN_OPTION      = 'wc_test_opted_in_emails_override';
-	public const TRANSACTIONAL_OPTION = 'wc_test_transactional_emails_override';
-
-	/**
-	 * Register both filters.
-	 */
-	public function register(): void {
-		add_filter(
-			'woocommerce_email_template_sync_opted_in_emails',
-			array( $this, 'merge_opted_in' ),
-			100
-		);
-		add_filter(
-			'woocommerce_transactional_emails_for_block_editor',
-			array( $this, 'merge_transactional' ),
-			100
-		);
-	}
-
-	/**
-	 * Non-destructively merge the test-controlled opted-in list into the production return value.
-	 *
-	 * @param mixed $opted_in The upstream filter value (expected: array keyed by email id).
-	 * @return array The merged map.
-	 */
-	public function merge_opted_in( $opted_in ): array {
-		$opted_in = is_array( $opted_in ) ? $opted_in : array();
-		$override = get_option( self::OPTED_IN_OPTION, array() );
-		if ( ! is_array( $override ) || empty( $override ) ) {
-			return $opted_in;
-		}
-		return array_merge( $opted_in, $override );
-	}
-
-	/**
-	 * Non-destructively merge the test-controlled "registered for block editor" list with
-	 * the production return value, deduping by email id.
-	 *
-	 * @param mixed $emails The upstream filter value (expected: array of email ids).
-	 * @return array The merged list with stable numeric keys.
-	 */
-	public function merge_transactional( $emails ): array {
-		$emails   = is_array( $emails ) ? $emails : array();
-		$override = get_option( self::TRANSACTIONAL_OPTION, array() );
-		if ( ! is_array( $override ) || empty( $override ) ) {
-			return $emails;
-		}
-		return array_values( array_unique( array_merge( $emails, $override ) ) );
-	}
-}
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-rest-controller.php b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-rest-controller.php
index d8747f58823..c099e81249a 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-rest-controller.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-rest-controller.php
@@ -18,9 +18,9 @@ defined( 'ABSPATH' ) || exit;
 /**
  * Helper REST endpoints exposed under /wc-email-test-helper/v1/ for Playwright E2E tests.
  *
- * Health endpoint is open; every other route requires manage_options. The plugin's location
- * under tests/e2e/test-plugins/ — only mounted via .wp-env.e2e.json for the test environment —
- * provides the second layer of defense.
+ * Every route requires manage_options. The plugin's location under tests/e2e/test-plugins/ —
+ * only mounted via .wp-env.e2e.json for the test environment — provides the second layer
+ * of defense.
  */
 class REST_Controller {

@@ -30,16 +30,6 @@ class REST_Controller {
 	 * Register all REST routes.
 	 */
 	public function register_routes(): void {
-		register_rest_route(
-			self::NAMESPACE,
-			'/health',
-			array(
-				'methods'             => WP_REST_Server::READABLE,
-				'callback'            => array( $this, 'health' ),
-				'permission_callback' => '__return_true',
-			)
-		);
-
 		register_rest_route(
 			self::NAMESPACE,
 			'/reset-post/(?P<email_id>[a-z0-9_]+)',
@@ -123,33 +113,6 @@ class REST_Controller {
 			)
 		);

-		register_rest_route(
-			self::NAMESPACE,
-			'/trigger-backfill',
-			array(
-				'methods'             => WP_REST_Server::CREATABLE,
-				'callback'            => array( $this, 'trigger_backfill' ),
-				'permission_callback' => array( self::class, 'require_admin' ),
-			)
-		);
-
-		register_rest_route(
-			self::NAMESPACE,
-			'/tracks',
-			array(
-				array(
-					'methods'             => WP_REST_Server::READABLE,
-					'callback'            => array( $this, 'get_tracks' ),
-					'permission_callback' => array( self::class, 'require_admin' ),
-				),
-				array(
-					'methods'             => WP_REST_Server::DELETABLE,
-					'callback'            => array( $this, 'clear_tracks' ),
-					'permission_callback' => array( self::class, 'require_admin' ),
-				),
-			)
-		);
-
 		register_rest_route(
 			self::NAMESPACE,
 			'/set-option',
@@ -171,23 +134,6 @@ class REST_Controller {
 		);
 	}

-	/**
-	 * Health probe. Tests call this in beforeAll to confirm the helper plugin is loaded.
-	 *
-	 * @param WP_REST_Request $request The REST request (unused; REST callback signature requires it).
-	 * @return WP_REST_Response
-	 */
-	public function health( WP_REST_Request $request ): WP_REST_Response {
-		unset( $request );
-		return new WP_REST_Response(
-			array(
-				'ok'      => true,
-				'version' => '1.0.0',
-			),
-			200
-		);
-	}
-
 	/**
 	 * Delete the woo_email post for the given email type, clear template manager state,
 	 * then recreate a published post from the file template synchronously.
@@ -517,101 +463,6 @@ class REST_Controller {
 		);
 	}

-	/**
-	 * Run the legacy-post backfill inline, then snapshot a count of stamped posts.
-	 * Production `run()` returns false unconditionally — value reported back is for caller
-	 * convenience only. Tests assert via meta snapshots before/after.
-	 *
-	 * WC_Tracks::record_event() short-circuits when site tracking is disabled (the
-	 * default in wp-env), so it never reaches the woocommerce_tracks_event_properties
-	 * filter that Tracks_Recorder hooks into. To capture the _backfill_completed event
-	 * reliably we inject a custom recorder via set_event_recorder() that writes
-	 * directly to the Tracks_Recorder log option, then restore the default (null)
-	 * after the backfill completes.
-	 *
-	 * @param WP_REST_Request $request The REST request (unused).
-	 * @return WP_REST_Response
-	 */
-	public function trigger_backfill( WP_REST_Request $request ): WP_REST_Response {
-		unset( $request );
-
-		// Inject a direct-write recorder so _backfill_completed lands in the log
-		// even when WC_Tracks::record_event() is disabled for the test environment.
-		// The recorder is gated on the same wc_test_tracks_enabled option that
-		// Tracks_Recorder uses, so it only fires when a spy is active and the log
-		// stays empty for tests that don't attach a spy.
-		//
-		// Event names get the 'wcadmin_' prefix to match WC_Tracks::PREFIX — that's
-		// what server-side events are dispatched as via WC_Tracks::record_event().
-		// The TRACKS_EVENTS constants in classifications.ts expect the prefixed name.
-		\Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncTracker::set_event_recorder(
-			static function ( string $event_name, array $payload ): void {
-				if ( 'yes' !== get_option( Tracks_Recorder::ENABLED_OPTION, 'no' ) ) {
-					return;
-				}
-				$log = get_option( Tracks_Recorder::LOG_OPTION, array() );
-				if ( ! is_array( $log ) ) {
-					$log = array();
-				}
-				$log[] = array(
-					'name'         => 'wcadmin_' . $event_name,
-					'properties'   => $payload,
-					'timestamp_ms' => (int) ( microtime( true ) * 1000 ),
-				);
-				update_option( Tracks_Recorder::LOG_OPTION, $log, false );
-			}
-		);
-
-		$ran = \Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncBackfill::run();
-
-		// Restore the default recorder so subsequent calls don't double-log via
-		// both the injected recorder and any future WC_Tracks path.
-		\Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateSyncTracker::set_event_recorder( null );
-
-		global $wpdb;
-		$stamped = (int) $wpdb->get_var(
-			$wpdb->prepare(
-				"SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value <> ''",
-				\Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY
-			)
-		);
-
-		return new WP_REST_Response(
-			array(
-				'ran'     => (bool) $ran,
-				'stamped' => $stamped,
-			),
-			200
-		);
-	}
-
-	/**
-	 * Return the in-option Tracks event log captured by Tracks_Recorder.
-	 *
-	 * @param WP_REST_Request $request The REST request (unused).
-	 * @return WP_REST_Response
-	 */
-	public function get_tracks( WP_REST_Request $request ): WP_REST_Response {
-		unset( $request );
-		$log = get_option( Tracks_Recorder::LOG_OPTION, array() );
-		return new WP_REST_Response(
-			array( 'events' => is_array( $log ) ? $log : array() ),
-			200
-		);
-	}
-
-	/**
-	 * Clear the Tracks event log option so the next read starts empty.
-	 *
-	 * @param WP_REST_Request $request The REST request (unused).
-	 * @return WP_REST_Response
-	 */
-	public function clear_tracks( WP_REST_Request $request ): WP_REST_Response {
-		unset( $request );
-		delete_option( Tracks_Recorder::LOG_OPTION );
-		return new WP_REST_Response( array( 'cleared' => true ), 200 );
-	}
-
 	/**
 	 * Write a typed value to a WordPress option, preserving array/object structure.
 	 * Body shape: `{ "option_name": string, "option_value": mixed }`. Unlike the
@@ -653,7 +504,7 @@ class REST_Controller {
 	}

 	/**
-	 * Permission callback used by every non-health endpoint. Requires the manage_options
+	 * Permission callback used by every endpoint. Requires the manage_options
 	 * capability. The plugin is only mounted in test environments via .wp-env.e2e.json — it
 	 * does not ship in any production WooCommerce build — which provides the second
 	 * layer of defense.
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-tracks-recorder.php b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-tracks-recorder.php
deleted file mode 100644
index 8e41b611d5b..00000000000
--- a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/includes/class-tracks-recorder.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-/**
- * Server-side Tracks event recorder for the WC Email Template Sync test helper plugin.
- *
- * @package WC_Email_Template_Sync_Test_Helper
- */
-
-declare( strict_types=1 );
-
-namespace WC_Email_Template_Sync_Test_Helper;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Mirrors server-side recordEvent calls and the backfill-complete action to an option
- * so Playwright tests can drain and assert. Dormant unless wc_test_tracks_enabled=yes.
- */
-class Tracks_Recorder {
-
-	public const ENABLED_OPTION = 'wc_test_tracks_enabled';
-	public const LOG_OPTION     = 'wc_test_tracks_log';
-
-	/**
-	 * Register hooks.
-	 */
-	public function register(): void {
-		add_filter( 'woocommerce_tracks_event_properties', array( $this, 'record' ), 100, 2 );
-	}
-
-	/**
-	 * Append a record to the tracks log when enabled. Always returns the untouched properties.
-	 *
-	 * @param array  $properties Event properties (passed through unchanged).
-	 * @param string $event_name Event name.
-	 * @return array
-	 */
-	public function record( $properties, $event_name ): array {
-		if ( 'yes' !== get_option( self::ENABLED_OPTION, 'no' ) ) {
-			return is_array( $properties ) ? $properties : array();
-		}
-
-		$log = get_option( self::LOG_OPTION, array() );
-		if ( ! is_array( $log ) ) {
-			$log = array();
-		}
-
-		$log[] = array(
-			'name'         => (string) $event_name,
-			'properties'   => is_array( $properties ) ? $properties : array(),
-			'timestamp_ms' => (int) ( microtime( true ) * 1000 ),
-		);
-
-		update_option( self::LOG_OPTION, $log, false );
-
-		return is_array( $properties ) ? $properties : array();
-	}
-}
diff --git a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/wc-email-template-sync-test-helper.php b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/wc-email-template-sync-test-helper.php
index 12a10350dc0..5953884ad40 100644
--- a/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/wc-email-template-sync-test-helper.php
+++ b/plugins/woocommerce/tests/e2e/test-plugins/wc-email-template-sync-test-helper/wc-email-template-sync-test-helper.php
@@ -19,16 +19,12 @@ defined( 'ABSPATH' ) || exit;
 define( 'WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR', plugin_dir_path( __FILE__ ) );

 require_once WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR . 'includes/class-template-html-overrides.php';
-require_once WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR . 'includes/class-opted-in-overrides.php';
-require_once WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR . 'includes/class-tracks-recorder.php';
 require_once WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR . 'includes/class-rest-controller.php';

 add_action(
 	'plugins_loaded',
 	static function () {
 		( new WC_Email_Template_Sync_Test_Helper\Template_HTML_Overrides() )->register();
-		( new WC_Email_Template_Sync_Test_Helper\Opted_In_Overrides() )->register();
-		( new WC_Email_Template_Sync_Test_Helper\Tracks_Recorder() )->register();
 	},
 	20
 );
@@ -39,19 +35,3 @@ add_action(
 		( new WC_Email_Template_Sync_Test_Helper\REST_Controller() )->register_routes();
 	}
 );
-
-// Register the fake third-party WC_Email subclass ONLY when an option signals tests want it.
-// The class file is lazy-loaded here because it extends WC_Email, which is not defined
-// until the woocommerce plugin has loaded — eager-loading at plugin-bootstrap time would
-// fatal on PHP class resolution.
-add_filter(
-	'woocommerce_email_classes',
-	static function ( $emails ) {
-		if ( 'yes' !== get_option( 'wc_test_fake_third_party_email_enabled', 'no' ) ) {
-			return $emails;
-		}
-		require_once WC_EMAIL_TEMPLATE_SYNC_TEST_HELPER_DIR . 'includes/class-fake-third-party-email.php';
-		$emails['WC_Email_Template_Sync_Test_Helper\\Fake_Third_Party_Email'] = new \WC_Email_Template_Sync_Test_Helper\Fake_Third_Party_Email();
-		return $emails;
-	}
-);
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-loads.spec.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-loads.spec.ts
index 0fbb2de7819..492ab7ed3e2 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-loads.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-loads.spec.ts
@@ -133,6 +133,12 @@ test.describe(
 				// eslint-disable-next-line playwright/no-wait-for-selector -- wait for the tab to be loaded.
 				await newPage.waitForSelector( '.wp-block-heading' );
 				await page.close(); // close the original tab.
+				// The editor strips preview_nonce from the preview link but keeps
+				// preview=true, which is what makes WordPress serve the unsaved
+				// post rather than the published one.
+				expect(
+					new URL( newPage.url() ).searchParams.get( 'preview' )
+				).toBe( 'true' );
 				await expect( newPage.locator( 'body' ) ).toContainText(
 					'New order: #12345'
 				);
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
index 7c5e4f607db..ffc3c3f09c1 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/core-flows.spec.ts
@@ -22,7 +22,7 @@ import {
 	simulateCoreBump,
 	triggerDetectionSweep,
 } from './helpers/simulate-plugin-update';
-import { assertNoLeakedFixtureState } from './helpers/leaked-state-checks';
+import { resetFixtureState } from './helpers/reset-fixture-state';
 import { STATUS } from './helpers/classifications';
 import { locks } from '../../../fixtures/fixtures';

@@ -41,7 +41,7 @@ test.describe(
 			const cleanupErrors: unknown[] = [];

 			try {
-				await assertNoLeakedFixtureState();
+				await resetFixtureState();
 			} catch ( error ) {
 				cleanupErrors.push( error );
 			}
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/leaked-state-checks.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/leaked-state-checks.ts
deleted file mode 100644
index 7c5e96f848e..00000000000
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/leaked-state-checks.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * External dependencies
- */
-import { expect } from '@playwright/test';
-import { createClient } from '@woocommerce/e2e-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { admin } from '../../../../test-data/data';
-import playwrightConfig from '../../../../playwright.config';
-import { TEST_HELPER_API_BASE } from './classifications';
-import {
-	clearAllTemplateHtmlOverrides,
-	clearOptedInOverride,
-	clearTransactionalEmailsOverride,
-	disableTracksLog,
-	disableFakeThirdPartyEmail,
-} from './test-helper-plugin';
-
-const baseURL = playwrightConfig.use?.baseURL ?? '';
-
-function apiClient() {
-	return createClient( baseURL, {
-		type: 'basic',
-		username: admin.username,
-		password: admin.password,
-	} );
-}
-
-/**
- * Call from afterEach in every spec. Snapshots fixture state, force-cleans
- * everything, and asserts the snapshot was clean. A test that triggers leaked
- * cleanup fails the run even if its body assertions passed — the next test
- * still starts from a clean slate because cleanup ran before the assertion.
- */
-export async function assertNoLeakedFixtureState(): Promise< void > {
-	const client = apiClient();
-	const tracksRes = await client.get( `${ TEST_HELPER_API_BASE }/tracks` );
-	const trackCount = ( ( tracksRes?.data?.events ?? [] ) as unknown[] )
-		.length;
-
-	await clearAllTemplateHtmlOverrides();
-	await clearOptedInOverride();
-	await clearTransactionalEmailsOverride();
-	await disableTracksLog();
-	await disableFakeThirdPartyEmail();
-	await client.delete( `${ TEST_HELPER_API_BASE }/tracks`, {} );
-
-	expect( trackCount, 'Tracks log not drained at end of test' ).toBe( 0 );
-}
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/reset-fixture-state.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/reset-fixture-state.ts
new file mode 100644
index 00000000000..922f020dda5
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/reset-fixture-state.ts
@@ -0,0 +1,36 @@
+/**
+ * Internal dependencies
+ */
+import {
+	clearAllTemplateHtmlOverrides,
+	clearOptedInOverride,
+	clearTransactionalEmailsOverride,
+	disableFakeThirdPartyEmail,
+} from './test-helper-plugin';
+
+/**
+ * Call from afterEach in every spec. Clears every option the test helper
+ * plugin reads, so the next test starts from a store the fixtures have not
+ * touched. Every option is cleared even when clearing another one fails, so
+ * one failed request cannot leave the rest set for the tests that follow.
+ */
+export async function resetFixtureState(): Promise< void > {
+	const errors: unknown[] = [];
+
+	for ( const clear of [
+		clearAllTemplateHtmlOverrides,
+		clearOptedInOverride,
+		clearTransactionalEmailsOverride,
+		disableFakeThirdPartyEmail,
+	] ) {
+		try {
+			await clear();
+		} catch ( error ) {
+			errors.push( error );
+		}
+	}
+
+	if ( errors.length > 0 ) {
+		throw new AggregateError( errors, 'Fixture state reset failed.' );
+	}
+}
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/seed-woo-email.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/seed-woo-email.ts
index 9b39c38ce05..dfa0f1f3b1d 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/seed-woo-email.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/seed-woo-email.ts
@@ -123,62 +123,6 @@ export async function seedWooEmailPost(
 	return postId;
 }

-/**
- * Create a woo_email post directly via the seed-bulk endpoint, bypassing the
- * WCTransactionalEmailPostsGenerator. The created post has no entry in the
- * options-table mapping used by WCTransactionalEmailPostsManager, so the
- * backfill and divergence-sweep pipelines cannot resolve its email_id and
- * will skip it entirely.
- *
- * Use this for scenarios that need a woo_email post for an email type that is
- * NOT in the sync registry (e.g. a third-party email that is registered as a
- * WC_Email subclass but is not enrolled in the block-editor transactional
- * emails list).
- */
-export async function seedWooEmailPostDirect(
-	seed: Pick< WooEmailSeed, 'postContent' | 'stripStampMeta' >
-): Promise< number > {
-	const meta: Record< string, unknown > = {};
-
-	if ( seed.stripStampMeta ) {
-		meta[ META_KEYS.STATUS ] = null;
-		meta[ META_KEYS.SOURCE_HASH ] = null;
-		meta[ META_KEYS.SOURCE_VERSION ] = null;
-		meta[ META_KEYS.LAST_SYNCED_AT ] = null;
-		meta[ META_KEYS.BACKFILLED ] = null;
-	}
-
-	const postData: Record< string, unknown > = {
-		post_type: 'woo_email',
-		post_status: 'publish',
-	};
-	if ( seed.postContent !== undefined ) {
-		postData.post_content = seed.postContent;
-	}
-
-	const client = apiClient();
-	const res = await client.post( `${ TEST_HELPER_API_BASE }/seed-bulk`, {
-		seeds: [
-			{
-				post: postData,
-				meta,
-			},
-		],
-	} );
-
-	const results: Array< { post_id?: number; error?: string } > =
-		res?.data?.results ?? [];
-	const first = results[ 0 ];
-	if ( ! first?.post_id ) {
-		throw new Error(
-			`seedWooEmailPostDirect: failed to create post — ${
-				first?.error ?? 'no post_id returned'
-			}`
-		);
-	}
-	return Number( first.post_id );
-}
-
 export async function getWooEmailPostContent(
 	postId: number
 ): Promise< string > {
@@ -188,38 +132,3 @@ export async function getWooEmailPostContent(
 	);
 	return String( res?.data?.post_content ?? '' );
 }
-
-export type ResetResult = {
-	content: string;
-	version: string | null;
-	source_hash: string | null;
-	synced_at: string | null;
-	/** The post-reset sync status (e.g. "in_sync") for sync-enabled emails, or null otherwise. */
-	status: string | null;
-};
-
-/**
- * Call the /reset endpoint for a woo_email post using basic-auth credentials,
- * bypassing the cookie+nonce requirement of the WP REST API for authenticated
- * cookie sessions. Resets the post content to the canonical WooCommerce template.
- *
- * The reset endpoint returns the post-reset sync status (for example,
- * "in_sync") in the `status` field.
- */
-export async function resetWooEmailTemplate(
-	postId: number
-): Promise< ResetResult > {
-	const client = apiClient();
-	const res = await client.post(
-		`woocommerce-email-editor/v1/emails/${ postId }/reset`,
-		{}
-	);
-	if ( res?.data?.content === undefined ) {
-		throw new Error(
-			`resetWooEmailTemplate: unexpected response for post ${ postId }: ${ JSON.stringify(
-				res?.data
-			) }`
-		);
-	}
-	return res.data as ResetResult;
-}
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/simulate-plugin-update.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/simulate-plugin-update.ts
index ed919dcfddc..f310d3a8dd3 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/simulate-plugin-update.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/simulate-plugin-update.ts
@@ -43,22 +43,6 @@ export async function triggerDetectionSweep(): Promise< {
 	};
 }

-export async function triggerBackfill(): Promise< {
-	ran: boolean;
-	stamped: number;
-} > {
-	const client = apiClient();
-	const res = await client.post(
-		`${ TEST_HELPER_API_BASE }/trigger-backfill`,
-		{}
-	);
-	const body = res?.data ?? {};
-	return {
-		ran: Boolean( body.ran ),
-		stamped: Number( body.stamped ?? 0 ),
-	};
-}
-
 /**
  * Simulate a core template version bump by seeding `oldHtml` as the active
  * canonical-content override for `emailId`. While the override is active, any
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/test-helper-plugin.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/test-helper-plugin.ts
index 60ce3fc5daf..ac14c5afb74 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/test-helper-plugin.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/update-propagation/helpers/test-helper-plugin.ts
@@ -16,7 +16,6 @@ const OPTIONS = {
 	TEMPLATE_HTML_OVERRIDE: 'wc_test_template_html_override',
 	OPTED_IN_OVERRIDE: 'wc_test_opted_in_emails_override',
 	TRANSACTIONAL_OVERRIDE: 'wc_test_transactional_emails_override',
-	TRACKS_ENABLED: 'wc_test_tracks_enabled',
 	FAKE_THIRD_PARTY_EMAIL_ENABLED: 'wc_test_fake_third_party_email_enabled',
 } as const;

@@ -58,38 +57,14 @@ export async function clearAllTemplateHtmlOverrides(): Promise< void > {
 	await clearTemplateHtmlOverride();
 }

-export async function setOptedInOverride(
-	overrides: Record< string, { version: string } >
-): Promise< void > {
-	await setOption( OPTIONS.OPTED_IN_OVERRIDE, overrides );
-}
-
 export async function clearOptedInOverride(): Promise< void > {
 	await deleteOption( OPTIONS.OPTED_IN_OVERRIDE );
 }

-export async function setTransactionalEmailsOverride(
-	emailIds: string[]
-): Promise< void > {
-	await setOption( OPTIONS.TRANSACTIONAL_OVERRIDE, emailIds );
-}
-
 export async function clearTransactionalEmailsOverride(): Promise< void > {
 	await deleteOption( OPTIONS.TRANSACTIONAL_OVERRIDE );
 }

-export async function enableTracksLog(): Promise< void > {
-	await setOption( OPTIONS.TRACKS_ENABLED, 'yes' );
-}
-
-export async function disableTracksLog(): Promise< void > {
-	await deleteOption( OPTIONS.TRACKS_ENABLED );
-}
-
-export async function enableFakeThirdPartyEmail(): Promise< void > {
-	await setOption( OPTIONS.FAKE_THIRD_PARTY_EMAIL_ENABLED, 'yes' );
-}
-
 export async function disableFakeThirdPartyEmail(): Promise< void > {
 	await deleteOption( OPTIONS.FAKE_THIRD_PARTY_EMAIL_ENABLED );
 }
diff --git a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-emails-test.php b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-emails-test.php
index 79f3135e397..fc4c9a18e25 100644
--- a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-emails-test.php
+++ b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-emails-test.php
@@ -9,7 +9,6 @@ declare( strict_types = 1 );

 use Automattic\WooCommerce\Internal\Admin\EmailPreview\EmailPreview;
 use Automattic\WooCommerce\Internal\Email\EmailColors;
-use Automattic\WooCommerce\Internal\Email\EmailFont;
 use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\StaticMockerHack;

 require_once __DIR__ . '/class-wc-settings-unit-test-case.php';
@@ -135,22 +134,46 @@ class WC_Settings_Emails_Test extends WC_Settings_Unit_Test_Case {
 	}

 	/**
-	 * @testdox The email font setting renders every supported font and the selected value.
+	 * @testdox The email font setting renders every shipped font, with its stack, and the selected value.
 	 */
 	public function test_email_font_family_setting_contract(): void {
-		$settings_by_id = $this->index_settings_by_id( ( new WC_Settings_Emails() )->get_settings_for_section( '' ) );
+		// Spelled out rather than read from EmailFont so that dropping a font
+		// from the shipped list fails here instead of shrinking both sides.
+		$expected_fonts = array(
+			'Arial'           => "Arial, 'Helvetica Neue', Helvetica, sans-serif",
+			'Comic Sans MS'   => "'Comic Sans MS', 'Marker Felt-Thin', Arial, sans-serif",
+			'Courier New'     => "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace",
+			'Georgia'         => "Georgia, Times, 'Times New Roman', serif",
+			'Helvetica'       => "'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif",
+			'Lucida'          => "'Lucida Sans Unicode', 'Lucida Grande', sans-serif",
+			'Tahoma'          => 'Tahoma, Verdana, Segoe, sans-serif',
+			'Times New Roman' => "'Times New Roman', Times, Baskerville, Georgia, serif",
+			'Trebuchet MS'    => "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif",
+			'Verdana'         => 'Verdana, Geneva, sans-serif',
+		);
+
+		$sut            = new WC_Settings_Emails();
+		$settings_by_id = $this->index_settings_by_id( $sut->get_settings_for_section( '' ) );
 		$setting        = $settings_by_id['woocommerce_email_font_family'];

 		$this->assertSame( 'Font family', $setting['title'] );
 		$this->assertSame( 'email_font_family', $setting['type'] );
 		$this->assertSame( 'Helvetica', $setting['default'] );

+		// The settings page renders a row by dispatching its type through this
+		// action, so the select below never reaches the page without the hook.
+		$this->assertSame(
+			10,
+			has_action( 'woocommerce_admin_field_email_font_family', array( $sut, 'email_font_family' ) ),
+			'The font family row is rendered from woocommerce_admin_field_email_font_family.'
+		);
+
 		$setting['field_name'] = $setting['id'];
 		$setting['value']      = 'Georgia';

 		ob_start();
 		try {
-			( new WC_Settings_Emails() )->email_font_family( $setting );
+			$sut->email_font_family( $setting );
 			$output = (string) ob_get_contents();
 		} finally {
 			ob_end_clean();
@@ -160,21 +183,69 @@ class WC_Settings_Emails_Test extends WC_Settings_Unit_Test_Case {
 		$select   = $this->get_element_by_id( $document, 'woocommerce_email_font_family' );

 		$options = $select->getElementsByTagName( 'option' );
-		$this->assertCount( count( EmailFont::$font ), $options );
+		$this->assertCount( count( $expected_fonts ), $options );

 		$rendered_fonts = array();
+		$rendered_names = array();
 		$selected       = array();
 		foreach ( $options as $option ) {
 			$rendered_fonts[ $option->getAttribute( 'value' ) ] = $option->getAttribute( 'data-font-family' );
+			$rendered_names[]                                   = $option->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode defines this public property name.
 			if ( $option->hasAttribute( 'selected' ) ) {
 				$selected[] = $option->getAttribute( 'value' );
 			}
 		}

-		$this->assertSame( EmailFont::$font, $rendered_fonts );
+		$this->assertSame( $expected_fonts, $rendered_fonts );
+		$this->assertSame( array_keys( $expected_fonts ), $rendered_names );
 		$this->assertSame( array( 'Georgia' ), $selected );
 	}

+	/**
+	 * @testdox The color palette settings row carries the title the palette section heading renders.
+	 */
+	public function test_email_color_palette_setting_row_contract(): void {
+		$sut      = new WC_Settings_Emails();
+		$settings = $sut->get_settings_for_section( '' );
+
+		// Indexing by ID is not enough here: the sectionend that closes the
+		// palette section reuses the same ID.
+		$rows = array_values(
+			array_filter(
+				$settings,
+				function ( $setting ) {
+					return 'email_color_palette' === ( $setting['id'] ?? '' )
+						&& 'email_color_palette' === ( $setting['type'] ?? '' );
+				}
+			)
+		);
+
+		$this->assertCount( 1, $rows, 'The default section defines exactly one color palette row.' );
+		$this->assertSame( 'Color palette', $rows[0]['title'] );
+
+		// The settings page renders a row by dispatching its type through this
+		// action, so the heading below never reaches the page without the hook.
+		$this->assertSame(
+			10,
+			has_action( 'woocommerce_admin_field_email_color_palette', array( $sut, 'email_color_palette' ) ),
+			'The color palette row is rendered from woocommerce_admin_field_email_color_palette.'
+		);
+
+		ob_start();
+		try {
+			$sut->email_color_palette( $rows[0] );
+			$output = (string) ob_get_contents();
+		} finally {
+			ob_end_clean();
+		}
+
+		$document = $this->load_html_document( $output . '</table>' );
+		$headings = $document->getElementsByTagName( 'h2' );
+
+		$this->assertGreaterThan( 0, $headings->length, 'The palette section renders its heading.' );
+		$this->assertSame( 'Color palette', $headings->item( 0 )->textContent ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode defines this public property name.
+	}
+
 	/**
 	 * @testdox The email footer setting exposes the current placeholder contract.
 	 */
@@ -196,17 +267,26 @@ class WC_Settings_Emails_Test extends WC_Settings_Unit_Test_Case {
 	}

 	/**
-	 * @testdox A single email preview renders its exact type, content settings, URL, and sender values.
+	 * @testdox A single email preview hangs off the email settings screen and renders its exact type, content settings, URL, and sender values.
 	 */
 	public function test_email_preview_single_contract(): void {
 		update_option( 'woocommerce_email_from_name', 'Woo Test Store' );
 		update_option( 'woocommerce_email_from_address', 'orders@example.com' );

 		$email = WC_Emails::instance()->get_emails()[ WC_Email_Customer_Processing_Order::class ];
+		$sut   = new WC_Settings_Emails();
+
+		// The per-email settings screen renders the preview through this action,
+		// so the mount below never reaches the page without the hook.
+		$this->assertSame(
+			10,
+			has_action( 'woocommerce_email_settings_after', array( $sut, 'email_preview_single' ) ),
+			'The single email preview is hooked onto woocommerce_email_settings_after.'
+		);

 		ob_start();
 		try {
-			( new WC_Settings_Emails() )->email_preview_single( $email );
+			$sut->email_preview_single( $email );
 			$output = (string) ob_get_contents();
 		} finally {
 			ob_end_clean();
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplierTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplierTest.php
index 6caec15c947..5c34f7eb84f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplierTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateAutoApplierTest.php
@@ -46,6 +46,10 @@ class WCEmailTemplateAutoApplierTest extends \WC_Unit_Test_Case {
 		update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
 		update_option( WCEmailTemplateDivergenceDetector::BACKFILL_COMPLETE_OPTION, 'yes' );

+		// Eagerly boot \WC_Emails so the \WC_Email class is autoloaded before any
+		// test reflects on it via getMockBuilder() / onlyMethods().
+		\WC_Emails::instance();
+
 		// Reuse the divergence-detector fixture file — same shape, same @version header.
 		$this->fixtures_base = dirname( __DIR__ ) . '/WCTransactionalEmails/fixtures/';
 		$this->posts_manager = WCTransactionalEmailPostsManager::get_instance();
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSelectiveApplierTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSelectiveApplierTest.php
index 8edb3ab7990..dbf7cb3b72f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSelectiveApplierTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateSelectiveApplierTest.php
@@ -44,6 +44,10 @@ class WCEmailTemplateSelectiveApplierTest extends \WC_Unit_Test_Case {

 		update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );

+		// Eagerly boot \WC_Emails so the \WC_Email class is autoloaded before any
+		// test reflects on it via getMockBuilder() / onlyMethods().
+		\WC_Emails::instance();
+
 		$this->fixtures_base = __DIR__ . '/fixtures/';
 		$this->posts_manager = WCTransactionalEmailPostsManager::get_instance();