Commit 9b6267f38c9 for woocommerce

commit 9b6267f38c9c188438123b250322c68c052ad5cf
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Oct 5 17:47:44 2026 +0300

    Fix the classic checkout State field staying hidden after a country change (#69395)

    * fix(checkout): show State again after leaving a country that hides it

    Since #63928, address-i18n.js hides the State row when the country
    locale sets hidden => true, as Cyprus, Bosnia and Herzegovina and
    Botswana do in core. It never shows the row again and leaves that to
    country-select.js, which only shows it when it swaps a dropdown or a
    hidden input for a text input.

    Switching from one of those countries to another country with no state
    list, such as Lithuania, keeps the same text input, so the row stayed
    hidden while the new locale marked it required. The classic checkout
    then refused the order with "Billing State / County is a required
    field." for a field the shopper could not see.

    Show the State row whenever the locale does not hide it, unless its
    control is the hidden input that country-select.js and the server use
    for a country with an empty state list. Core always hides State for
    those countries in the locale too, but extensions that add an empty
    list through woocommerce_states rely on that input to keep the row
    hidden.

    The e2e test switches the classic checkout from Cyprus to Lithuania
    and back. It fails on trunk at the Lithuania step.

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * chore(changelog): add entry for the hidden State field fix

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): assert the exact required class on the State row

    The State row check used toHaveClass( /validate-required/ ), a substring
    match on the whole class attribute, so any longer class containing that
    text would also satisfy it. toContainClass() compares class tokens, which
    is what the assertion means. Playwright 1.63 in this repo supports it.

    Raised in review by CodeRabbit.

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): cover an empty state list in the State field test

    The fix keeps the State row hidden when its control is the hidden input
    that country-select.js uses for an empty state list, but the test never
    reached that check: Cyprus is hidden by its locale and Lithuania has no
    state list at all. Replacing the check with an unconditional show still
    passed.

    The test now gives Andorra an empty list through the woocommerce_states
    filter, set per browser context with the e2e-filters cookie, and asserts
    the row stays hidden after switching to it. Cyprus and Lithuania have no
    core state list, so replacing every list leaves their steps unchanged.
    The always-show variant now fails at the Andorra step.

    Raised in review by CodeRabbit.

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * chore(changelog): name address forms in the hidden State field entry

    The entry said the State field comes back on the classic checkout, but
    address-i18n.js also runs on the My Account address form and the cart
    shipping calculator, and the fix applies there too. Both were tested.

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(e2e): check the State field after a country switch in My Account

    address-i18n.js and country-select.js also run on the My Account
    address form, where the State field stayed hidden in the same way after
    switching from Cyprus to a country with no state list. The billing
    address test now switches to Cyprus, then Lithuania, before it picks the
    US, and checks that State hides and then comes back as required.

    This extends the existing test rather than adding one: it adds two
    selections and no page load. The new step fails on trunk's
    address-i18n.js at the Lithuania check.

    Refs #69352

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-69352-classic-checkout-hidden-state-after-country-change b/plugins/woocommerce/changelog/fix-69352-classic-checkout-hidden-state-after-country-change
new file mode 100644
index 00000000000..9013369ca87
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-69352-classic-checkout-hidden-state-after-country-change
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show the State field again on the classic checkout and address forms after switching from a country that hides it.
diff --git a/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js b/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
index 9dea540c989..c5ee2f27d1e 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
@@ -91,11 +91,11 @@ jQuery( function( $ ) {
 					field.data( 'priority', fieldLocale.priority );
 				}

-				// Hidden fields. State visibility (show) is managed by
-				// country-select.js, but locale can still hide it.
+				// Hidden fields. country-select.js swaps the State input for a hidden
+				// one when the country has an empty state list; keep that row hidden.
 				if ( true === fieldLocale.hidden ) {
 					field.hide().find( ':input' ).val( '' );
-				} else if ( 'state' !== key ) {
+				} else if ( 'state' !== key || ! field.find( 'input[type="hidden"]' ).length ) {
 					field.show();
 				}

diff --git a/plugins/woocommerce/tests/e2e/tests/checkout/checkout-shortcode-state-field.spec.ts b/plugins/woocommerce/tests/e2e/tests/checkout/checkout-shortcode-state-field.spec.ts
new file mode 100644
index 00000000000..0887004491c
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e/tests/checkout/checkout-shortcode-state-field.spec.ts
@@ -0,0 +1,69 @@
+/**
+ * External dependencies
+ */
+import {
+	addAProductToCart,
+	WC_API_PATH,
+} from '@woocommerce/e2e-utils-playwright';
+
+/**
+ * Internal dependencies
+ */
+import { expect, test as baseTest } from '../../fixtures/fixtures';
+import { getFakeProduct } from '../../utils/data';
+import { setFilterValue } from '../../utils/filters';
+import {
+	createClassicCheckoutPage,
+	CLASSIC_CHECKOUT_PAGE,
+} from '../../utils/pages';
+
+const test = baseTest.extend( {
+	product: async ( { restApi }, use ) => {
+		let product;
+
+		await restApi
+			.post( `${ WC_API_PATH }/products`, getFakeProduct( { dec: 0 } ) )
+			.then( ( response ) => {
+				product = response.data;
+			} );
+
+		await use( product );
+
+		await restApi.delete( `${ WC_API_PATH }/products/${ product.id }`, {
+			force: true,
+		} );
+	},
+} );
+
+test( 'Shortcode checkout shows or hides the State field for the selected country', async ( {
+	page,
+	product,
+} ) => {
+	await createClassicCheckoutPage();
+	await page.context().clearCookies();
+	await addAProductToCart( page, product.id, 1 );
+	// Give Andorra an empty state list, as an extension can. The filter replaces
+	// every list, which leaves Cyprus and Lithuania as they are: neither has one.
+	await setFilterValue( page, 'woocommerce_states', { AD: [] } );
+	await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
+
+	const country = page.locator( '#billing_country' );
+	const stateRow = page.locator( '#billing_state_field' );
+
+	// Cyprus has no state list and its locale hides State.
+	await country.selectOption( 'CY' );
+	await expect( stateRow ).toBeHidden();
+
+	// Lithuania has no state list and a required State.
+	await country.selectOption( 'LT' );
+	await expect( stateRow ).toBeVisible();
+	await expect( stateRow ).toContainClass( 'validate-required' );
+
+	// A locale that hides State still hides it.
+	await country.selectOption( 'CY' );
+	await expect( stateRow ).toBeHidden();
+
+	// An empty state list keeps State hidden even though the locale shows it.
+	await country.selectOption( 'AD' );
+	await expect( stateRow ).toBeHidden();
+} );
diff --git a/plugins/woocommerce/tests/e2e/tests/my-account/my-account-addresses.spec.ts b/plugins/woocommerce/tests/e2e/tests/my-account/my-account-addresses.spec.ts
index afa20abf382..1d7a8cdb635 100644
--- a/plugins/woocommerce/tests/e2e/tests/my-account/my-account-addresses.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/my-account/my-account-addresses.spec.ts
@@ -55,6 +55,15 @@ test.describe( 'Customer can manage addresses in My Account > Addresses page', (
 			.locator( '#billing_address_1' )
 			.fill( '123 Evergreen Terrace' );
 		await page.locator( '#billing_city' ).fill( 'Frisco' );
+
+		// State comes back after leaving a country whose locale hides it.
+		const stateRow = page.locator( '#billing_state_field' );
+		await page.locator( '#billing_country' ).selectOption( 'CY' );
+		await expect( stateRow ).toBeHidden();
+		await page.locator( '#billing_country' ).selectOption( 'LT' );
+		await expect( stateRow ).toBeVisible();
+		await expect( stateRow ).toContainClass( 'validate-required' );
+
 		await page.locator( '#billing_country' ).selectOption( 'US' );
 		await page.locator( '#billing_state' ).selectOption( 'CA' );
 		await page.locator( '#billing_postcode' ).fill( '97403' );