Commit 9cd4e981242 for woocommerce

commit 9cd4e9812429b1aa067baa037356520047648b32
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Tue Sep 15 16:43:58 2026 +0300

    [tests] Move 5 Checkout block editor control E2E tests to Jest (#68586)

    * test(blocks): Move 5 Checkout block editor control E2E tests to Jest

    The Checkout block merchant spec ran eight browser titles. Five of
    them toggled one inspector control each and read the result back in
    the Site Editor canvas: the dark mode inputs toggle, the Return to
    Cart link toggle, and the visibility and requirement controls for
    Company, Address line 2, and Phone.

    Each of those controls maps a click to a block attribute or a site
    setting in a small component, and Jest reaches that mapping directly.
    Add Jest suites for BlockSettings, the Checkout Actions editor,
    AddressFieldControls, and the Checkout editor's attribute and field
    default mapping, and strengthen the Terms frontend suite. Keep three
    browser titles: insertion once, accepting the terms before checkout,
    and persisting a required Company field from the editor to the
    storefront.

    Consolidates the mega-branch slices:
    - Slice 024: test(blocks): Move Checkout controls below E2E
    - test(blocks): strengthen checkout terms observers (Terms frontend
      suite only)
    - refactor(e2e): simplify migrated Blocks test contracts (this spec
      only)

    Refs TESTOPS-234
    Refs #68046

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

    * test(blocks): Assert both terms calls clear the same validation error id

    Two independent pattern matches pass even when the ids disagree, which
    is the thing worth asserting: the component derives one
    `validationErrorId` and hands it to both the effect cleanup and the
    checked branch.

    Read the ids out of the mock calls instead, anchor the shape once, then
    assert the second call got the first call's id. Both halves earn their
    place. Appending a suffix to the cleanup call fails the anchored pattern,
    and handing it a well-formed id with a different instance number fails
    the equality.

    Refs #68586

    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-234-checkout-block-editor-controls b/plugins/woocommerce/changelog/testops-234-checkout-block-editor-controls
new file mode 100644
index 00000000000..58c19024dd3
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-checkout-block-editor-controls
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move 5 Checkout block editor control E2E tests to Jest; the Checkout block merchant spec keeps 3 browser titles.
+
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/block-settings/test/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/block-settings/test/index.tsx
new file mode 100644
index 00000000000..b3cb3e75c49
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/block-settings/test/index.tsx
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import { BlockSettings } from '../index';
+
+jest.mock( '@wordpress/block-editor', () => ( {
+	...jest.requireActual( '@wordpress/block-editor' ),
+	InspectorControls: jest.fn( ( { children } ) => <div>{ children }</div> ),
+} ) );
+
+describe( 'Cart and Checkout block settings', () => {
+	it.each( [
+		{ initialValue: false, expectedValue: true },
+		{ initialValue: true, expectedValue: false },
+	] )(
+		'maps hasDarkControls=$initialValue to $expectedValue',
+		async ( { initialValue, expectedValue } ) => {
+			const user = userEvent.setup();
+			const setAttributes = jest.fn();
+
+			render(
+				<BlockSettings
+					attributes={ {
+						hasDarkControls: initialValue,
+						showFormStepNumbers: false,
+					} }
+					setAttributes={ setAttributes }
+				/>
+			);
+
+			const darkModeToggle = screen.getByRole( 'checkbox', {
+				name: 'Dark mode inputs',
+			} );
+			expect( darkModeToggle ).toHaveProperty( 'checked', initialValue );
+
+			await user.click( darkModeToggle );
+
+			expect( setAttributes ).toHaveBeenCalledTimes( 1 );
+			expect( setAttributes ).toHaveBeenCalledWith( {
+				hasDarkControls: expectedValue,
+			} );
+		}
+	);
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/edit.test.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/edit.test.tsx
new file mode 100644
index 00000000000..7b15a51b509
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/edit.test.tsx
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import { Edit } from '../edit';
+
+jest.mock( '@wordpress/block-editor', () => ( {
+	InspectorControls: jest.fn( ( { children } ) => <div>{ children }</div> ),
+	RichText: jest.fn( ( { value, placeholder } ) => (
+		<span>{ value || placeholder }</span>
+	) ),
+	useBlockProps: Object.assign(
+		jest.fn( () => ( { className: '' } ) ),
+		{
+			save: jest.fn( () => ( {} ) ),
+		}
+	),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	useSelect: jest.fn( () => 2 ),
+} ) );
+
+jest.mock( '@woocommerce/editor-components/page-selector', () => () => null );
+
+jest.mock( '@woocommerce/base-components/cart-checkout', () => ( {
+	PlaceOrderButton: jest.fn( ( { label } ) => <button>{ label }</button> ),
+	ReturnToCartButton: jest.fn( ( { children } ) => (
+		<span>{ children }</span>
+	) ),
+} ) );
+
+jest.mock( '@woocommerce/block-settings', () => ( {
+	CHECKOUT_PAGE_ID: 1,
+} ) );
+
+const expectReturnToCartVisible = () => {
+	expect( screen.getByText( 'Return to Cart' ) ).toBeVisible();
+};
+
+const expectReturnToCartHidden = () => {
+	expect( screen.queryByText( 'Return to Cart' ) ).not.toBeInTheDocument();
+};
+
+describe( 'Checkout Actions editor', () => {
+	it.each( [
+		{ initialValue: false, expectedValue: true },
+		{ initialValue: true, expectedValue: false },
+	] )(
+		'maps showReturnToCart=$initialValue to $expectedValue and previews the current state',
+		async ( { initialValue, expectedValue } ) => {
+			const user = userEvent.setup();
+			const setAttributes = jest.fn();
+
+			render(
+				<Edit
+					attributes={ {
+						cartPageId: 1,
+						showReturnToCart: initialValue,
+						placeOrderButtonLabel: 'Place Order',
+						priceSeparator: '·',
+						returnToCartButtonLabel: 'Return to Cart',
+					} }
+					setAttributes={ setAttributes }
+				/>
+			);
+
+			const returnToCartToggle = screen.getByRole( 'checkbox', {
+				name: 'Show a "Return to Cart" link',
+			} );
+			expect( returnToCartToggle ).toHaveProperty(
+				'checked',
+				initialValue
+			);
+			if ( initialValue ) {
+				expectReturnToCartVisible();
+			} else {
+				expectReturnToCartHidden();
+			}
+
+			await user.click( returnToCartToggle );
+
+			expect( setAttributes ).toHaveBeenCalledTimes( 1 );
+			expect( setAttributes ).toHaveBeenCalledWith( {
+				showReturnToCart: expectedValue,
+			} );
+		}
+	);
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/frontend.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/frontend.js
index 4d75e7f082d..23c0b676035 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/frontend.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/frontend.js
@@ -20,6 +20,12 @@ import { validationStore } from '@woocommerce/block-data';
 import * as actionCreators from '@woocommerce/block-data/validation/actions';
 import FrontendBlock from '../frontend';

+jest.mock( '@woocommerce/block-settings', () => ( {
+	...jest.requireActual( '@woocommerce/block-settings' ),
+	TERMS_URL: 'https://example.com/terms/',
+	PRIVACY_URL: 'https://example.com/privacy/',
+} ) );
+
 jest.mock( '@woocommerce/block-data/validation/actions', () => {
 	const actions = jest.requireActual(
 		'@woocommerce/block-data/validation/actions'
@@ -33,7 +39,35 @@ jest.mock( '@woocommerce/block-data/validation/actions', () => {
 } );

 describe( 'FrontendBlock', () => {
-	it( 'Renders a checkbox if the checkbox prop is true', async () => {
+	it( 'Renders the default Terms and Privacy links without a checkbox', () => {
+		render(
+			<SlotFillProvider>
+				<FrontendBlock
+					checkbox={ false }
+					text=""
+					showSeparator={ false }
+				/>
+			</SlotFillProvider>
+		);
+
+		expect(
+			screen.getByText(
+				( _, element ) =>
+					element?.tagName === 'SPAN' &&
+					element.textContent ===
+						'By proceeding with your purchase you agree to our Terms and Conditions and Privacy Policy'
+			)
+		).toBeVisible();
+		expect(
+			screen.getByRole( 'link', { name: 'Terms and Conditions' } )
+		).toHaveAttribute( 'href', 'https://example.com/terms/' );
+		expect(
+			screen.getByRole( 'link', { name: 'Privacy Policy' } )
+		).toHaveAttribute( 'href', 'https://example.com/privacy/' );
+		expect( screen.queryByRole( 'checkbox' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'Renders a checkbox if the checkbox prop is true', () => {
 		const { container } = render(
 			<SlotFillProvider>
 				<FrontendBlock
@@ -44,7 +78,7 @@ describe( 'FrontendBlock', () => {
 			</SlotFillProvider>
 		);

-		const checkbox = await findByLabelText(
+		const checkbox = queryByLabelText(
 			container,
 			'I agree to the terms and conditions'
 		);
@@ -73,6 +107,7 @@ describe( 'FrontendBlock', () => {

 	it( 'Clears any validation errors when the checkbox is checked', async () => {
 		const user = userEvent.setup();
+		actionCreators.clearValidationError.mockClear();
 		const { container } = render(
 			<SlotFillProvider>
 				<FrontendBlock
@@ -89,9 +124,19 @@ describe( 'FrontendBlock', () => {
 		await act( async () => {
 			await user.click( checkbox );
 		} );
-		expect( actionCreators.clearValidationError ).toHaveBeenLastCalledWith(
-			expect.stringMatching( /terms-and-conditions-\d/ )
+		expect( actionCreators.clearValidationError ).toHaveBeenCalledTimes(
+			2
 		);
+
+		const [ [ cleanupId ], [ checkedId ] ] =
+			actionCreators.clearValidationError.mock.calls;
+
+		// The component derives one `validationErrorId` and hands it to both
+		// the effect cleanup and the checked branch. Matching each call
+		// against the pattern separately would pass even if the two ids
+		// diverged, so assert the shape once and then that both calls agree.
+		expect( cleanupId ).toMatch( /^terms-and-conditions-\d+$/ );
+		expect( checkedId ).toBe( cleanupId );
 	} );

 	it( 'Renders and describes the validation error when the checkbox is required and unchecked', async () => {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/address-field-controls.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/address-field-controls.tsx
new file mode 100644
index 00000000000..78df232c75a
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/address-field-controls.tsx
@@ -0,0 +1,147 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import { AddressFieldControls } from '../address-field-controls';
+
+const mockEditEntityRecord = jest.fn();
+const mockDispatch = jest.fn( () => ( {
+	editEntityRecord: mockEditEntityRecord,
+} ) );
+const mockUseCheckoutBlockContext = jest.fn();
+
+jest.mock( '@wordpress/block-editor', () => ( {
+	InspectorControls: jest.fn( ( { children } ) => <div>{ children }</div> ),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	dispatch: ( store: string ) => mockDispatch( store ),
+} ) );
+
+jest.mock( '@wordpress/core-data', () => ( {
+	store: 'core',
+} ) );
+
+jest.mock( '../context', () => ( {
+	useCheckoutBlockContext: () => mockUseCheckoutBlockContext(),
+} ) );
+
+type FieldName = 'company' | 'address_2' | 'phone';
+type FieldState = 'hidden' | 'optional' | 'required';
+
+const fields: Array< {
+	label: string;
+	name: FieldName;
+	option: string;
+} > = [
+	{
+		label: 'Company',
+		name: 'company',
+		option: 'woocommerce_checkout_company_field',
+	},
+	{
+		label: 'Address line 2',
+		name: 'address_2',
+		option: 'woocommerce_checkout_address_2_field',
+	},
+	{
+		label: 'Phone',
+		name: 'phone',
+		option: 'woocommerce_checkout_phone_field',
+	},
+];
+
+const getField = ( state: FieldState ) => ( {
+	hidden: state === 'hidden',
+	required: state === 'required',
+} );
+
+const renderControls = ( field: FieldName, state: FieldState ) => {
+	mockUseCheckoutBlockContext.mockReturnValue( {
+		defaultFields: {
+			company: getField( field === 'company' ? state : 'hidden' ),
+			address_2: getField( field === 'address_2' ? state : 'hidden' ),
+			phone: getField( field === 'phone' ? state : 'hidden' ),
+		},
+	} );
+
+	render( <AddressFieldControls /> );
+};
+
+const expectUpdate = ( option: string, value: FieldState ) => {
+	expect( mockDispatch ).toHaveBeenCalledWith( 'core' );
+	expect( mockEditEntityRecord ).toHaveBeenCalledTimes( 1 );
+	expect( mockEditEntityRecord ).toHaveBeenCalledWith(
+		'root',
+		'site',
+		undefined,
+		{ [ option ]: value }
+	);
+};
+
+describe.each( fields )( '$label address field control', ( field ) => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+	} );
+
+	it( 'shows a hidden field as optional', async () => {
+		const user = userEvent.setup();
+		renderControls( field.name, 'hidden' );
+
+		const visibilityToggle = screen.getByRole( 'checkbox', {
+			name: field.label,
+		} );
+		expect( visibilityToggle ).not.toBeChecked();
+
+		await user.click( visibilityToggle );
+
+		expectUpdate( field.option, 'optional' );
+	} );
+
+	it( 'hides an optional field', async () => {
+		const user = userEvent.setup();
+		renderControls( field.name, 'optional' );
+
+		const visibilityToggle = screen.getByRole( 'checkbox', {
+			name: field.label,
+		} );
+		expect( visibilityToggle ).toBeChecked();
+		expect(
+			screen.getByRole( 'radio', { name: 'Optional' } )
+		).toBeChecked();
+
+		await user.click( visibilityToggle );
+
+		expectUpdate( field.option, 'hidden' );
+	} );
+
+	it( 'makes an optional field required', async () => {
+		const user = userEvent.setup();
+		renderControls( field.name, 'optional' );
+
+		expect(
+			screen.getByRole( 'radio', { name: 'Optional' } )
+		).toBeChecked();
+		await user.click( screen.getByRole( 'radio', { name: 'Required' } ) );
+
+		expectUpdate( field.option, 'required' );
+	} );
+
+	it( 'makes a required field optional', async () => {
+		const user = userEvent.setup();
+		renderControls( field.name, 'required' );
+
+		expect(
+			screen.getByRole( 'radio', { name: 'Required' } )
+		).toBeChecked();
+		await user.click( screen.getByRole( 'radio', { name: 'Optional' } ) );
+
+		expectUpdate( field.option, 'optional' );
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/edit.tsx
new file mode 100644
index 00000000000..a67905b3428
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/edit.tsx
@@ -0,0 +1,276 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { Form } from '@woocommerce/base-components/cart-checkout';
+import type { FormFields, ShippingAddress } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit } from '../edit';
+import type { Attributes } from '../types';
+
+let mockFieldSettings: Record< string, string > = {};
+let mockCapturedDefaultFields: FormFields | undefined;
+let mockRenderingCheckoutEdit = false;
+
+jest.mock( '@wordpress/data', () => {
+	const data = jest.requireActual( '@wordpress/data' );
+	return {
+		...data,
+		useSelect: (
+			mapSelect: ( select: ( store: unknown ) => unknown ) => unknown,
+			dependencies?: unknown[]
+		) => {
+			if ( mockRenderingCheckoutEdit ) {
+				return mapSelect( () => ( {
+					getEditedEntityRecord: () => mockFieldSettings,
+				} ) );
+			}
+
+			return data.useSelect( mapSelect, dependencies );
+		},
+	};
+} );
+
+jest.mock( '@wordpress/block-editor', () => {
+	const InnerBlocks = Object.assign(
+		jest.fn( () => null ),
+		{
+			Content: jest.fn( () => null ),
+		}
+	);
+	const useBlockProps = Object.assign(
+		jest.fn( () => ( {} ) ),
+		{
+			save: jest.fn( () => ( {} ) ),
+		}
+	);
+
+	return {
+		InnerBlocks,
+		InspectorControls: jest.fn( ( { children } ) => <>{ children }</> ),
+		useBlockProps,
+	};
+} );
+
+jest.mock( '@woocommerce/base-context', () => ( {
+	...jest.requireActual( '@woocommerce/base-context' ),
+	CheckoutProvider: jest.fn( ( { children } ) => <>{ children }</> ),
+	EditorProvider: jest.fn( ( { children, previewData } ) => {
+		mockCapturedDefaultFields = previewData.defaultFields;
+		return <>{ children }</>;
+	} ),
+	useCheckoutAddress: jest.fn( () => ( {
+		defaultFields: mockCapturedDefaultFields,
+	} ) ),
+} ) );
+
+jest.mock( '@woocommerce/base-components/sidebar-layout', () => ( {
+	SidebarLayout: jest.fn( ( { children, className } ) => (
+		<div className={ className }>{ children }</div>
+	) ),
+} ) );
+
+jest.mock( '@woocommerce/blocks-checkout', () => ( {
+	...jest.requireActual( '@woocommerce/blocks-checkout' ),
+	SlotFillProvider: jest.fn( ( { children } ) => <>{ children }</> ),
+} ) );
+
+jest.mock( '../../cart-checkout-shared', () => ( {
+	addClassToBody: jest.fn(),
+	BlockSettings: jest.fn( () => null ),
+	useBlockPropsWithLocking: jest.fn( () => ( {} ) ),
+} ) );
+
+jest.mock( '../inner-blocks', () => ( {} ) );
+
+const checkoutAttributes: Attributes = {
+	hasDarkControls: false,
+	showFormStepNumbers: false,
+	showOrderNotes: true,
+	showPolicyLinks: true,
+	showReturnToCart: false,
+	showRateAfterTaxName: false,
+	cartPageId: 1,
+	showCompanyField: false,
+	requireCompanyField: false,
+	showApartmentField: false,
+	requireApartmentField: false,
+	showPhoneField: false,
+	requirePhoneField: false,
+};
+
+const emptyShippingAddress: ShippingAddress = {
+	first_name: '',
+	last_name: '',
+	company: '',
+	address_1: '',
+	address_2: '',
+	city: '',
+	state: '',
+	postcode: '',
+	country: '',
+	phone: '',
+};
+
+const renderCheckoutEdit = (
+	fieldSettings: Record< string, string > = {},
+	hasDarkControls = false
+) => {
+	mockFieldSettings = fieldSettings;
+	mockCapturedDefaultFields = undefined;
+	mockRenderingCheckoutEdit = true;
+	const result = render(
+		<Edit
+			clientId="checkout-client-id"
+			attributes={ { ...checkoutAttributes, hasDarkControls } }
+			setAttributes={ jest.fn() }
+		/>
+	);
+	mockRenderingCheckoutEdit = false;
+
+	if ( ! mockCapturedDefaultFields ) {
+		throw new Error( 'Checkout Edit did not provide default fields.' );
+	}
+
+	return result;
+};
+
+type FieldName = 'company' | 'address_2' | 'phone';
+type FieldState = 'hidden' | 'optional' | 'required';
+
+const fieldCases: Array< {
+	field: FieldName;
+	label: string;
+	addLabel?: string;
+} > = [
+	{ field: 'company', label: 'Company' },
+	{
+		field: 'address_2',
+		label: 'Apartment, suite, etc.',
+		addLabel: '+ Add apartment, suite, etc.',
+	},
+	{ field: 'phone', label: 'Phone' },
+];
+
+const stateCases: Array< {
+	state: FieldState;
+	hidden: boolean;
+	required: boolean;
+} > = [
+	{ state: 'hidden', hidden: true, required: false },
+	{ state: 'optional', hidden: false, required: false },
+	{ state: 'required', hidden: false, required: true },
+];
+
+const expectHiddenField = ( label: string ) => {
+	expect( screen.queryByLabelText( label ) ).not.toBeInTheDocument();
+	expect(
+		screen.queryByLabelText( `${ label } (optional)` )
+	).not.toBeInTheDocument();
+};
+
+const expectHiddenAddControl = ( addLabel: string ) => {
+	expect(
+		screen.queryByRole( 'button', { name: addLabel } )
+	).not.toBeInTheDocument();
+};
+
+const expectCollapsedOptionalAddressLine = (
+	label: string,
+	addLabel: string
+) => {
+	expect( screen.getByRole( 'button', { name: addLabel } ) ).toBeVisible();
+	expect( screen.getByLabelText( label ) ).toHaveAttribute(
+		'aria-hidden',
+		'true'
+	);
+};
+
+const expectVisibleField = ( label: string, required: boolean ) => {
+	const input = screen.getByLabelText(
+		required ? label : `${ label } (optional)`
+	);
+	expect( input ).toBeVisible();
+	expect( ( input as HTMLInputElement ).required ).toBe( required );
+};
+
+describe( 'Checkout editor consumers', () => {
+	beforeEach( () => {
+		mockFieldSettings = {};
+		mockCapturedDefaultFields = undefined;
+		mockRenderingCheckoutEdit = false;
+	} );
+
+	it( 'omits the dark controls class when hasDarkControls is false', () => {
+		const { container } = renderCheckoutEdit();
+		const checkoutLayout = container.querySelector( '.wc-block-checkout' );
+
+		expect( checkoutLayout ).toBeInTheDocument();
+		expect( checkoutLayout ).not.toHaveClass( 'has-dark-controls' );
+	} );
+
+	it( 'adds the dark controls class when hasDarkControls is true', () => {
+		const { container } = renderCheckoutEdit( {}, true );
+		const checkoutLayout = container.querySelector( '.wc-block-checkout' );
+
+		expect( checkoutLayout ).toBeInTheDocument();
+		expect( checkoutLayout ).toHaveClass( 'has-dark-controls' );
+	} );
+
+	it.each(
+		fieldCases.flatMap( ( fieldCase ) =>
+			stateCases.map( ( stateCase ) => ( {
+				...fieldCase,
+				...stateCase,
+			} ) )
+		)
+	)(
+		'maps $field=$state from the root-site record into the real form',
+		( { field, label, addLabel, state, hidden, required } ) => {
+			const optionName = `woocommerce_checkout_${ field }_field`;
+			const { unmount } = renderCheckoutEdit( {
+				[ optionName ]: state,
+			} );
+			const defaultFields = mockCapturedDefaultFields as FormFields;
+
+			expect( defaultFields[ field ] ).toMatchObject( {
+				hidden,
+				required,
+			} );
+			unmount();
+
+			const fields: ( keyof FormFields )[] =
+				field === 'address_2'
+					? [ 'address_1', 'address_2' ]
+					: [ field ];
+			render(
+				<Form< ShippingAddress >
+					id="shipping"
+					addressType="shipping"
+					fields={ fields }
+					isEditing
+					onChange={ jest.fn() }
+					values={ emptyShippingAddress }
+				/>
+			);
+
+			if ( hidden ) {
+				expectHiddenField( label );
+				if ( addLabel ) {
+					expectHiddenAddControl( addLabel );
+				}
+				return;
+			}
+
+			if ( field === 'address_2' && ! required && addLabel ) {
+				expectCollapsedOptionalAddressLine( label, addLabel );
+				return;
+			}
+
+			expectVisibleField( label, required );
+		}
+	);
+} );
diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-checkout-block-editor-controls b/plugins/woocommerce/client/blocks/changelog/testops-234-checkout-block-editor-controls
new file mode 100644
index 00000000000..58c19024dd3
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/testops-234-checkout-block-editor-controls
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move 5 Checkout block editor control E2E tests to Jest; the Checkout block merchant spec keeps 3 browser titles.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
index dcb7aaee331..71a987c4be6 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
@@ -14,16 +14,6 @@ import {
 import { CheckoutPage } from './checkout.page';
 import { REGULAR_PRICED_PRODUCT_NAME } from './constants';

-declare global {
-	interface Window {
-		wcSettings: {
-			storePages: {
-				terms: { permalink: string };
-				privacy: { permalink: string };
-			};
-		};
-	}
-}
 const blockData: BlockData = {
 	name: 'Checkout',
 	slug: 'woocommerce/checkout',
@@ -104,80 +94,22 @@ test.describe( 'Merchant → Checkout', () => {
 		);
 	} );

-	test.describe( 'Can adjust T&S and Privacy Policy options', () => {
-		test( 'Merchant can see T&S and Privacy Policy links without checkbox', async ( {
-			page,
-			frontendUtils,
-			checkoutPageObject,
-		} ) => {
-			await frontendUtils.goToShop();
-			await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
-			await frontendUtils.goToCheckout();
-			await expect(
-				frontendUtils.page.getByText(
-					'By proceeding with your purchase you agree to our Terms and Conditions and Privacy Policy'
-				)
-			).toBeVisible();
-
-			const termsAndConditions = frontendUtils.page
-				.getByRole( 'link' )
-				.getByText( 'Terms and Conditions' )
-				.first();
-			const privacyPolicy = frontendUtils.page
-				.getByRole( 'link' )
-				.getByText( 'Privacy Policy' )
-				.first();
-
-			const { termsPageUrl, privacyPageUrl } = await page.evaluate(
-				() => {
-					const { terms, privacy } = window.wcSettings.storePages;
-
-					return {
-						termsPageUrl: terms.permalink,
-						privacyPageUrl: privacy.permalink,
-					};
-				}
-			);
-			await expect( termsAndConditions ).toHaveAttribute(
-				'href',
-				termsPageUrl
-			);
-			await expect( privacyPolicy ).toHaveAttribute(
-				'href',
-				privacyPageUrl
-			);
-			await checkoutPageObject.fillInCheckoutWithTestData();
-			await checkoutPageObject.placeOrder();
-			await expect(
-				frontendUtils.page.getByText(
-					'Thank you. Your order has been received.'
-				)
-			).toBeVisible();
-		} );
-	} );
-
-	test( 'Merchant can see T&S and Privacy Policy links with checkbox', async ( {
+	test( 'Merchant must accept T&S before checkout', async ( {
 		frontendUtils,
 		checkoutPageObject,
-		admin,
 		editor,
 	} ) => {
-		await admin.visitSiteEditor( {
-			postId: `${ BLOCK_THEME_SLUG }//page-checkout`,
-			postType: 'wp_template',
-			canvas: 'edit',
-		} );
-		await editor.openDocumentSettingsSidebar();
 		await editor.selectBlocks(
 			blockSelectorInEditor +
 				'  [data-type="woocommerce/checkout-terms-block"]'
 		);
-		let requireTermsCheckbox = editor.page.getByRole( 'checkbox', {
+		const requireTermsCheckbox = editor.page.getByRole( 'checkbox', {
 			name: 'Require checkbox',
 			exact: true,
 		} );
 		await requireTermsCheckbox.check();
 		await editor.saveSiteEditorEntities();
+
 		await frontendUtils.goToShop();
 		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
 		await frontendUtils.goToCheckout();
@@ -191,12 +123,7 @@ test.describe( 'Merchant → Checkout', () => {
 			'aria-invalid',
 			'true'
 		);
-
-		await frontendUtils.page
-			.getByLabel(
-				'You must accept our Terms and Conditions and Privacy Policy to continue with your purchase.'
-			)
-			.check();
+		await checkboxWithError.check();

 		await checkoutPageObject.placeOrder();
 		await expect(
@@ -204,6 +131,22 @@ test.describe( 'Merchant → Checkout', () => {
 				'Thank you. Your order has been received'
 			)
 		).toBeVisible();
+	} );
+
+	test( 'Merchant can persist a required Company field', async ( {
+		frontendUtils,
+		admin,
+		editor,
+		requestUtils,
+	} ) => {
+		await requestUtils.rest( {
+			method: 'POST',
+			path: 'e2e-options/update',
+			data: {
+				option_name: 'woocommerce_checkout_company_field',
+				option_value: 'hidden',
+			},
+		} );

 		await admin.visitSiteEditor( {
 			postId: `${ BLOCK_THEME_SLUG }//page-checkout`,
@@ -213,359 +156,76 @@ test.describe( 'Merchant → Checkout', () => {
 		await editor.openDocumentSettingsSidebar();
 		await editor.selectBlocks(
 			blockSelectorInEditor +
-				'  [data-type="woocommerce/checkout-terms-block"]'
+				'  [data-type="woocommerce/checkout-shipping-address-block"]'
 		);
-		requireTermsCheckbox = editor.page.getByRole( 'checkbox', {
-			name: 'Require checkbox',
-			exact: true,
-		} );
-		await requireTermsCheckbox.uncheck();
-		await editor.saveSiteEditorEntities();
-	} );
-
-	test.describe( 'Attributes', () => {
-		test.beforeEach( async ( { editor } ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.selectBlocks( blockSelectorInEditor );
-		} );
-
-		test( 'can enable dark mode inputs', async ( { editor, page } ) => {
-			const toggleLabel = page.getByLabel( 'Dark mode inputs' );
-			await toggleLabel.check();
-
-			const shippingAddressBlock = await editor.getBlockByName(
-				'woocommerce/checkout'
-			);

-			const darkControls = shippingAddressBlock.locator(
-				'.wc-block-checkout.has-dark-controls'
-			);
-			await expect( darkControls ).toBeVisible();
-			await toggleLabel.uncheck();
-			await expect( darkControls ).toBeHidden();
+		const shippingAddressBlock = await editor.getBlockByName(
+			'woocommerce/checkout-shipping-address-block'
+		);
+		const shippingCompanyInput =
+			shippingAddressBlock.getByLabel( 'Company' );
+		const shippingCompanyToggle = editor.page.getByRole( 'checkbox', {
+			name: 'Company',
+			exact: true,
 		} );
+		const companyRequirement = editor.page.locator(
+			'.wc-block-components-require-company-field'
+		);

-		test.describe( 'Shipping and billing addresses', () => {
-			test.beforeEach( async ( { editor } ) => {
-				await editor.openDocumentSettingsSidebar();
-				await editor.selectBlocks( blockSelectorInEditor );
-			} );
-
-			test( 'Company input visibility and optional and required can be toggled', async ( {
-				editor,
-			} ) => {
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-shipping-address-block"]'
-				);
-
-				const shippingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-shipping-address-block'
-				);
-
-				const shippingCompanyInput =
-					shippingAddressBlock.getByLabel( 'Company' );
-
-				const shippingCompanyToggle = editor.page.getByRole(
-					'checkbox',
-					{
-						name: 'Company',
-						exact: true,
-					}
-				);
-
-				const shippingCompanyOptionalToggle = editor.page.locator(
-					'.wc-block-components-require-company-field >> text="Optional"'
-				);
-
-				const shippingCompanyRequiredToggle = editor.page.locator(
-					'.wc-block-components-require-company-field >> text="Required"'
-				);
-
-				// Verify that the company field is hidden by default.
-				await expect( shippingCompanyInput ).toBeHidden();
-
-				// Enable the company field.
-				await expect( async () => {
-					await shippingCompanyToggle.check();
-				} ).toPass();
-
-				// Verify that the company field is visible and the field is optional.
-				await expect( shippingCompanyInput ).toBeVisible();
-				await expect( shippingCompanyOptionalToggle ).toBeChecked();
-				await expect( shippingCompanyInput ).not.toHaveAttribute(
-					'required'
-				);
-
-				// Make the company field required.
-				await expect( async () => {
-					await shippingCompanyRequiredToggle.check();
-				} ).toPass();
-
-				// Verify that the company field is required.
-				await expect( shippingCompanyRequiredToggle ).toBeChecked();
-
-				// Disable the company field.
-				await expect( async () => {
-					await shippingCompanyToggle.uncheck();
-				} ).toPass();
-
-				// Verify that the company field is hidden.
-				await expect( shippingCompanyInput ).toBeHidden();
-
-				// Display the billing address form.
-				await editor.canvas
-					.getByLabel( 'Use same address for billing' )
-					.uncheck();
-
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-billing-address-block"]'
-				);
-
-				const billingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-billing-address-block'
-				);
-
-				const billingCompanyInput =
-					billingAddressBlock.getByLabel( 'Company' );
-
-				const billingCompanyToggle = editor.page.getByRole(
-					'checkbox',
-					{
-						name: 'Company',
-						exact: true,
-					}
-				);
-
-				// Verify the company field on the billing address has the correct state from the shipping address.
-				await expect( billingCompanyToggle ).not.toBeChecked();
-				await expect( billingCompanyInput ).toBeHidden();
-			} );
-
-			test( 'Apartment input visibility and optional and required can be toggled', async ( {
-				editor,
-			} ) => {
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-shipping-address-block"]'
-				);
-
-				const shippingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-shipping-address-block'
-				);
-
-				const shippingApartmentInput = shippingAddressBlock.getByLabel(
-					'Apartment, suite, etc.'
-				);
-
-				const shippingApartmentLink = shippingAddressBlock.getByRole(
-					'button',
-					{
-						name: '+ Add apartment, suite, etc.',
-					}
-				);
-
-				const shippingApartmentToggle = editor.page.getByRole(
-					'checkbox',
-					{
-						name: 'Address line 2',
-						exact: true,
-					}
-				);
-
-				const shippingApartmentOptionalToggle = editor.page.locator(
-					'.wc-block-components-require-address_2-field >> text="Optional"'
-				);
-
-				const shippingApartmentRequiredToggle = editor.page.locator(
-					'.wc-block-components-require-address_2-field >> text="Required"'
-				);
-
-				// Verify that the apartment link is visible by default.
-				await expect( shippingApartmentLink ).toBeVisible();
-
-				// Verify that the apartment field is hidden by default and the field is optional.
-				await expect( shippingApartmentInput ).not.toBeInViewport();
-				await expect( shippingApartmentOptionalToggle ).toBeChecked();
-
-				// Make the apartment number required.
-				await expect( async () => {
-					await shippingApartmentRequiredToggle.check();
-				} ).toPass();
-
-				// Verify that the apartment field is required.
-				await expect( shippingApartmentRequiredToggle ).toBeChecked();
-				await expect( shippingApartmentInput ).toHaveAttribute(
-					'required'
-				);
-
-				// Disable the apartment field.
-				await expect( async () => {
-					await shippingApartmentToggle.uncheck();
-				} ).toPass();
-
-				// Verify that the apartment link and the apartment field are hidden.
-				await expect( shippingApartmentLink ).toBeHidden();
-				await expect( shippingApartmentInput ).not.toBeInViewport();
-
-				// Display the billing address form.
-				await editor.canvas
-					.getByLabel( 'Use same address for billing' )
-					.uncheck();
-
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-billing-address-block"]'
-				);
-
-				const billingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-billing-address-block'
-				);
-
-				const billingApartmentInput = billingAddressBlock.getByLabel(
-					'Apartment, suite, etc.'
-				);
-
-				const billingApartmentLink = billingAddressBlock.getByRole(
-					'button',
-					{
-						name: '+ Add apartment, suite, etc.',
-					}
-				);
-
-				const billingApartmentToggle = editor.page.getByRole(
-					'checkbox',
-					{
-						name: 'Address line 2',
-						exact: true,
-					}
-				);
-
-				// Verify the apartment field on the billing address has the correct state from the shipping address.
-				await expect( billingApartmentToggle ).not.toBeChecked();
-				await expect( billingApartmentLink ).toBeHidden();
-				await expect( billingApartmentInput ).not.toBeInViewport();
-			} );
-
-			test( 'Phone input visibility and optional and required can be toggled', async ( {
-				editor,
-			} ) => {
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-shipping-address-block"]'
-				);
-
-				const shippingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-shipping-address-block'
-				);
-
-				const shippingPhoneInput =
-					shippingAddressBlock.getByLabel( 'Phone' );
-
-				const shippingPhoneToggle = editor.page.getByRole( 'checkbox', {
-					name: 'Phone',
-					exact: true,
-				} );
-
-				const shippingPhoneOptionalToggle = editor.page.locator(
-					'.wc-block-components-require-phone-field >> text="Optional"'
-				);
-
-				const shippingPhoneRequiredToggle = editor.page.locator(
-					'.wc-block-components-require-phone-field >> text="Required"'
-				);
-
-				// Verify that the phone field is visible by default and the field is optional.
-				await expect( shippingPhoneInput ).toBeVisible();
-				await expect( shippingPhoneOptionalToggle ).toBeChecked();
-				await expect( shippingPhoneInput ).not.toHaveAttribute(
-					'required'
-				);
-
-				// Make the phone number required.
-				await expect( async () => {
-					await shippingPhoneRequiredToggle.check();
-				} ).toPass();
-
-				// Verify that the phone field is required.
-				await expect( shippingPhoneRequiredToggle ).toBeChecked();
-				await expect( shippingPhoneInput ).toHaveAttribute(
-					'required'
-				);
-
-				// Disable the phone field.
-				await expect( async () => {
-					await shippingPhoneToggle.uncheck();
-				} ).toPass();
-
-				// Verify that the phone field is hidden.
-				await expect( shippingPhoneInput ).toBeHidden();
-
-				// Display the billing address form.
-				await editor.canvas
-					.getByLabel( 'Use same address for billing' )
-					.uncheck();
-
-				await editor.selectBlocks(
-					blockSelectorInEditor +
-						'  [data-type="woocommerce/checkout-billing-address-block"]'
-				);
-
-				const billingAddressBlock = await editor.getBlockByName(
-					'woocommerce/checkout-billing-address-block'
-				);
-
-				const billingPhoneInput =
-					billingAddressBlock.getByLabel( 'Phone' );
-
-				const billingPhoneToggle = editor.page.getByRole( 'checkbox', {
-					name: 'Phone',
-					exact: true,
-				} );
+		await expect( shippingCompanyToggle ).not.toBeChecked();
+		await expect( shippingCompanyInput ).toBeHidden();
+		await expect( async () => {
+			await shippingCompanyToggle.check();
+		} ).toPass();
+		await expect( shippingCompanyInput ).toBeVisible();
+		await expect(
+			companyRequirement.getByRole( 'radio', { name: 'Optional' } )
+		).toBeChecked();
+		await expect( shippingCompanyInput ).not.toHaveAttribute( 'required' );

-				// Verify the phone field on the billing address has the correct state from the shipping address.
-				await expect( billingPhoneToggle ).not.toBeChecked();
-				await expect( billingPhoneInput ).toBeHidden();
-			} );
+		const requiredCompany = companyRequirement.getByRole( 'radio', {
+			name: 'Required',
 		} );
-	} );
+		await expect( async () => {
+			await requiredCompany.check();
+		} ).toPass();
+		await expect( requiredCompany ).toBeChecked();
+		await expect( shippingCompanyInput ).toHaveAttribute( 'required' );
+		await editor.saveSiteEditorEntities();

-	test.describe( 'Checkout actions', () => {
-		test.beforeEach( async ( { editor } ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.selectBlocks( blockSelectorInEditor );
+		await admin.visitSiteEditor( {
+			postId: `${ BLOCK_THEME_SLUG }//page-checkout`,
+			postType: 'wp_template',
+			canvas: 'edit',
 		} );
+		await editor.openDocumentSettingsSidebar();
+		await editor.selectBlocks(
+			blockSelectorInEditor +
+				'  [data-type="woocommerce/checkout-shipping-address-block"]'
+		);
+		await expect(
+			editor.page
+				.locator( '.wc-block-components-require-company-field' )
+				.getByRole( 'radio', { name: 'Required' } )
+		).toBeChecked();

-		test( 'Return to cart link is visible and can be toggled', async ( {
-			editor,
-		} ) => {
-			await editor.selectBlocks(
-				`${ blockSelectorInEditor } .wp-block-woocommerce-checkout-actions-block`
-			);
-
-			// Turn on return to cart link and check it's visible in the block.
-			const returnToCartLinkToggle = editor.page.getByLabel(
-				'Show a "Return to Cart" link',
-				{ exact: true }
-			);
-			await returnToCartLinkToggle.check();
-			const shippingAddressBlock = await editor.getBlockByName(
-				'woocommerce/checkout-actions-block'
-			);
-
-			// Turn on return to cart link and check it shows in the block.
-			const returnToCartLink = shippingAddressBlock.getByText(
-				'Return to Cart',
-				{ exact: true }
-			);
-
-			// Turn off return to cart link and check it's not visible in the block.
-			await expect( returnToCartLink ).toBeVisible();
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.goToCheckout();

-			await returnToCartLinkToggle.uncheck();
+		const shippingCompany = frontendUtils.page
+			.getByRole( 'group', { name: 'Shipping address' } )
+			.getByLabel( 'Company' );
+		await expect( shippingCompany ).toBeVisible();
+		await expect( shippingCompany ).toHaveAttribute( 'required' );

-			await expect( returnToCartLink ).toBeHidden();
-		} );
+		await frontendUtils.page
+			.getByLabel( 'Use same address for billing' )
+			.uncheck();
+		const billingCompany = frontendUtils.page
+			.getByRole( 'group', { name: 'Billing address' } )
+			.getByLabel( 'Company' );
+		await expect( billingCompany ).toBeVisible();
+		await expect( billingCompany ).toHaveAttribute( 'required' );
 	} );
 } );