Commit 95e0a58cefc for woocommerce
commit 95e0a58cefcbd16da79faa41ccea9c6ea1407e53
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Tue Sep 15 16:05:06 2026 +0300
[tests] Reduce additional checkout field E2E tests from 12 to 7 (#68588)
* test(blocks): Reduce additional checkout field E2E tests from 12 to 7
Three Blocks E2E specs ran twelve browser titles for additional
checkout fields. Several walked the same journey for a guest and a
logged-in shopper, and the merchant spec split one order editor
journey into two titles.
Add CheckoutFieldsAdminTest. It registers an address, a contact, and
an order field, fires the billing and shipping admin field filters,
and checks group placement, the formatted meta box properties, and
that each field's update callback saves to the right group. Merge the
two merchant titles into one HPOS order editor journey with a step per
half, and remove the guest required-field title and three logged-in
titles. Keep trunk's JSON schema validation title: no lower test
submits a value that fails a field's validation pattern.
Consolidates the mega-branch slices:
- blocks-flow-07-additional-fields: test(blocks): right-size checkout
field coverage
- test(e2e): Stabilize Blocks acceptance fixtures (guest-shopper spec
only: the conditional field's cart total moves from $40 to $60)
Refs TESTOPS-234
Refs #68046
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Own the checkbox error message and sanitize-before-validate
Two behaviours the demoted browser titles were the last owners of.
`ValidatedCheckboxControl` replaces the generated validity message with
the field's own `errorMessage`, which is how a required checkbox's
custom message reaches a shopper. A new Jest pair covers it: one case
for the custom message, one for the generated fallback, so the first
cannot pass on any message that happens to render.
The route test registers a field that uppercases in `sanitize_callback`
and requires uppercase in `validate_callback`. A lowercase value is
invalid as submitted and valid once sanitized, so it can only be
accepted if validation reads the sanitized value, and the stored value
proves which one was kept. A second value stays invalid after
uppercasing, which separates the ordering from the rule itself.
Uppercasing rather than trimming is deliberate. The first attempt reused
`gov-id`, whose `sanitize_callback` trims, and it passed with that
callback disabled: the address sanitizers already trim. Nothing else in
the pipeline changes case.
Mutation confirms both. Removing the `errorMessage` override fails the
custom-message case and leaves the fallback green, and disabling the
field's `sanitize_callback` fails the value that is only valid once
sanitized.
Refs #68588
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(store-api): Use ctype_upper in the sanitize-before-validate field
The field's validate_callback matched /^[A-Z]+$/ where ctype_upper()
says the same thing more directly, which is what the repo's guidance on
regular expressions asks for.
It is also slightly stricter. PCRE's `$` matches before a trailing
newline, so "ABC\n" satisfied the pattern and does not satisfy
ctype_upper().
Checked the swap did not blunt the test: with the field's
sanitize_callback reduced to returning its input, the request comes back
400 instead of 200 and the test fails, so the assertion still depends on
sanitization running before validation. Restored byte-identical after.
Whole suite on this branch: 15173 tests, 59258 assertions, no failures.
Refs TESTOPS-234
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Cover the checkbox error clearing and fix a no-op assertion
Three things, all in the coverage this PR moved off the deleted shopper
spec.
The Jest test rendered with checked fixed at false, so it only covered
revealing the error. Clearing runs through a different path --
onChange -> validateInput( false ) -> clearValidationError -- and nothing
exercised it at any layer once the E2E went. A regression there leaves a
blocking error on screen after the shopper has already ticked the box.
The new case drives it through a stateful parent, the way checkout does.
Verified by mutation: dropping the validateInput call from the change
handler turns it red, and production was restored afterwards.
The guest-shopper spec then asserted that "Add shipping insurance is a
required field." was hidden. That string appears nowhere in the plugin --
the block renders "Please check this box if you want to proceed." -- so
toBeHidden() matched zero elements and passed whether or not the error
had cleared. It now asserts the same string the visible check above it
uses.
Finally, that spec cleared the field's $59 required threshold with three
$20 products, a dollar of headroom. A fourth product makes it $21, so a
sale price or a change in how totals are computed cannot quietly drop the
field out of "required" and fail the rest of the test for an unrelated
reason.
Refs TESTOPS-234
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-fields b/plugins/woocommerce/changelog/testops-234-checkout-block-fields
new file mode 100644
index 00000000000..4352a4c5cd1
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-checkout-block-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce additional checkout field E2E tests from 12 to 7; a new CheckoutFieldsAdminTest owns the order admin field formatting and update contract.
+
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/blocks-components/checkbox-control/test/validated-checkbox-control.tsx b/plugins/woocommerce/client/blocks/packages/public-api/blocks-components/checkbox-control/test/validated-checkbox-control.tsx
new file mode 100644
index 00000000000..8206124aaa6
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/public-api/blocks-components/checkbox-control/test/validated-checkbox-control.tsx
@@ -0,0 +1,93 @@
+/**
+ * External dependencies
+ */
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import { validationStore } from '@woocommerce/block-data';
+import { dispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import ValidatedCheckboxControl from '../validated-checkbox-control';
+
+describe( 'ValidatedCheckboxControl', () => {
+ const label = 'Test required checkbox';
+ const errorMessage = 'Please check the box or you will be unable to order';
+
+ const renderUnchecked = ( props = {} ) =>
+ render(
+ <ValidatedCheckboxControl
+ id="required-checkbox"
+ label={ label }
+ required
+ checked={ false }
+ onChange={ () => void 0 }
+ { ...props }
+ />
+ );
+
+ it( "Shows the field's own error message once errors are revealed", async () => {
+ renderUnchecked( { errorMessage } );
+
+ // Validation runs on mount but starts hidden, which is what a shopper sees
+ // before they try to place the order.
+ expect( screen.queryByText( errorMessage ) ).not.toBeInTheDocument();
+
+ await act( async () => {
+ dispatch( validationStore ).showAllValidationErrors();
+ } );
+
+ expect( screen.getByText( errorMessage ) ).toBeInTheDocument();
+ } );
+
+ it( 'Clears the error once the shopper checks the box', async () => {
+ // The two cases above render with checked fixed at false, so they only cover
+ // the reveal. The clear runs through a different path entirely --
+ // onChange -> validateInput( false ) -> clearValidationError -- and a
+ // regression there leaves a blocking error on screen after the shopper has
+ // already fixed the problem. Drive it through a stateful parent, the way
+ // checkout does, so the checkbox actually ends up checked.
+ const StatefulCheckbox = () => {
+ const [ checked, setChecked ] = useState( false );
+
+ return (
+ <ValidatedCheckboxControl
+ id="required-checkbox"
+ label={ label }
+ required
+ checked={ checked }
+ onChange={ setChecked }
+ errorMessage={ errorMessage }
+ />
+ );
+ };
+
+ render( <StatefulCheckbox /> );
+
+ await act( async () => {
+ dispatch( validationStore ).showAllValidationErrors();
+ } );
+ expect( screen.getByText( errorMessage ) ).toBeInTheDocument();
+
+ fireEvent.click( screen.getByRole( 'checkbox' ) );
+
+ expect( screen.getByRole( 'checkbox' ) ).toBeChecked();
+ expect( screen.queryByText( errorMessage ) ).not.toBeInTheDocument();
+ } );
+
+ it( 'Falls back to the generated message when the field supplies none', async () => {
+ renderUnchecked();
+
+ await act( async () => {
+ dispatch( validationStore ).showAllValidationErrors();
+ } );
+
+ // Without this the test above would pass on any message at all, rather than
+ // on the one the field asked for.
+ expect( screen.queryByText( errorMessage ) ).not.toBeInTheDocument();
+ expect(
+ screen.getByText( 'Please check this box if you want to proceed.' )
+ ).toBeInTheDocument();
+ } );
+} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
index cf45a55174d..c4e0e7c12f1 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.guest-shopper.block_theme.spec.ts
@@ -29,96 +29,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
);
} );
- test( 'Shopper can see an error message when a required field is not filled in the checkout form', async ( {
- checkoutPageObject,
- frontendUtils,
- } ) => {
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'For my non-ascii named friend: niño',
- },
- address: {
- shipping: {
- 'Government ID': '',
- 'Confirm government ID': '',
- },
- billing: {
- 'Government ID': '54321',
- 'Confirm government ID': '54321',
- },
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- // Use the data store to specifically unset the field value - this is because it might be saved in the user-state.
- await checkoutPageObject.page.evaluate( () => {
- window.wp.data.dispatch( 'wc/store/cart' ).setShippingAddress( {
- 'first-plugin-namespace/road-size': '',
- } );
- } );
-
- await checkoutPageObject.placeOrder( false );
-
- // Test that the required checkbox warning shows up after submitting without interacting.
- await expect(
- checkoutPageObject.page.getByText(
- 'Please check the box or you will be unable to order'
- )
- ).toBeVisible();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .click();
-
- await expect(
- checkoutPageObject.page.getByText(
- 'Please check the box or you will be unable to order'
- )
- ).toBeHidden();
-
- // Test that unchecking shows and checking again hides the message.
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .uncheck();
-
- await expect(
- checkoutPageObject.page.getByText(
- 'Please check the box or you will be unable to order'
- )
- ).toBeVisible();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .click();
-
- await expect(
- checkoutPageObject.page.getByText(
- 'Please check the box or you will be unable to order'
- )
- ).toBeHidden();
-
- await expect(
- checkoutPageObject.page.getByText(
- 'Please enter a valid government id'
- )
- ).toBeVisible();
- } );
-
test( 'Shopper can fill in the checkout form with additional fields and can have different value for same field in shipping and billing address', async ( {
checkoutPageObject,
frontendUtils,
@@ -402,16 +312,21 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCheckout();
- // The shipping insurance field should be hidden by default (cart total < 2000)
+ // The field is hidden while the cart total is at or below $40.
await expect(
checkoutPageObject.page.getByLabel( 'Add shipping insurance' )
).toBeHidden();
await frontendUtils.goToShop();
await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
+ await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
await frontendUtils.goToCheckout();
- // The shipping insurance field should now be visible (cart total > 2000)
+ // Four $20 products against the field's $59 required threshold. Three
+ // would also clear it, but only by a dollar, so any sale price or
+ // total-computation change would silently drop the field out of
+ // "required" and fail the assertions below for an unrelated reason.
await expect(
checkoutPageObject.page.getByLabel( 'Add shipping insurance' )
).toBeVisible();
@@ -458,10 +373,13 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
await checkoutPageObject.waitForCheckoutToFinishUpdating();
- // The error should be gone
+ // The error should be gone. Assert the same string the visible check
+ // above uses: "Add shipping insurance is a required field." appears
+ // nowhere in the plugin, so toBeHidden() was matching zero elements
+ // and passing whether or not the error had actually cleared.
await expect(
checkoutPageObject.page.getByText(
- 'Add shipping insurance is a required field.'
+ 'Please check this box if you want to proceed.'
)
).toBeHidden();
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
index be1fa356551..070008aedc8 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.merchant.block_theme.spec.ts
@@ -29,387 +29,317 @@ test.describe( 'Merchant → Additional Checkout Fields', () => {
await frontendUtils.goToCheckout();
} );
- test( 'Merchant can see additional fields in the order admin page', async ( {
+ test( 'Merchant can view and edit additional fields in the order admin page', async ( {
checkoutPageObject,
admin,
} ) => {
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you!',
- 'Is this a personal purchase or a business purchase?':
- 'business',
- },
- address: {
- shipping: {
- 'Government ID': '12345',
- 'Confirm government ID': '12345',
+ await test.step( 'Merchant can see additional fields in the order admin page', async () => {
+ await checkoutPageObject.editShippingDetails();
+ await checkoutPageObject.unsyncBillingWithShipping();
+ await checkoutPageObject.editBillingDetails();
+ await checkoutPageObject.fillInCheckoutWithTestData(
+ {},
+ {
+ contact: {
+ 'Alternative Email': 'test@test.com',
+ 'Enter a gift message to include in the package':
+ 'This is for you!',
+ 'Is this a personal purchase or a business purchase?':
+ 'business',
},
- billing: {
- 'Government ID': '54321',
- 'Confirm government ID': '54321',
+ address: {
+ shipping: {
+ 'Government ID': '12345',
+ 'Confirm government ID': '12345',
+ },
+ billing: {
+ 'Government ID': '54321',
+ 'Confirm government ID': '54321',
+ },
},
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .selectOption( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .selectOption( 'narrow' );
-
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .check();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .check();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
-
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .uncheck();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .check();
-
- await checkoutPageObject.placeOrder();
-
- const orderId = checkoutPageObject.getOrderId();
- await admin.page.goto(
- `wp-admin/post.php?post=${ orderId }&action=edit`
- );
-
- await expect(
- admin.page.getByText( 'Government ID: 12345', { exact: true } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Confirm government ID: 12345', {
- exact: true,
- } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Government ID: 54321', { exact: true } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Confirm government ID: 54321', {
- exact: true,
- } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'What is your favourite colour?: Blue' )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Enter a gift message to include in the package: This is for you!'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Do you want to subscribe to our newsletter?: Yes'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Would you like a free gift with your order?: Yes'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Can a truck fit down your road?: Yes' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Can a truck fit down your road?: No' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How wide is your road?: Wide' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How wide is your road?: Narrow' )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Is this a personal purchase or a business purchase?: Business'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How did you hear about us?: Other' )
- ).toBeVisible();
- } );
-
- test( 'Merchant can edit custom fields from the order admin page', async ( {
- checkoutPageObject,
- admin,
- } ) => {
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you!',
- 'Is this a personal purchase or a business purchase?':
- 'business',
- },
- address: {
- shipping: {
- 'Government ID': '12345',
- 'Confirm government ID': '12345',
+ order: {
+ 'How did you hear about us?': 'Other',
+ 'What is your favourite colour?': 'Blue',
},
- billing: {
- 'Government ID': '54321',
- 'Confirm government ID': '54321',
- },
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .selectOption( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .selectOption( 'narrow' );
-
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .check();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .check();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
-
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .uncheck();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .check();
-
- await checkoutPageObject.placeOrder();
-
- const orderId = checkoutPageObject.getOrderId();
- await admin.page.goto(
- `wp-admin/post.php?post=${ orderId }&action=edit`
- );
-
- await admin.page
- .getByRole( 'heading', { name: 'Billing Edit' } )
- .getByRole( 'link' )
- .click();
-
- // Change all the billing details
- await admin.page
- .getByRole( 'textbox', {
- name: 'Government ID',
- exact: true,
- } )
- .fill( '99999' );
- await admin.page
- .getByRole( 'textbox', {
- name: 'Confirm government ID',
- exact: true,
- } )
- .fill( '99999' );
- await admin.page
- .getByRole( 'checkbox', {
- name: 'Can a truck fit down your road?',
- } )
- .check();
-
- // Use Locator here because the select2 box is duplicated in shipping.
- await admin.page
- .locator(
- '[id="\\_wc_billing\\/first-plugin-namespace\\/road-size"]'
- )
- .selectOption( 'wide' );
-
- // Handle changing the contact fields.
- await admin.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .uncheck();
- await admin.page
- .getByLabel( 'Enter a gift message to include in the package' )
- .fill( 'Some other message' );
- await admin.page
- .getByLabel( 'Is this a personal purchase or a business purchase?' )
- .selectOption( 'personal' );
-
- const clickPromise = admin.page
- .getByRole( 'button', { name: 'Update' } )
- .first()
- .click();
-
- const navigationPromise = admin.page.waitForEvent( 'domcontentloaded' );
-
- // When update is clicked without waiting for DOMContentLoaded the page becomes
- // available before click handlers are attached to the shipping edit link.
- await Promise.all( [ clickPromise, navigationPromise ] );
-
- await admin.page
- .getByRole( 'heading', { name: 'Shipping Edit' } )
- .getByRole( 'link' )
- .click();
-
- // Change all the shipping details
- await admin.page
- .getByRole( 'textbox', {
- name: 'Government ID',
- exact: true,
- } )
- .fill( '88888' );
- await admin.page
- .getByRole( 'textbox', {
- name: 'Confirm government ID',
- exact: true,
- } )
- .fill( '88888' );
- await admin.page
- .getByRole( 'checkbox', {
- name: 'Can a truck fit down your road?',
- } )
- .uncheck();
-
- // Use Locator here because the select2 box is duplicated in billing.
- await admin.page
- .locator(
- '[id="\\_wc_shipping\\/first-plugin-namespace\\/road-size"]'
- )
- .selectOption( 'super-wide' );
-
- // Handle changing the additional information fields.
- await admin.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .uncheck();
- await admin.page
- .getByLabel( 'What is your favourite colour?' )
- .fill( 'Green' );
- await admin.page
- .getByLabel( 'How did you hear about us?' )
- .selectOption( 'google' );
-
- await admin.page
- .getByRole( 'button', { name: 'Update' } )
- .first()
- .click();
-
- await admin.page.waitForLoadState( 'domcontentloaded' );
+ }
+ );
+
+ // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
+ // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
+ // by label alone is not reliable unless we know the value.
+ await checkoutPageObject.page
+ .getByRole( 'group', {
+ name: 'Shipping address',
+ } )
+ .getByLabel( 'How wide is your road?' )
+ .selectOption( 'wide' );
+ await checkoutPageObject.page
+ .getByRole( 'group', {
+ name: 'Billing address',
+ } )
+ .getByLabel( 'How wide is your road?' )
+ .selectOption( 'narrow' );
+
+ await checkoutPageObject.page.evaluate(
+ 'document.activeElement.blur()'
+ );
+
+ await checkoutPageObject.page
+ .getByLabel( 'Would you like a free gift with your order?' )
+ .check();
+ await checkoutPageObject.page
+ .getByLabel( 'Do you want to subscribe to our newsletter?' )
+ .check();
+ await checkoutPageObject.page
+ .getByRole( 'group', {
+ name: 'Shipping address',
+ } )
+ .getByLabel( 'Can a truck fit down your road?' )
+ .check();
+
+ await checkoutPageObject.page
+ .getByRole( 'group', {
+ name: 'Billing address',
+ } )
+ .getByLabel( 'Can a truck fit down your road?' )
+ .uncheck();
+
+ await checkoutPageObject.page
+ .getByLabel( 'Test required checkbox' )
+ .check();
+
+ await checkoutPageObject.placeOrder();
+
+ const orderId = checkoutPageObject.getOrderId();
+ await admin.page.goto(
+ `wp-admin/admin.php?page=wc-orders&action=edit&id=${ orderId }`
+ );
+ await expect( admin.page ).toHaveURL(
+ new RegExp(
+ `admin\\.php\\?page=wc-orders&action=edit&id=${ orderId }$`
+ )
+ );
+
+ await expect(
+ admin.page.getByText( 'Government ID: 12345', { exact: true } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Confirm government ID: 12345', {
+ exact: true,
+ } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Government ID: 54321', { exact: true } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Confirm government ID: 54321', {
+ exact: true,
+ } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'What is your favourite colour?: Blue' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Enter a gift message to include in the package: This is for you!'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Do you want to subscribe to our newsletter?: Yes'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Would you like a free gift with your order?: Yes'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Can a truck fit down your road?: Yes' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Can a truck fit down your road?: No' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How wide is your road?: Wide' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How wide is your road?: Narrow' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Is this a personal purchase or a business purchase?: Business'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How did you hear about us?: Other' )
+ ).toBeVisible();
+ } );
- await expect(
- admin.page.getByText( 'Government ID: 88888', { exact: true } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Confirm government ID: 88888', {
- exact: true,
- } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Government ID: 99999', { exact: true } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Confirm government ID: 99999', {
- exact: true,
- } )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'What is your favourite colour?: Green' )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Enter a gift message to include in the package: Some other message'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Do you want to subscribe to our newsletter?: No'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Would you like a free gift with your order?: No'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Can a truck fit down your road?: Yes' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'Can a truck fit down your road?: No' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How wide is your road?: Super wide' )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How wide is your road?: Wide' )
- ).toBeVisible();
- await expect(
- admin.page.getByText(
- 'Is this a personal purchase or a business purchase?: Personal'
- )
- ).toBeVisible();
- await expect(
- admin.page.getByText( 'How did you hear about us?: Google' )
- ).toBeVisible();
+ const orderAdminUrl = admin.page.url();
+
+ await test.step( 'Merchant can edit custom fields from the order admin page', async () => {
+ await admin.page.goto( orderAdminUrl );
+
+ await admin.page
+ .getByRole( 'heading', { name: 'Billing Edit' } )
+ .getByRole( 'link' )
+ .click();
+
+ // Change all the billing details
+ await admin.page
+ .getByRole( 'textbox', {
+ name: 'Government ID',
+ exact: true,
+ } )
+ .fill( '99999' );
+ await admin.page
+ .getByRole( 'textbox', {
+ name: 'Confirm government ID',
+ exact: true,
+ } )
+ .fill( '99999' );
+ await admin.page
+ .getByRole( 'checkbox', {
+ name: 'Can a truck fit down your road?',
+ } )
+ .check();
+
+ // Use Locator here because the select2 box is duplicated in shipping.
+ await admin.page
+ .locator(
+ '[id="\\_wc_billing\\/first-plugin-namespace\\/road-size"]'
+ )
+ .selectOption( 'wide' );
+
+ // Handle changing the contact fields.
+ await admin.page
+ .getByLabel( 'Do you want to subscribe to our newsletter?' )
+ .uncheck();
+ await admin.page
+ .getByLabel( 'Enter a gift message to include in the package' )
+ .fill( 'Some other message' );
+ await admin.page
+ .getByLabel(
+ 'Is this a personal purchase or a business purchase?'
+ )
+ .selectOption( 'personal' );
+
+ const clickPromise = admin.page
+ .getByRole( 'button', { name: 'Update' } )
+ .first()
+ .click();
+
+ const navigationPromise =
+ admin.page.waitForEvent( 'domcontentloaded' );
+
+ // When update is clicked without waiting for DOMContentLoaded the page becomes
+ // available before click handlers are attached to the shipping edit link.
+ await Promise.all( [ clickPromise, navigationPromise ] );
+
+ await admin.page
+ .getByRole( 'heading', { name: 'Shipping Edit' } )
+ .getByRole( 'link' )
+ .click();
+
+ // Change all the shipping details
+ await admin.page
+ .getByRole( 'textbox', {
+ name: 'Government ID',
+ exact: true,
+ } )
+ .fill( '88888' );
+ await admin.page
+ .getByRole( 'textbox', {
+ name: 'Confirm government ID',
+ exact: true,
+ } )
+ .fill( '88888' );
+ await admin.page
+ .getByRole( 'checkbox', {
+ name: 'Can a truck fit down your road?',
+ } )
+ .uncheck();
+
+ // Use Locator here because the select2 box is duplicated in billing.
+ await admin.page
+ .locator(
+ '[id="\\_wc_shipping\\/first-plugin-namespace\\/road-size"]'
+ )
+ .selectOption( 'super-wide' );
+
+ // Handle changing the additional information fields.
+ await admin.page
+ .getByLabel( 'Would you like a free gift with your order?' )
+ .uncheck();
+ await admin.page
+ .getByLabel( 'What is your favourite colour?' )
+ .fill( 'Green' );
+ await admin.page
+ .getByLabel( 'How did you hear about us?' )
+ .selectOption( 'google' );
+
+ await admin.page
+ .getByRole( 'button', { name: 'Update' } )
+ .first()
+ .click();
+
+ await admin.page.waitForLoadState( 'domcontentloaded' );
+
+ await expect(
+ admin.page.getByText( 'Government ID: 88888', { exact: true } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Confirm government ID: 88888', {
+ exact: true,
+ } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Government ID: 99999', { exact: true } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Confirm government ID: 99999', {
+ exact: true,
+ } )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'What is your favourite colour?: Green' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Enter a gift message to include in the package: Some other message'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Do you want to subscribe to our newsletter?: No'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Would you like a free gift with your order?: No'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Can a truck fit down your road?: Yes' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'Can a truck fit down your road?: No' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How wide is your road?: Super wide' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How wide is your road?: Wide' )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText(
+ 'Is this a personal purchase or a business purchase?: Personal'
+ )
+ ).toBeVisible();
+ await expect(
+ admin.page.getByText( 'How did you hear about us?: Google' )
+ ).toBeVisible();
+ } );
} );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
index d42f15a840f..62ab317b26a 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/additional-fields.shopper.block_theme.spec.ts
@@ -29,529 +29,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
);
} );
- test( 'Shopper can fill in the checkout form with additional fields and can have different value for same field in shipping and billing address', async ( {
- checkoutPageObject,
- frontendUtils,
- } ) => {
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you!',
- 'Is this a personal purchase or a business purchase?':
- 'business',
- },
- address: {
- shipping: {
- 'Government ID': '12345',
- 'Confirm government ID': '12345',
- 'How wide is your road? (optional)': 'wide',
- },
- billing: {
- 'Government ID': '54321',
- 'Confirm government ID': '54321',
- 'How wide is your road? (optional)': 'narrow',
- },
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .check();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .check();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
-
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .uncheck();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .check();
-
- await checkoutPageObject.placeOrder();
-
- expect(
- await checkoutPageObject.verifyAdditionalFieldsDetails( [
- [ 'Government ID', '12345' ],
- [ 'Government ID', '54321' ],
- [ 'What is your favourite colour?', 'Blue' ],
- [
- 'Enter a gift message to include in the package',
- 'This is for you!',
- ],
- [ 'Do you want to subscribe to our newsletter?', 'Yes' ],
- [ 'Would you like a free gift with your order?', 'Yes' ],
- [ 'Can a truck fit down your road?', 'Yes' ],
- [ 'Can a truck fit down your road?', 'No' ],
- [ 'How wide is your road?', 'Wide' ],
- [ 'How wide is your road?', 'Narrow' ],
- [
- 'Is this a personal purchase or a business purchase?',
- 'business',
- ],
- ] )
- ).toBe( true );
-
- await frontendUtils.emptyCart();
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.editBillingDetails();
-
- // Now check all the fields previously filled are still filled on a fresh checkout.
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel(
- 'Enter a gift message to include in the package'
- )
- ).toHaveValue( 'This is for you!' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel(
- 'Is this a personal purchase or a business purchase?'
- )
- ).toHaveValue( 'business' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- ).toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Government ID', { exact: true } )
- ).toHaveValue( '12345' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Confirm Government ID' )
- ).toHaveValue( '12345' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- ).toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road? (optional)' )
- ).toHaveValue( 'wide' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Government ID', { exact: true } )
- ).toHaveValue( '54321' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Confirm Government ID' )
- ).toHaveValue( '54321' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- ).not.toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road? (optional)' )
- ).toHaveValue( 'narrow' );
- } );
-
- test( 'Shopper can change the values of fields multiple times and place the order', async ( {
- checkoutPageObject,
- frontendUtils,
- } ) => {
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you!',
- 'Is this a personal purchase or a business purchase?':
- 'business',
- },
- address: {
- shipping: {
- 'Government ID': '12345',
- 'Confirm government ID': '12345',
- 'How wide is your road? (optional)': 'wide',
- },
- billing: {
- 'Government ID': '54321',
- 'Confirm government ID': '54321',
- 'How wide is your road? (optional)': 'narrow',
- },
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- // Change the shipping and billing select fields again.
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- address: {
- shipping: {
- 'How wide is your road? (optional)': 'super-wide',
- },
- billing: {
- 'How wide is your road? (optional)': 'wide',
- },
- },
- }
- );
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .check();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .check();
-
- // Check both "Can a truck fit down your road?" checkboxes (one in shipping, one in billing).
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
- // Check this one here, but don't uncheck it later.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .uncheck();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .uncheck();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .uncheck();
-
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you, from me!',
- 'Is this a personal purchase or a business purchase?':
- 'personal',
- },
- address: {
- shipping: {
- 'Government ID': '98765',
- 'Confirm government ID': '98765',
- },
- billing: {
- 'Government ID': '43210',
- 'Confirm government ID': '43210',
- },
- },
- order: {
- 'What is your favourite colour?': 'Red',
- 'How did you hear about us?':
- 'Select a how did you hear about us? (optional)',
- },
- }
- );
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .check();
- await checkoutPageObject.placeOrder();
-
- expect(
- await checkoutPageObject.verifyAdditionalFieldsDetails( [
- [ 'Government ID', '98765' ],
- [ 'Government ID', '43210' ],
- [ 'What is your favourite colour?', 'Red' ],
- [ 'Would you like a free gift with your order?', 'No' ],
- // One checkbox is checked, the other is unchecked.
- [ 'Can a truck fit down your road?', 'No' ],
- [ 'Can a truck fit down your road?', 'Yes' ],
- // Different values in different address types.
- [ 'How wide is your road?', 'Wide' ],
- [ 'How wide is your road?', 'Super wide' ],
- [
- 'Enter a gift message to include in the package',
- 'This is for you, from me!',
- ],
- [ 'Do you want to subscribe to our newsletter?', 'No' ],
- ] )
- ).toBe( true );
-
- // This optional select field was unset, so it should not be visible on the confirmation. Can't check this
- // with the above function so we will check it "manually".
- await expect(
- checkoutPageObject.page.getByText(
- 'How did you hear about us?'
- )
- ).toBeHidden();
-
- // Checking that one of the boxes is checked, and one is unchecked
- await expect(
- checkoutPageObject.page.getByText(
- 'Can a truck fit down your road?No'
- )
- ).toBeVisible();
- await expect(
- checkoutPageObject.page.getByText(
- 'Can a truck fit down your road?Yes'
- )
- ).toBeVisible();
- } );
-
- test( 'Shopper can input unsanitized values that become sanitized after checkout', async ( {
- checkoutPageObject,
- frontendUtils,
- } ) => {
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.unsyncBillingWithShipping();
- await checkoutPageObject.editBillingDetails();
- await checkoutPageObject.fillInCheckoutWithTestData(
- {},
- {
- contact: {
- 'Alternative Email': 'test@test.com',
- 'Enter a gift message to include in the package':
- 'This is for you!',
- 'Is this a personal purchase or a business purchase?':
- 'business',
- },
- address: {
- shipping: {
- 'Government ID': ' 1. 2 3 4 5 ',
- 'Confirm government ID': '1 2345',
- 'How wide is your road? (optional)': 'wide',
- },
- billing: {
- 'Government ID': ' 5. 4 3 2 1 ',
- 'Confirm government ID': '543 21',
- 'How wide is your road? (optional)': 'narrow',
- },
- },
- order: {
- 'How did you hear about us?': 'Other',
- 'What is your favourite colour?': 'Blue',
- },
- }
- );
-
- await checkoutPageObject.page
- .getByLabel( 'Would you like a free gift with your order?' )
- .check();
- await checkoutPageObject.page
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- .check();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .check();
-
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- .uncheck();
-
- await checkoutPageObject.page
- .getByLabel( 'Test required checkbox' )
- .check();
-
- await checkoutPageObject.placeOrder();
-
- expect(
- await checkoutPageObject.verifyAdditionalFieldsDetails( [
- [ 'Government ID', '12345' ],
- [ 'Government ID', '54321' ],
- [ 'What is your favourite colour?', 'Blue' ],
- [
- 'Enter a gift message to include in the package',
- 'This is for you!',
- ],
- [ 'Do you want to subscribe to our newsletter?', 'Yes' ],
- [ 'Would you like a free gift with your order?', 'Yes' ],
- [ 'Can a truck fit down your road?', 'Yes' ],
- [ 'Can a truck fit down your road?', 'No' ],
- [ 'How wide is your road?', 'Wide' ],
- [ 'How wide is your road?', 'Narrow' ],
- [
- 'Is this a personal purchase or a business purchase?',
- 'business',
- ],
- ] )
- ).toBe( true );
-
- await frontendUtils.emptyCart();
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( REGULAR_PRICED_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
-
- await checkoutPageObject.editShippingDetails();
- await checkoutPageObject.editBillingDetails();
-
- // Now check all the fields previously filled are still filled on a fresh checkout.
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel(
- 'Enter a gift message to include in the package'
- )
- ).toHaveValue( 'This is for you!' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel(
- 'Is this a personal purchase or a business purchase?'
- )
- ).toHaveValue( 'business' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Contact information',
- } )
- .getByLabel( 'Do you want to subscribe to our newsletter?' )
- ).toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Government ID', { exact: true } )
- ).toHaveValue( '12345' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Confirm Government ID' )
- ).toHaveValue( '12345' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- ).toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road? (optional)' )
- ).toHaveValue( 'wide' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Government ID', { exact: true } )
- ).toHaveValue( '54321' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Confirm Government ID' )
- ).toHaveValue( '54321' );
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'Can a truck fit down your road?' )
- ).not.toBeChecked();
- await expect(
- checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road? (optional)' )
- ).toHaveValue( 'narrow' );
- } );
-
test( 'Shopper can see server-side validation errors', async ( {
checkoutPageObject,
frontendUtils,
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsAdminTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsAdminTest.php
new file mode 100644
index 00000000000..64b324b5856
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsAdminTest.php
@@ -0,0 +1,310 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Domain\Services;
+
+use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
+use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
+use Automattic\WooCommerce\Blocks\Package;
+use WC_Order;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the CheckoutFieldsAdmin class.
+ */
+class CheckoutFieldsAdminTest extends WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var CheckoutFieldsAdmin
+ */
+ private $sut;
+
+ /**
+ * Checkout fields controller.
+ *
+ * @var CheckoutFields
+ */
+ private $controller;
+
+ /**
+ * Field IDs registered by the test.
+ *
+ * @var string[]
+ */
+ private $registered_fields = array();
+
+ /**
+ * Whether this test registered the admin hooks.
+ *
+ * @var bool
+ */
+ private $registered_hooks = false;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->sut = Package::container()->get( CheckoutFieldsAdmin::class );
+ $this->controller = Package::container()->get( CheckoutFields::class );
+
+ if ( false === has_filter( 'woocommerce_admin_billing_fields', array( $this->sut, 'admin_address_fields' ) ) ) {
+ $this->sut->init();
+ $this->registered_hooks = true;
+ }
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ foreach ( $this->registered_fields as $field_id ) {
+ __internal_woocommerce_blocks_deregister_checkout_field( $field_id );
+ }
+
+ if ( $this->registered_hooks ) {
+ remove_filter( 'woocommerce_admin_billing_fields', array( $this->sut, 'admin_address_fields' ), 10 );
+ remove_filter( 'woocommerce_admin_billing_fields', array( $this->sut, 'admin_contact_fields' ), 10 );
+ remove_filter( 'woocommerce_admin_shipping_fields', array( $this->sut, 'admin_address_fields' ), 10 );
+ remove_filter( 'woocommerce_admin_shipping_fields', array( $this->sut, 'admin_order_fields' ), 10 );
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox Should inject formatted fields and persist admin updates in the correct groups.
+ */
+ public function test_injects_and_updates_additional_fields_for_each_admin_group(): void {
+ $address_field = 'test-namespace/delivery-note';
+ $contact_field = 'test-namespace/contact-method';
+ $order_field = 'test-namespace/gift-wrap';
+
+ $this->register_checkout_field(
+ array(
+ 'id' => $address_field,
+ 'label' => 'Delivery note',
+ 'location' => 'address',
+ 'type' => 'text',
+ )
+ );
+ $this->register_checkout_field(
+ array(
+ 'id' => $contact_field,
+ 'label' => 'Preferred contact method',
+ 'location' => 'contact',
+ 'type' => 'select',
+ 'options' => array(
+ array(
+ 'label' => 'Email',
+ 'value' => 'email',
+ ),
+ array(
+ 'label' => 'Phone',
+ 'value' => 'phone',
+ ),
+ ),
+ )
+ );
+ $this->register_checkout_field(
+ array(
+ 'id' => $order_field,
+ 'label' => 'Add gift wrap',
+ 'location' => 'order',
+ 'type' => 'checkbox',
+ )
+ );
+
+ $order = \WC_Helper_Order::create_order();
+ $order->set_created_via( 'store-api' );
+ $this->controller->persist_field_for_order( $address_field, 'Reception', $order, 'billing', false );
+ $this->controller->persist_field_for_order( $address_field, 'Side door', $order, 'shipping', false );
+ $this->controller->persist_field_for_order( $contact_field, 'email', $order, 'other', false );
+ $this->controller->persist_field_for_order( $order_field, true, $order, 'other', false );
+ $order->save();
+
+ $base_fields = array( 'state' => array( 'label' => 'State' ) );
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Firing an existing admin filter to exercise its callbacks, not declaring a new hook.
+ $billing_fields = apply_filters( 'woocommerce_admin_billing_fields', $base_fields, $order, 'edit' );
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Firing an existing admin filter to exercise its callbacks, not declaring a new hook.
+ $shipping_fields = apply_filters( 'woocommerce_admin_shipping_fields', $base_fields, $order, 'edit' );
+ $billing_address_id = '_wc_billing/' . $address_field;
+ $shipping_address_id = '_wc_shipping/' . $address_field;
+ $contact_admin_id = '_wc_other/' . $contact_field;
+ $order_admin_id = '_wc_other/' . $order_field;
+ $billing_address = $this->find_field_by_id( $billing_fields, $billing_address_id );
+ $shipping_address = $this->find_field_by_id( $shipping_fields, $shipping_address_id );
+
+ $this->assertSame( $base_fields['state'], $billing_fields['state'] ?? null, 'The existing billing state field should remain unchanged.' );
+ $this->assertSame( $base_fields['state'], $shipping_fields['state'] ?? null, 'The existing shipping state field should remain unchanged.' );
+ $this->assert_field_follows_state( $billing_fields, $billing_address_id, 'billing' );
+ $this->assert_field_follows_state( $shipping_fields, $shipping_address_id, 'shipping' );
+ $this->assertSame( $contact_field, array_key_last( $billing_fields ), 'Contact fields should be appended to the billing field list.' );
+ $this->assertSame( $order_field, array_key_last( $shipping_fields ), 'Order fields should be appended to the shipping field list.' );
+ $this->assertSame( 1, $this->count_fields_by_id( $billing_fields, $billing_address_id ), 'The billing address field should be injected exactly once.' );
+ $this->assertSame( 1, $this->count_fields_by_id( $billing_fields, $contact_admin_id ), 'The contact field should be injected exactly once in billing.' );
+ $this->assertSame( 0, $this->count_fields_by_id( $billing_fields, $shipping_address_id ), 'The shipping address field should not be injected in billing.' );
+ $this->assertSame( 0, $this->count_fields_by_id( $billing_fields, $order_admin_id ), 'The order field should not be injected in billing.' );
+ $this->assertSame( 1, $this->count_fields_by_id( $shipping_fields, $shipping_address_id ), 'The shipping address field should be injected exactly once.' );
+ $this->assertSame( 1, $this->count_fields_by_id( $shipping_fields, $order_admin_id ), 'The order field should be injected exactly once in shipping.' );
+ $this->assertSame( 0, $this->count_fields_by_id( $shipping_fields, $billing_address_id ), 'The billing address field should not be injected in shipping.' );
+ $this->assertSame( 0, $this->count_fields_by_id( $shipping_fields, $contact_admin_id ), 'The contact field should not be injected in shipping.' );
+
+ $this->assert_field_properties(
+ array(
+ 'id' => $billing_address_id,
+ 'label' => 'Delivery note',
+ 'value' => 'Reception',
+ 'type' => 'text',
+ 'update_callback' => array( $this->sut, 'update_callback' ),
+ 'show' => true,
+ 'wrapper_class' => 'form-field-wide',
+ ),
+ $billing_address,
+ 'Billing address fields should be injected with their current value and billing-prefixed ID.'
+ );
+ $this->assert_field_properties(
+ array(
+ 'id' => $shipping_address_id,
+ 'label' => 'Delivery note',
+ 'value' => 'Side door',
+ 'type' => 'text',
+ 'update_callback' => array( $this->sut, 'update_callback' ),
+ 'show' => true,
+ 'wrapper_class' => 'form-field-wide',
+ ),
+ $shipping_address,
+ 'Shipping address fields should be injected with their current value and shipping-prefixed ID.'
+ );
+ $this->assert_field_properties(
+ array(
+ 'id' => $contact_admin_id,
+ 'label' => 'Preferred contact method',
+ 'value' => 'email',
+ 'type' => 'select',
+ 'update_callback' => array( $this->sut, 'update_callback' ),
+ 'show' => true,
+ 'wrapper_class' => 'form-field-wide',
+ 'options' => array(
+ 'email' => 'Email',
+ 'phone' => 'Phone',
+ ),
+ ),
+ $billing_fields[ $contact_field ],
+ 'Contact select fields should expose their labels, options, current value, and update callback.'
+ );
+ $this->assert_field_properties(
+ array(
+ 'id' => $order_admin_id,
+ 'label' => 'Add gift wrap',
+ 'value' => true,
+ 'type' => 'checkbox',
+ 'update_callback' => array( $this->sut, 'update_callback' ),
+ 'show' => true,
+ 'wrapper_class' => 'form-field-wide',
+ 'checked_value' => '1',
+ 'unchecked_value' => '0',
+ ),
+ $shipping_fields[ $order_field ],
+ 'Order checkbox fields should expose their checked and unchecked representations.'
+ );
+
+ call_user_func( $billing_address['update_callback'], $billing_address['id'], 'Warehouse', $order );
+ call_user_func( $shipping_address['update_callback'], $shipping_address['id'], 'Loading bay', $order );
+ call_user_func( $billing_fields[ $contact_field ]['update_callback'], $billing_fields[ $contact_field ]['id'], 'phone', $order );
+ call_user_func( $shipping_fields[ $order_field ]['update_callback'], $shipping_fields[ $order_field ]['id'], '0', $order );
+ $order->save();
+
+ $reloaded_order = wc_get_order( $order->get_id() );
+ $this->assertInstanceOf( WC_Order::class, $reloaded_order, 'The updated order should reload from storage.' );
+ if ( ! $reloaded_order instanceof WC_Order ) {
+ throw new \RuntimeException( 'The updated order could not be reloaded from storage.' );
+ }
+ $this->assertSame( 'Warehouse', $this->controller->get_field_from_object( $address_field, $reloaded_order, 'billing' ), 'Billing updates should remain in the billing group.' );
+ $this->assertSame( 'Loading bay', $this->controller->get_field_from_object( $address_field, $reloaded_order, 'shipping' ), 'Shipping updates should remain in the shipping group.' );
+ $this->assertSame( 'phone', $this->controller->get_field_from_object( $contact_field, $reloaded_order, 'other' ), 'Contact updates should remain in the other group.' );
+ $this->assertFalse( $this->controller->get_field_from_object( $order_field, $reloaded_order, 'other' ), 'Unchecked order fields should reload as false from the other group.' );
+ }
+
+ /**
+ * Register a checkout field and track it for cleanup.
+ *
+ * @param array $field Field registration arguments.
+ */
+ private function register_checkout_field( array $field ): void {
+ woocommerce_register_additional_checkout_field( $field );
+ $this->registered_fields[] = $field['id'];
+ }
+
+ /**
+ * Assert selected field properties without rejecting compatible additions.
+ *
+ * @param array $expected Expected field properties.
+ * @param array $actual Actual field properties.
+ * @param string $message Assertion context.
+ */
+ private function assert_field_properties( array $expected, array $actual, string $message ): void {
+ foreach ( $expected as $property => $value ) {
+ $this->assertArrayHasKey( $property, $actual, sprintf( '%s Missing property: %s.', $message, $property ) );
+ $this->assertSame( $value, $actual[ $property ], sprintf( '%s Unexpected property: %s.', $message, $property ) );
+ }
+ }
+
+ /**
+ * Assert that an injected address field immediately follows the state field.
+ *
+ * @param array $fields Admin field definitions.
+ * @param string $field_id Generated address field ID.
+ * @param string $group Address group name.
+ */
+ private function assert_field_follows_state( array $fields, string $field_id, string $group ): void {
+ $field_keys = array_keys( $fields );
+ $field_values = array_values( $fields );
+ $state_index = array_search( 'state', $field_keys, true );
+
+ $this->assertNotFalse( $state_index, sprintf( 'The %s state field should remain in the field list.', $group ) );
+ $this->assertSame(
+ $field_id,
+ $field_values[ false === $state_index ? 0 : $state_index + 1 ]['id'] ?? null,
+ sprintf( 'The %s address field should be injected immediately after state.', $group )
+ );
+ }
+
+ /**
+ * Count formatted admin fields with a generated ID.
+ *
+ * @param array $fields Admin field definitions.
+ * @param string $field_id Generated field ID.
+ */
+ private function count_fields_by_id( array $fields, string $field_id ): int {
+ return count(
+ array_filter(
+ $fields,
+ static function ( array $field ) use ( $field_id ): bool {
+ return ( $field['id'] ?? '' ) === $field_id;
+ }
+ )
+ );
+ }
+
+ /**
+ * Find a formatted admin field by its generated ID.
+ *
+ * @param array $fields Admin field definitions.
+ * @param string $field_id Generated field ID.
+ * @return array
+ */
+ private function find_field_by_id( array $fields, string $field_id ): array {
+ foreach ( $fields as $field ) {
+ if ( ( $field['id'] ?? '' ) === $field_id ) {
+ return $field;
+ }
+ }
+
+ $this->fail( sprintf( 'Expected to find admin field with ID %s.', $field_id ) );
+ return array();
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
index 1882ab8934a..55994100052 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
@@ -2400,6 +2400,98 @@ class AdditionalFields extends \WP_Test_REST_TestCase {
$this->assertEquals( 'shipping-saved-gov-id', ( (array) $data['shipping_address'] )['plugin-namespace/gov-id'], print_r( $data, true ) );
}
+ /**
+ * Ensures an additional field is sanitized before it is validated, and the sanitized value is what gets stored.
+ */
+ public function test_additional_field_is_sanitized_before_it_is_validated() {
+ $id = 'plugin-namespace/upper-code';
+
+ // Uppercasing is the point: no generic sanitizer in the request pipeline does it,
+ // so a lowercase value can only reach a passing validation through this field's
+ // own `sanitize_callback`. Trimming would not work here, because the address
+ // sanitizers already trim whatever the field callback is given.
+ \woocommerce_register_additional_checkout_field(
+ array(
+ 'id' => $id,
+ 'label' => 'Upper code',
+ 'location' => 'order',
+ 'type' => 'text',
+ 'required' => true,
+ 'sanitize_callback' => function ( $value ) {
+ return strtoupper( $value );
+ },
+ 'validate_callback' => function ( $value ) {
+ return ctype_upper( $value );
+ },
+ )
+ );
+
+ // Still invalid after uppercasing, so a rejection here is the field's rule doing
+ // its job rather than the ordering.
+ $response = $this->checkout_with_upper_code( $id, 'ab1' );
+ $data = $response->get_data();
+
+ $this->assertEquals( 400, $response->get_status(), print_r( $data, true ) );
+
+ // Invalid as submitted and valid once uppercased.
+ $response = $this->checkout_with_upper_code( $id, 'abc' );
+ $data = $response->get_data();
+
+ $this->assertEquals( 200, $response->get_status(), print_r( $data, true ) );
+ $this->assertEquals( 'ABC', ( (array) $data['additional_fields'] )[ $id ], print_r( $data, true ) );
+ }
+
+ /**
+ * Dispatch a checkout request carrying one value for a registered order-location field.
+ *
+ * @param string $id Field ID.
+ * @param string $value Value to submit.
+ * @return \WP_REST_Response
+ */
+ private function checkout_with_upper_code( string $id, string $value ) {
+ $request = new \WP_REST_Request( 'POST', '/wc/store/v1/checkout' );
+ $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
+ $request->set_body_params(
+ array(
+ 'billing_address' => (object) array(
+ 'first_name' => 'test',
+ 'last_name' => 'test',
+ 'company' => '',
+ 'address_1' => 'test',
+ 'address_2' => '',
+ 'city' => 'test',
+ 'state' => '',
+ 'postcode' => 'cb241ab',
+ 'country' => 'GB',
+ 'phone' => '',
+ 'email' => 'testaccount@test.com',
+ 'plugin-namespace/gov-id' => 'gov id',
+ ),
+ 'shipping_address' => (object) array(
+ 'first_name' => 'test',
+ 'last_name' => 'test',
+ 'company' => '',
+ 'address_1' => 'test',
+ 'address_2' => '',
+ 'city' => 'test',
+ 'state' => '',
+ 'postcode' => 'cb241ab',
+ 'country' => 'GB',
+ 'phone' => '',
+ 'plugin-namespace/gov-id' => 'gov id',
+ ),
+ 'payment_method' => WC_Gateway_BACS::ID,
+ 'additional_fields' => array(
+ 'plugin-namespace/job-function' => 'engineering',
+ 'plugin-namespace/leave-on-porch' => true,
+ $id => $value,
+ ),
+ )
+ );
+
+ return rest_get_server()->dispatch( $request );
+ }
+
/**
* Ensures that saved values are returned in the checkout response.
*/