Commit 447fd1b706d for woocommerce

commit 447fd1b706dc9d02cbe6e77f098b604c7782f985
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Thu Sep 24 18:18:18 2026 +0300

    Revert "[tests] Move 4 extension cart update E2E tests to Jest and PHPUnit" (#69047)

    * revert: Revert "[tests] Move 4 extension cart update E2E tests to Jest and PHPUnit" (#68585)

    This reverts commit dc6c80ab111bf87f22c6e630f159538bffdb242a.

    Rubik asked for the E2E migration's test changes in its areas to be
    reverted until the team can review them. #68585 is one of the 11 PRs on
    the revert list Rubik agreed on 2026-09-24.

    The revert brings back the browser tests the PR removed or cut down and
    removes the lower-layer tests it added in their place. Only test code
    and changelog entries change; nothing ships.

    Refs TESTOPS-234
    Refs #68585

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

    * style: Add the space after function in a restored test closure

    The #68585 revert brings back a closure in the Store API cart
    extensions test written as `function()`. The rule that wants
    `function ()` postdates it, and the CI lint job checks changed lines,
    so the restored line would fail it as it is.

    Refs #68585

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

    ---------

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

diff --git a/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-extensibility b/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-extensibility
deleted file mode 100644
index 7cbcdb3e571..00000000000
--- a/plugins/woocommerce/client/blocks/changelog/testops-234-cart-checkout-extensibility
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Move 4 extension cart update E2E tests to Jest and PHPUnit, covering the extensionCartUpdate wrapper, the cart thunk, the notice context, and the CartExtensions route; keep one browser title per spec.
-
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/thunks.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/thunks.ts
index b50e9787307..59a92989540 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/thunks.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/cart/test/thunks.ts
@@ -430,13 +430,12 @@ describe( 'applyExtensionCartUpdate', () => {
 	it( 'should include both addresses when customer data is not dirty', async () => {
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 		} )( { dispatch } as never );

 		expect( dispatch.receiveCart ).toHaveBeenCalledWith( mockResponse );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should set prefersCollection true when the extension response selects local pickup', async () => {
@@ -501,7 +500,7 @@ describe( 'applyExtensionCartUpdate', () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 		} )( { dispatch } as never );
@@ -510,14 +509,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
 		expect( received ).toHaveProperty( 'totals' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should strip both addresses when customer data is dirty and overwriteDirtyCustomerData is false', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: false,
@@ -527,14 +525,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
 		expect( received ).toHaveProperty( 'totals' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should include both addresses when overwriteDirtyCustomerData is true', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: true,
@@ -543,14 +540,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		const received = dispatch.receiveCart.mock.calls[ 0 ][ 0 ];
 		expect( received ).toHaveProperty( 'shipping_address' );
 		expect( received ).toHaveProperty( 'billing_address' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should overwrite only shipping_address when specified as object', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: { shipping_address: true },
@@ -562,14 +558,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		} );
 		expect( received ).not.toHaveProperty( 'billing_address' );
 		expect( received ).toHaveProperty( 'totals' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should overwrite only billing_address when specified as object', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: { billing_address: true },
@@ -580,14 +575,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received.billing_address ).toEqual( {
 			address_1: '456 Bill Ave',
 		} );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should overwrite both addresses when both specified in object', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: {
@@ -603,14 +597,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received.billing_address ).toEqual( {
 			address_1: '456 Bill Ave',
 		} );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should strip both addresses when object has explicit false flags', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: {
@@ -623,14 +616,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
 		expect( received ).toHaveProperty( 'totals' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should overwrite specified address even when customer data is not dirty', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( false );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: { shipping_address: true },
@@ -645,14 +637,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		expect( received.billing_address ).toEqual( {
 			address_1: '456 Bill Ave',
 		} );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should treat null as false (no overwrite)', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData:
@@ -662,14 +653,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		const received = dispatch.receiveCart.mock.calls[ 0 ][ 0 ];
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should treat an array as false (no overwrite)', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: [
@@ -680,14 +670,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		const received = dispatch.receiveCart.mock.calls[ 0 ][ 0 ];
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should treat non-boolean address fields as false', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: {
@@ -699,14 +688,13 @@ describe( 'applyExtensionCartUpdate', () => {
 		const received = dispatch.receiveCart.mock.calls[ 0 ][ 0 ];
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
-		expect( result ).toBe( mockResponse );
 	} );

 	it( 'should default missing address fields to false', async () => {
 		mockGetIsCustomerDataDirty.mockReturnValue( true );
 		const dispatch = createMockDispatch();

-		const result = await applyExtensionCartUpdate( {
+		await applyExtensionCartUpdate( {
 			namespace: 'test',
 			data: {},
 			overwriteDirtyCustomerData: {},
@@ -715,25 +703,5 @@ describe( 'applyExtensionCartUpdate', () => {
 		const received = dispatch.receiveCart.mock.calls[ 0 ][ 0 ];
 		expect( received ).not.toHaveProperty( 'shipping_address' );
 		expect( received ).not.toHaveProperty( 'billing_address' );
-		expect( result ).toBe( mockResponse );
-	} );
-
-	it( 'should dispatch and reject the same API error', async () => {
-		const dispatch = createMockDispatch();
-		const error = {
-			code: 'test_error',
-			message: 'This is an error with cart context.',
-			data: { status: 400, context: 'wc/cart' },
-		};
-		mockApiFetchWithHeaders.mockRejectedValueOnce( error );
-
-		await expect(
-			applyExtensionCartUpdate( {
-				namespace: 'test',
-				data: {},
-			} )( { dispatch } as never )
-		).rejects.toBe( error );
-
-		expect( dispatch.receiveError ).toHaveBeenCalledWith( error );
 	} );
 } );
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/block-data/utils/test/process-error-response.ts b/plugins/woocommerce/client/blocks/packages/public-api/block-data/utils/test/process-error-response.ts
index 994276d0df1..d153fc25e84 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/block-data/utils/test/process-error-response.ts
+++ b/plugins/woocommerce/client/blocks/packages/public-api/block-data/utils/test/process-error-response.ts
@@ -49,14 +49,6 @@ const errorResponse: ApiErrorResponse = {
 	},
 };

-const errorResponseWithoutContext: ApiErrorResponse = {
-	code: 'woocommerce_rest_cart_extensions_error',
-	message: 'There is no such namespace registered: test-plugin.',
-	data: {
-		status: 400,
-	},
-};
-
 describe( 'getNoticeContextFromErrorResponse', () => {
 	it( 'should generate notice contexts and ids for the correct fields/errors', () => {
 		const result = getNoticeContextFromErrorResponse( errorResponse );
@@ -81,24 +73,9 @@ describe( 'getNoticeContextFromErrorResponse', () => {
 		);
 		expect( result[ 0 ].context ).toEqual( 'test_context' );
 	} );
-
-	it( 'should use the cart context for a non-parameter error without a server context', () => {
-		expect(
-			getNoticeContextFromErrorResponse( errorResponseWithoutContext )
-		).toEqual( [
-			{
-				id: 'woocommerce_rest_cart_extensions_error',
-				context: 'wc/cart',
-			},
-		] );
-	} );
 } );

 describe( 'processErrorResponse', () => {
-	beforeEach( () => {
-		jest.clearAllMocks();
-	} );
-
 	it( 'should dismiss old notices and create new ones', () => {
 		processErrorResponse( errorResponse );
 		expect( createNotice ).toHaveBeenCalledTimes( 2 );
@@ -120,18 +97,4 @@ describe( 'processErrorResponse', () => {
 			}
 		);
 	} );
-
-	it( 'should create a non-parameter error notice in the cart context by default', () => {
-		processErrorResponse( errorResponseWithoutContext );
-
-		expect( createNotice ).toHaveBeenCalledTimes( 1 );
-		expect( createNotice ).toHaveBeenCalledWith(
-			'error',
-			'There is no such namespace registered: test-plugin.',
-			{
-				id: 'woocommerce_rest_cart_extensions_error',
-				context: 'wc/cart',
-			}
-		);
-	} );
 } );
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/blocks-checkout/utils/test/extension-cart-update.ts b/plugins/woocommerce/client/blocks/packages/public-api/blocks-checkout/utils/test/extension-cart-update.ts
deleted file mode 100644
index 3c85acb8609..00000000000
--- a/plugins/woocommerce/client/blocks/packages/public-api/blocks-checkout/utils/test/extension-cart-update.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * External dependencies
- */
-import { dispatch } from '@wordpress/data';
-import { processErrorResponse } from '@woocommerce/block-data';
-import type { CartResponse, ExtensionCartUpdateArgs } from '@woocommerce/types';
-
-/**
- * Internal dependencies
- */
-import { extensionCartUpdate } from '../extension-cart-update';
-import { STORE_KEY } from '../../../block-data/cart/constants';
-
-jest.mock( '@wordpress/data', () => ( {
-	dispatch: jest.fn(),
-} ) );
-
-jest.mock( '@woocommerce/block-data', () => ( {
-	processErrorResponse: jest.fn(),
-} ) );
-
-const mockDispatch = dispatch as jest.MockedFunction< typeof dispatch >;
-const mockProcessErrorResponse = processErrorResponse as jest.MockedFunction<
-	typeof processErrorResponse
->;
-const mockApplyExtensionCartUpdate = jest.fn();
-
-describe( 'extensionCartUpdate', () => {
-	beforeEach( () => {
-		jest.clearAllMocks();
-		mockDispatch.mockReturnValue( {
-			applyExtensionCartUpdate: mockApplyExtensionCartUpdate,
-		} as never );
-	} );
-
-	it.each< [ string, ExtensionCartUpdateArgs ] >( [
-		[
-			'with overwrite omitted',
-			{ namespace: 'test-extension', data: { value: 'omitted' } },
-		],
-		[
-			'with overwrite disabled',
-			{
-				namespace: 'test-extension',
-				data: { value: 'false' },
-				overwriteDirtyCustomerData: false,
-			},
-		],
-		[
-			'with overwrite enabled',
-			{
-				namespace: 'test-extension',
-				data: { value: 'true' },
-				overwriteDirtyCustomerData: true,
-			},
-		],
-		[
-			'with shipping-address overwrite enabled',
-			{
-				namespace: 'test-extension',
-				data: { value: 'shipping' },
-				overwriteDirtyCustomerData: { shipping_address: true },
-			},
-		],
-		[
-			'with billing-address overwrite enabled',
-			{
-				namespace: 'test-extension',
-				data: { value: 'billing' },
-				overwriteDirtyCustomerData: { billing_address: true },
-			},
-		],
-	] )(
-		'forwards arguments and fulfills with the same cart response %s',
-		async ( _, args ) => {
-			const response = { items: [] } as CartResponse;
-			mockApplyExtensionCartUpdate.mockResolvedValueOnce( response );
-
-			const result = await extensionCartUpdate( args );
-
-			expect( mockDispatch ).toHaveBeenCalledWith( STORE_KEY );
-			expect( mockApplyExtensionCartUpdate ).toHaveBeenCalledWith( args );
-			expect( result ).toBe( response );
-		}
-	);
-
-	it( 'rejects the same non-special error without creating a notice', async () => {
-		const error = {
-			code: 'test_error',
-			message: 'This is an extension error.',
-		};
-		mockApplyExtensionCartUpdate.mockRejectedValueOnce( error );
-
-		await expect(
-			extensionCartUpdate( {
-				namespace: 'test-extension',
-				data: {},
-			} )
-		).rejects.toBe( error );
-
-		expect( mockProcessErrorResponse ).not.toHaveBeenCalled();
-	} );
-
-	it( 'processes a special cart-extension error once and rejects that same error', async () => {
-		const error = {
-			code: 'woocommerce_rest_cart_extensions_error',
-			message: 'The cart extension could not be processed.',
-		};
-		mockApplyExtensionCartUpdate.mockRejectedValueOnce( error );
-
-		await expect(
-			extensionCartUpdate( {
-				namespace: 'test-extension',
-				data: {},
-			} )
-		).rejects.toBe( error );
-
-		expect( mockProcessErrorResponse ).toHaveBeenCalledTimes( 1 );
-		expect( mockProcessErrorResponse ).toHaveBeenCalledWith( error );
-	} );
-} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-extension-callbacks.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-extension-callbacks.shopper.block_theme.spec.ts
index 95078213a1b..37cabfe774a 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-extension-callbacks.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/cart/cart-checkout-block-extension-callbacks.shopper.block_theme.spec.ts
@@ -1,14 +1,50 @@
 /**
  * External dependencies
  */
-import { expect, test } from '@woocommerce/e2e-utils';
+import { expect, test as base } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
  */
+import { CheckoutPage } from '../checkout/checkout.page';
 import { REGULAR_PRICED_PRODUCT_NAME } from '../checkout/constants';

+const test = base.extend< { checkoutPageObject: CheckoutPage } >( {
+	checkoutPageObject: async ( { page }, use ) => {
+		const pageObject = new CheckoutPage( {
+			page,
+		} );
+		await use( pageObject );
+	},
+} );
+
 test.describe( 'Shopper → Cart Extension Callbacks', () => {
+	test( 'Custom error code creates exception', async ( {
+		frontendUtils,
+		requestUtils,
+		page,
+	} ) => {
+		await requestUtils.activatePlugin(
+			'woocommerce-blocks-test-cart-extensions'
+		);
+
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.goToCart();
+
+		await expect(
+			page.evaluate( () =>
+				window.wc.blocksCheckout
+					.extensionCartUpdate( {
+						namespace: 'cart-extensions-test-helper',
+					} )
+					.catch( ( error ) => {
+						throw new Error( error.message );
+					} )
+			)
+		).rejects.toThrow( 'This is an error with cart context.' );
+	} );
+
 	test( 'Error code `woocommerce_rest_cart_extensions_error` creates notice', async ( {
 		frontendUtils,
 		requestUtils,
@@ -34,4 +70,27 @@ test.describe( 'Shopper → Cart Extension Callbacks', () => {
 				.getByText( 'This is an error with cart context.' )
 		).toBeVisible();
 	} );
+
+	test( 'Invalid callback namespace creates notice', async ( {
+		frontendUtils,
+		page,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+		await frontendUtils.goToCart();
+
+		await page.evaluate( () => {
+			window.wc.blocksCheckout.extensionCartUpdate( {
+				namespace: 'invalid-namespace',
+			} );
+		} );
+
+		await expect(
+			page
+				.locator( '.wc-block-components-notice-banner__content' )
+				.getByText(
+					'There is no such namespace registered: invalid-namespace.'
+				)
+		).toBeVisible();
+	} );
 } );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-extensibility.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-extensibility.shopper.block_theme.spec.ts
index fe772da10ab..e7a05fda2ba 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-extensibility.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block-extensibility.shopper.block_theme.spec.ts
@@ -40,13 +40,51 @@ test.describe( 'Shopper → Extensibility', () => {
 		await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
 		await frontendUtils.goToCheckout();
 	} );
-
 	test.describe( 'extensionCartUpdate', () => {
-		test( 'Cart data can be modified by extensions', async ( {
+		test( 'Response is not undefined in any code path', async ( {
 			checkoutPageObject,
 		} ) => {
-			const { page } = checkoutPageObject;
+			// With no additional args.
+			let response = await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update' } ).then( ( response ) => response );"
+			);
+			let resolvedResponse = await Promise.resolve( response );
+			expect( resolvedResponse ).not.toBeUndefined();
+			expect( resolvedResponse ).toHaveProperty( 'billing_address' );
+
+			// With overwriteDirtyCustomerData true.
+			response = await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update', overwriteDirtyCustomerData: true } ).then( ( response ) => response );"
+			);
+			resolvedResponse = await Promise.resolve( response );
+			expect( resolvedResponse ).not.toBeUndefined();
+			expect( resolvedResponse ).toHaveProperty( 'billing_address' );
+
+			// With overwriteDirtyCustomerData false.
+			response = await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update', overwriteDirtyCustomerData: false } ).then( ( response ) => response );"
+			);
+			resolvedResponse = await Promise.resolve( response );
+			expect( resolvedResponse ).not.toBeUndefined();
+			expect( resolvedResponse ).toHaveProperty( 'billing_address' );

+			// With a dirty customer object.
+			await checkoutPageObject.page
+				.getByLabel( 'Country/Region' )
+				.selectOption( 'United Kingdom (UK)' );
+			await expect(
+				checkoutPageObject.page.getByLabel( 'Country/Region' )
+			).toHaveValue( 'GB' );
+			response = await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update' } ).then( ( response ) => response );"
+			);
+			resolvedResponse = await Promise.resolve( response );
+			expect( resolvedResponse ).not.toBeUndefined();
+			expect( resolvedResponse ).toHaveProperty( 'billing_address' );
+		} );
+		test( 'Unpushed data is/is not overwritten depending on arg', async ( {
+			checkoutPageObject,
+		} ) => {
 			// Fill in the address, then wait until it has reached the server. The dirty flag is
 			// not enough on its own: it is cleared by whichever push finishes first, which can
 			// be an earlier one that did not carry the address.
@@ -54,7 +92,7 @@ test.describe( 'Shopper → Extensibility', () => {
 			await expect
 				.poll(
 					async () =>
-						page.evaluate( async () => {
+						checkoutPageObject.page.evaluate( async () => {
 							const response = await fetch(
 								'/wp-json/wc/store/v1/cart'
 							);
@@ -72,49 +110,54 @@ test.describe( 'Shopper → Extensibility', () => {
 				.toBe( '90210' );

 			// A postcode that fails validation is never pushed, so it only exists in the browser.
-			// A country change is not usable here: picking one pushes straight away so shipping
-			// can be recalculated, which would leave nothing unpushed to overwrite.
-			const postcode = page.locator( '#shipping-postcode' );
+			const postcode =
+				checkoutPageObject.page.locator( '#shipping-postcode' );
 			await postcode.fill( 'ABCDEF' );
 			await postcode.blur();
-			await page.waitForFunction( () => {
-				return (
+			await checkoutPageObject.page.waitForFunction(
+				() =>
 					window.localStorage.getItem(
 						'WOOCOMMERCE_CHECKOUT_IS_CUSTOMER_DATA_DIRTY'
 					) === 'true'
-				);
-			} );
+			);

 			// Without the arg, the unpushed postcode is kept.
-			await page.evaluate( () =>
-				window.wc.blocksCheckout.extensionCartUpdate( {
-					namespace: 'woocommerce-blocks-test-extension-cart-update',
-				} )
+			await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update' } )"
 			);
 			await expect( postcode ).toHaveValue( 'ABCDEF' );

 			// With overwriteDirtyCustomerData, the address from the server replaces it.
-			const overwriteResponse = await page.evaluate( () =>
-				window.wc.blocksCheckout.extensionCartUpdate( {
-					namespace: 'woocommerce-blocks-test-extension-cart-update',
-					overwriteDirtyCustomerData: true,
-				} )
-			);
-			expect( overwriteResponse.shipping_address.postcode ).toBe(
-				'90210'
+			await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update', overwriteDirtyCustomerData: true } )"
 			);
 			await expect( postcode ).toHaveValue( '90210' );

-			await page.evaluate( () =>
-				window.wc.blocksCheckout.extensionCartUpdate( {
-					namespace: 'woocommerce-blocks-test-extension-cart-update',
-					data: { 'test-name-change': true },
-					overwriteDirtyCustomerData: true,
-				} )
+			await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update', overwriteDirtyCustomerData: true } )"
 			);
-			await expect( page.getByLabel( 'First name' ) ).toHaveValue(
-				'Mr. Test'
+			await expect(
+				checkoutPageObject.page.getByLabel( 'Country/Region' )
+			).toHaveValue( 'US' );
+			await expect( postcode ).toHaveValue( '90210' );
+		} );
+		test( 'Cart data can be modified by extensions', async ( {
+			checkoutPageObject,
+		} ) => {
+			await checkoutPageObject.fillInCheckoutWithTestData();
+			await checkoutPageObject.page.waitForFunction( () => {
+				return (
+					window.localStorage.getItem(
+						'WOOCOMMERCE_CHECKOUT_IS_CUSTOMER_DATA_DIRTY'
+					) === 'false'
+				);
+			} );
+			await checkoutPageObject.page.evaluate(
+				"wc.blocksCheckout.extensionCartUpdate( { namespace: 'woocommerce-blocks-test-extension-cart-update', data: { 'test-name-change': true } } )"
 			);
+			await expect(
+				checkoutPageObject.page.getByLabel( 'First name' )
+			).toHaveValue( 'Mr. Test' );
 		} );
 	} );
 } );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
index 0eacde6b443..79301a7aa0c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartExtensions.php
@@ -9,9 +9,6 @@ use Automattic\WooCommerce\Enums\OrderItemType;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes\ControllerTestCase;
 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
-use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
-use Automattic\WooCommerce\StoreApi\StoreApi;

 /**
  * Cart Controller Tests.
@@ -51,11 +48,10 @@ class CartExtensions extends ControllerTestCase {
 			)
 		);
 	}
-
 	/**
-	 * @testdox Invalid namespace errors expose the mapped Store API response without a context override.
+	 * Test getting cart with invalid namespace.
 	 */
-	public function test_invalid_namespace(): void {
+	public function test_invalid_namespace() {
 		$request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/extensions' );
 		$request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
 		$request->set_body_params(
@@ -63,61 +59,10 @@ class CartExtensions extends ControllerTestCase {
 				'namespace' => 'test-plugin',
 			)
 		);
-		$response = rest_get_server()->dispatch( $request );
-		$data     = $response->get_data();
-
-		$this->assertSame( 400, $response->get_status(), 'An invalid extension namespace should return HTTP 400.' );
-		$this->assertSame( 'woocommerce_rest_cart_extensions_error', $data['code'], 'The response should use the mapped cart-extension error code.' );
-		$this->assertSame( 'There is no such namespace registered: test-plugin.', $data['message'], 'The response should identify the missing namespace exactly.' );
-		$this->assertSame( 400, $data['data']['status'], 'The response data should preserve the HTTP status.' );
-		$this->assertArrayNotHasKey( 'context', $data['data'], 'The invalid-namespace response should leave notice context selection to the client.' );
-	}
-
-	/**
-	 * @testdox Extension callback RouteException data passes through the public route unchanged.
-	 */
-	public function test_callback_route_exception_passes_through(): void {
-		$extend            = StoreApi::container()->get( ExtendSchema::class );
-		$original_callback = $extend->get_update_callback( 'valid-test-plugin' );
-
-		woocommerce_store_api_register_update_callback(
-			array(
-				'namespace' => 'valid-test-plugin',
-				'callback'  => function () {
-					throw new RouteException(
-						'test_error',
-						'This is an error with cart context.',
-						400,
-						array( 'context' => 'wc/cart' )
-					);
-				},
-			)
+		$this->assertAPIResponse(
+			$request,
+			400
 		);
-
-		try {
-			$request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/extensions' );
-			$request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
-			$request->set_body_params(
-				array(
-					'namespace' => 'valid-test-plugin',
-				)
-			);
-			$response = rest_get_server()->dispatch( $request );
-			$data     = $response->get_data();
-
-			$this->assertSame( 400, $response->get_status(), 'The callback RouteException should preserve its HTTP status.' );
-			$this->assertSame( 'test_error', $data['code'], 'The callback RouteException should preserve its error code.' );
-			$this->assertSame( 'This is an error with cart context.', $data['message'], 'The callback RouteException should preserve its message.' );
-			$this->assertSame( 400, $data['data']['status'], 'The callback RouteException response data should preserve its status.' );
-			$this->assertSame( 'wc/cart', $data['data']['context'], 'The callback RouteException should preserve its notice context.' );
-		} finally {
-			woocommerce_store_api_register_update_callback(
-				array(
-					'namespace' => 'valid-test-plugin',
-					'callback'  => $original_callback,
-				)
-			);
-		}
 	}

 	/**