Commit 87212404d32 for woocommerce

commit 87212404d32c03075fc4e0a991b6df0ece905f0c
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Sat Oct 10 00:26:55 2026 +0300

    [tests] Cover address- and locale-driven delivery visibility on the block checkout (#69647)

diff --git a/plugins/woocommerce/changelog/69647-wooplug-7841-shipping-tests-address-visibility b/plugins/woocommerce/changelog/69647-wooplug-7841-shipping-tests-address-visibility
new file mode 100644
index 00000000000..869fe55db86
--- /dev/null
+++ b/plugins/woocommerce/changelog/69647-wooplug-7841-shipping-tests-address-visibility
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Add ShippingController tests for address/locale-driven delivery visibility; no production change.
+
+
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
index 313af0c695f..82f34cfad4b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
@@ -10,7 +10,7 @@ use Automattic\WooCommerce\Blocks\Shipping\ShippingController;
 /**
  * Unit tests for the PatternRegistry class.
  */
-class ShippingControllerTest extends \WP_UnitTestCase {
+class ShippingControllerTest extends \WC_Unit_Test_Case {
 	/**
 	 * The registry instance.
 	 *
@@ -52,7 +52,7 @@ class ShippingControllerTest extends \WP_UnitTestCase {
 	 *
 	 * @return void
 	 */
-	protected function setUp(): void {
+	public function setUp(): void {
 		parent::setUp();

 		// Setup mock logger.
@@ -92,7 +92,7 @@ class ShippingControllerTest extends \WP_UnitTestCase {
 	 *
 	 * @return void
 	 */
-	protected function tearDown(): void {
+	public function tearDown(): void {
 		global $woocommerce;

 		update_option( 'woocommerce_checkout_page_id', $this->original_checkout_page_id );
@@ -322,4 +322,364 @@ class ShippingControllerTest extends \WP_UnitTestCase {
 	public function override_wc_logger() {
 		return $this->mock_logger;
 	}
+
+	/**
+	 * Build a shipping package the way WC_Shipping hands one to the filters.
+	 *
+	 * @param array $rate_keys Rate ids, e.g. array( 'flat_rate:1', 'pickup_location:0' ).
+	 * @return array
+	 */
+	private function package_offering( array $rate_keys ): array {
+		$rates = array();
+
+		foreach ( $rate_keys as $rate_key ) {
+			$method_id          = current( explode( ':', $rate_key ) );
+			$rates[ $rate_key ] = new \WC_Shipping_Rate( $rate_key, ucfirst( $method_id ), '10', array(), $method_id );
+		}
+
+		return array( 'rates' => $rates );
+	}
+
+	/**
+	 * Put the shopper on the block checkout. Only remove_shipping_if_no_address() asks which cart
+	 * it is; its sibling applies everywhere, so tests of that one say which cart they are on only
+	 * because the pair below varies exactly that.
+	 */
+	private function shopper_is_on_the_block_checkout(): void {
+		WC()->cart->cart_context = 'store-api';
+	}
+
+	/**
+	 * Give the customer an address complete enough to ship to.
+	 */
+	private function customer_enters_a_full_address(): void {
+		WC()->customer->set_shipping_country( 'US' );
+		WC()->customer->set_shipping_state( 'CA' );
+		WC()->customer->set_shipping_city( 'Beverly Hills' );
+		WC()->customer->set_shipping_postcode( '90210' );
+
+		$this->assertTrue(
+			WC()->customer->has_full_shipping_address(),
+			'The fixture address should count as complete, or a test expecting delivery to survive proves nothing.'
+		);
+	}
+
+	/**
+	 * The setting is off by default, and the screen only promises to hide costs once it is switched on.
+	 *
+	 * @testdox With the setting off, a shopper with no address still sees every option.
+	 *
+	 * @testWith [null]
+	 *           ["no"]
+	 *
+	 * @param string|null $stored What the option holds, null meaning it was never saved.
+	 */
+	public function test_nothing_is_hidden_while_the_setting_is_off( ?string $stored ): void {
+		$this->shopper_is_on_the_block_checkout();
+
+		if ( null === $stored ) {
+			delete_option( 'woocommerce_shipping_cost_requires_address' );
+		} else {
+			update_option( 'woocommerce_shipping_cost_requires_address', $stored );
+		}
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Nothing should be hidden while the merchant has not asked for it.'
+		);
+	}
+
+	/**
+	 * "Hide shipping costs until an address is entered." Collection needs no address, so it stays.
+	 *
+	 * @testdox With the setting on and no address, delivery is hidden and collection stays.
+	 */
+	public function test_delivery_is_hidden_until_an_address_is_entered(): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'free_shipping:2', 'local_pickup:3', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'local_pickup:3', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Only what the shopper can collect without giving an address should survive.'
+		);
+	}
+
+	/**
+	 * @testdox Completing the address brings the delivery options back.
+	 */
+	public function test_completing_the_address_brings_delivery_back(): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+		$this->customer_enters_a_full_address();
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'An address has been entered, so there is nothing left to wait for.'
+		);
+	}
+
+	/**
+	 * A half-entered address is not an address. Which fields count is the country's business, and
+	 * `has_full_shipping_address()` is what decides it.
+	 *
+	 * @testdox Delivery stays hidden while the address is only half entered.
+	 *
+	 * @dataProvider provider_incomplete_addresses
+	 *
+	 * @param array  $address What the shopper has typed so far.
+	 * @param string $why     What makes this address incomplete.
+	 */
+	public function test_delivery_stays_hidden_while_the_address_is_incomplete( array $address, string $why ): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+		WC()->customer->set_shipping_country( $address['country'] );
+		WC()->customer->set_shipping_state( $address['state'] );
+		WC()->customer->set_shipping_city( $address['city'] );
+		WC()->customer->set_shipping_postcode( $address['postcode'] );
+
+		$this->assertFalse(
+			WC()->customer->has_full_shipping_address(),
+			'The fixture address should be incomplete, or this test proves nothing: ' . $why
+		);
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Delivery should stay hidden until the address is finished: ' . $why
+		);
+	}
+
+	/**
+	 * Addresses a shopper can be part way through typing.
+	 *
+	 * @return array
+	 */
+	public function provider_incomplete_addresses(): array {
+		return array(
+			'a country and nothing else'  => array(
+				array(
+					'country'  => 'US',
+					'state'    => '',
+					'city'     => '',
+					'postcode' => '',
+				),
+				'only the country has been chosen',
+			),
+			'everything but the postcode' => array(
+				array(
+					'country'  => 'US',
+					'state'    => 'CA',
+					'city'     => 'Beverly Hills',
+					'postcode' => '',
+				),
+				'the US asks for a postcode and none has been typed',
+			),
+			'everything but the state'    => array(
+				array(
+					'country'  => 'US',
+					'state'    => '',
+					'city'     => 'Beverly Hills',
+					'postcode' => '90210',
+				),
+				'the US asks for a state and none has been chosen',
+			),
+		);
+	}
+
+	/**
+	 * A shopper can clear a field they already filled in, and what they had typed before is not a
+	 * reason to keep showing delivery.
+	 *
+	 * @testdox Clearing part of the address hides delivery again.
+	 */
+	public function test_clearing_part_of_the_address_hides_delivery_again(): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+		$this->customer_enters_a_full_address();
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Delivery should be on offer while the address is complete.'
+		);
+
+		WC()->customer->set_shipping_postcode( '' );
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+		$this->assertSame(
+			array( 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Once the postcode is taken back out, delivery should be hidden again.'
+		);
+	}
+
+	/**
+	 * What makes an address complete is the country's business. In a country that asks for neither a
+	 * postcode nor a state, the shopper has finished as soon as the city is in.
+	 *
+	 * @testdox In a country that asks for no postcode, a city is enough to bring delivery back.
+	 */
+	public function test_a_country_that_asks_for_no_postcode_needs_no_postcode(): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+		WC()->customer->set_shipping_country( 'AE' );
+		WC()->customer->set_shipping_city( 'Dubai' );
+
+		$this->assertTrue(
+			WC()->customer->has_full_shipping_address(),
+			'The UAE asks for neither a postcode nor a state, so this address should count as complete.'
+		);
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'Nothing should be held back for fields this country never asks for.'
+		);
+	}
+
+	/**
+	 * Stores change what their countries ask for through `woocommerce_get_country_locale`, and that
+	 * has to move what counts as a finished address here too.
+	 *
+	 * @testdox A field an extension made optional no longer holds delivery back.
+	 */
+	public function test_a_field_an_extension_made_optional_no_longer_holds_delivery_back(): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+		WC()->customer->set_shipping_country( 'US' );
+		WC()->customer->set_shipping_city( 'Beverly Hills' );
+		WC()->customer->set_shipping_state( 'CA' );
+
+		$this->assertFalse(
+			WC()->customer->has_full_shipping_address(),
+			'Ordinarily the US asks for a postcode, or this test would prove nothing.'
+		);
+
+		add_filter(
+			'woocommerce_get_country_locale',
+			static function ( $locale ) {
+				$locale['US']['postcode']['required'] = false;
+				return $locale;
+			}
+		);
+		// The locale is cached on first read, and the filter is attached after that read here.
+		WC()->countries->locale = array();
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'With the postcode made optional, the address is finished and delivery should show.'
+		);
+	}
+
+	/**
+	 * A locale can hide a field outright. A shopper is never shown it, so it cannot be something
+	 * they have left unfinished, whatever the same locale says about it being required.
+	 *
+	 * @testdox A field the locale hides does not hold delivery back, even when marked required.
+	 *
+	 * @dataProvider provider_locales_that_hide_the_postcode
+	 *
+	 * @param callable $hide_the_postcode Attaches the filter that hides it.
+	 * @param string   $why               Which locale entry is doing the hiding.
+	 */
+	public function test_a_field_the_locale_hides_does_not_hold_delivery_back( callable $hide_the_postcode, string $why ): void {
+		$this->shopper_is_on_the_block_checkout();
+		update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+		WC()->customer->set_shipping_country( 'US' );
+		WC()->customer->set_shipping_city( 'Beverly Hills' );
+		WC()->customer->set_shipping_state( 'CA' );
+
+		$this->assertFalse(
+			WC()->customer->has_full_shipping_address(),
+			'Ordinarily the US asks for a postcode, or this test would prove nothing.'
+		);
+
+		$hide_the_postcode();
+		// The locale is cached on first read, and the filter is attached after that read here.
+		WC()->countries->locale = array();
+
+		$packages = $this->shipping_controller->remove_shipping_if_no_address(
+			array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+		);
+
+		$this->assertSame(
+			array( 'flat_rate:1', 'pickup_location:0' ),
+			array_keys( $packages[0]['rates'] ),
+			'A field the shopper is never shown cannot be one they have left empty: ' . $why
+		);
+	}
+
+	/**
+	 * The two places a hidden flag can come from. They have separate filters, because
+	 * `WC_Countries::get_country_locale()` rebuilds its default entry after the country one.
+	 *
+	 * @return array
+	 */
+	public function provider_locales_that_hide_the_postcode(): array {
+		return array(
+			'hidden for this country'  => array(
+				static function () {
+					add_filter(
+						'woocommerce_get_country_locale',
+						static function ( $locale ) {
+							$locale['US']['postcode']['hidden']   = true;
+							$locale['US']['postcode']['required'] = true;
+							return $locale;
+						}
+					);
+				},
+				'the US locale hides it',
+			),
+			'hidden for every country' => array(
+				static function () {
+					add_filter(
+						'woocommerce_get_country_locale_default',
+						static function ( $fields ) {
+							$fields['postcode']['hidden']   = true;
+							$fields['postcode']['required'] = true;
+							return $fields;
+						}
+					);
+				},
+				'the default locale hides it',
+			),
+		);
+	}
 }