Commit a5c5785af67 for woocommerce
commit a5c5785af67a1ebb1d5f9ed44984c72367fb93e2
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Wed Oct 14 15:59:47 2026 +0300
[tests] Cover multi-package rate hiding and collection on the block checkout (#69648)
* [tests] Cover multi-package rate hiding and collection on the block checkout
Split out from #69207. Adds ShippingController tests for package-level hiding
mechanics and collection availability, plus a cart partial-address test; no production change.
* Add changefile(s) from automation for the following project(s): woocommerce
* Update plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
Co-authored-by: Raluca Stan <ralucastn@gmail.com>
* Remove trailing whitespace in ShippingControllerTest docblock
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Raluca Stan <ralucastn@gmail.com>
diff --git a/plugins/woocommerce/changelog/69648-wooplug-7841-shipping-tests-mechanics-collection b/plugins/woocommerce/changelog/69648-wooplug-7841-shipping-tests-mechanics-collection
new file mode 100644
index 00000000000..b6e643b453d
--- /dev/null
+++ b/plugins/woocommerce/changelog/69648-wooplug-7841-shipping-tests-mechanics-collection
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Add ShippingController tests for multi-package rate hiding and collection, plus a cart partial-address test; no production change.
+
+
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index b2e16a262ed..b9c07477923 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -1395,6 +1395,50 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
$this->assertFalse( WC()->cart->show_shipping(), "The classic checkout only treats hidden => true as hidden, so the {$field} stays required." );
}
+ /**
+ * The classic cart asks for the fields the shopper's own country requires, and GB asks for no
+ * state. Away from it the question is `has_full_shipping_address()`, which also wants a town,
+ * so the same half-typed address reads differently on the two sides.
+ *
+ * @testdox show_shipping() away from the classic cart wants the whole address, not the fields the country requires.
+ */
+ public function test_show_shipping_away_from_the_classic_cart_wants_the_whole_address(): void {
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+
+ $customer = WC()->cart->get_customer();
+ $this->original_shipping_address = array(
+ 'shipping_country' => $customer->get_shipping_country(),
+ 'shipping_state' => $customer->get_shipping_state(),
+ 'shipping_city' => $customer->get_shipping_city(),
+ 'shipping_postcode' => $customer->get_shipping_postcode(),
+ );
+ $customer->set_shipping_country( 'GB' );
+ $customer->set_shipping_state( '' );
+ $customer->set_shipping_city( '' );
+ $customer->set_shipping_postcode( 'PR1 4SS' );
+
+ $this->assertFalse(
+ $customer->has_full_shipping_address(),
+ 'The fixture turns on GB requiring a town, so this should be an incomplete address.'
+ );
+
+ WC()->cart->cart_context = 'shortcode';
+
+ $this->assertTrue(
+ WC()->cart->show_shipping(),
+ 'Field by field this is enough for GB, which requires no state and does not ask about the town.'
+ );
+
+ WC()->cart->cart_context = 'store-api';
+
+ $this->assertFalse(
+ WC()->cart->show_shipping(),
+ 'Asked as a whole, the same address is still missing a town.'
+ );
+ }
+
/**
* Hide a US address field in the country locale while leaving it required.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
index 294fade93be..b89e67e7469 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Shipping/ShippingControllerTest.php
@@ -685,4 +685,208 @@ class ShippingControllerTest extends \WC_Unit_Test_Case {
),
);
}
+
+
+ /**
+ * The classic cart hides the whole shipping section through `WC_Cart::show_shipping()` instead,
+ * because there pickup is one of the shipping methods rather than a separate choice.
+ *
+ * @testdox The classic cart is left alone, whatever the setting says.
+ */
+ public function test_the_classic_cart_is_left_alone(): void {
+ WC()->cart->cart_context = 'shortcode';
+ 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', 'pickup_location:0' ) ) )
+ );
+
+ $this->assertSame(
+ array( 'flat_rate:1', 'pickup_location:0' ),
+ array_keys( $packages[0]['rates'] ),
+ 'The classic cart decides this for itself and should not be filtered here.'
+ );
+ }
+
+ /**
+ * @testdox Every package is hidden, not just the first one.
+ */
+ public function test_every_package_is_hidden_not_just_the_first(): 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', 'pickup_location:0' ) ),
+ $this->package_offering( array( 'flat_rate:2', 'pickup_location:0' ) ),
+ )
+ );
+
+ $this->assertCount( 2, $packages, 'Both packages should still be there, with less in them.' );
+ $this->assertSame( array( 'pickup_location:0' ), array_keys( $packages[0]['rates'] ), 'The first package should be stripped.' );
+ $this->assertSame( array( 'pickup_location:0' ), array_keys( $packages[1]['rates'] ), 'And so should the second.' );
+ }
+
+ /**
+ * `WC()->customer` starts out null and is only built once a session exists, so this filter can
+ * run with no customer to ask. With nothing known about where the shopper is, there is no
+ * address, and the setting says to wait for one.
+ *
+ * @testdox With no customer to ask, delivery is hidden rather than shown.
+ */
+ public function test_delivery_is_hidden_when_there_is_no_customer_to_ask(): void {
+ $this->shopper_is_on_the_block_checkout();
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+ $customer = WC()->customer;
+ WC()->customer = null;
+
+ try {
+ $packages = $this->shipping_controller->remove_shipping_if_no_address(
+ array( $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ) )
+ );
+ } finally {
+ WC()->customer = $customer;
+ }
+
+ $this->assertSame(
+ array( 'pickup_location:0' ),
+ array_keys( $packages[0]['rates'] ),
+ 'Without a customer there is no address to have entered, so delivery should stay hidden.'
+ );
+ }
+
+ /**
+ * This filter checks `instanceof WC_Shipping_Rate` before reading the rate, so an entry an
+ * extension added that is not a rate is dropped.
+ * On a real request this guard is never reached: `filter_shipping_packages()`
+ * runs first on the same hook and fatals on the same entry. It only protects direct calls.
+ *
+ * @testdox A non-rate entry in the rates array does not survive the hiding.
+ */
+ public function test_only_real_rates_survive_the_hiding(): void {
+ $this->shopper_is_on_the_block_checkout();
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+ $package = $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) );
+ $package['rates']['pickup_location:9'] = 'not a rate at all';
+
+ $packages = $this->shipping_controller->remove_shipping_if_no_address( array( $package ) );
+
+ $this->assertSame(
+ array( 'pickup_location:0' ),
+ array_keys( $packages[0]['rates'] ),
+ 'Something that only looks like a pickup rate by its key should not be kept.'
+ );
+ }
+
+ /**
+ * The whole order is collected in one go, so it only makes sense to offer collection when every
+ * package can be collected. That is not something the settings screen says; it follows from an
+ * order having a single tax location, which the method's own docblock gives as the reason.
+ *
+ * @testdox Collection stays on offer while every package can be collected.
+ */
+ public function test_collection_stays_while_every_package_can_be_collected(): void {
+ $this->shopper_is_on_the_block_checkout();
+
+ $packages = $this->shipping_controller->filter_shipping_packages(
+ array(
+ $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ),
+ $this->package_offering( array( 'flat_rate:2', 'pickup_location:0' ) ),
+ )
+ );
+
+ $this->assertSame( array( 'flat_rate:1', 'pickup_location:0' ), array_keys( $packages[0]['rates'] ), 'Nothing should be withdrawn from the first package.' );
+ $this->assertSame( array( 'flat_rate:2', 'pickup_location:0' ), array_keys( $packages[1]['rates'] ), 'Nor from the second.' );
+ }
+
+ /**
+ * @testdox When one package cannot be collected, collection disappears from the whole order.
+ */
+ public function test_one_uncollectable_package_withdraws_collection_from_the_order(): void {
+ $this->shopper_is_on_the_block_checkout();
+
+ $packages = $this->shipping_controller->filter_shipping_packages(
+ array(
+ $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ),
+ $this->package_offering( array( 'flat_rate:2' ) ),
+ )
+ );
+
+ $this->assertSame( array( 'flat_rate:1' ), array_keys( $packages[0]['rates'] ), 'The collectable package should lose collection too, or the order could not be collected in one go.' );
+ $this->assertSame( array( 'flat_rate:2' ), array_keys( $packages[1]['rates'] ), 'The uncollectable package is left as it was.' );
+ }
+
+ /**
+ * A package can come back with nothing on offer, for example when nothing in it can be shipped
+ * to the address given.
+ *
+ * @testdox A package offering nothing at all counts as one that cannot be collected.
+ */
+ public function test_a_package_with_no_rates_counts_as_uncollectable(): void {
+ $this->shopper_is_on_the_block_checkout();
+
+ $packages = $this->shipping_controller->filter_shipping_packages(
+ array(
+ $this->package_offering( array( 'pickup_location:0' ) ),
+ $this->package_offering( array() ),
+ )
+ );
+
+ $this->assertSame( array(), array_keys( $packages[0]['rates'] ), 'A package with no options at all cannot be collected, so collection goes from the order.' );
+ $this->assertSame( array(), array_keys( $packages[1]['rates'] ), 'And the empty package is still an empty package.' );
+ }
+
+ /**
+ * What the shopper ends up with when both rules apply at once, through the filter the two are
+ * really attached to rather than by calling them in a chosen order.
+ *
+ * @testdox With costs hidden, no address and a package that cannot be collected, nothing is offered.
+ */
+ public function test_an_uncollectable_package_leaves_an_addressless_shopper_with_nothing(): void {
+ $this->shopper_is_on_the_block_checkout();
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+
+ $packages = apply_filters(
+ 'woocommerce_shipping_packages',
+ array(
+ $this->package_offering( array( 'flat_rate:1', 'pickup_location:0' ) ),
+ $this->package_offering( array( 'flat_rate:2' ) ),
+ )
+ );
+
+ $this->assertSame( array(), array_keys( $packages[0]['rates'] ), 'Collection went because the order cannot be collected, and delivery went because there is no address.' );
+ $this->assertSame( array(), array_keys( $packages[1]['rates'] ), 'The same is true of the package that was never collectable.' );
+ }
+
+ /**
+ * Unlike the sibling filter above it, this one asks no question about where the shopper is, so
+ * the classic cart loses collection in the same way. The reason holds there too: the classic
+ * cart also taxes a collected order at one place, the shop base. The collection method here is
+ * `local_pickup` rather than `pickup_location`, because that is what a classic cart offers.
+ *
+ * @testdox On the classic cart too, one uncollectable package withdraws collection.
+ */
+ public function test_the_classic_cart_also_loses_collection_for_the_whole_order(): void {
+ WC()->cart->cart_context = 'shortcode';
+
+ $packages = $this->shipping_controller->filter_shipping_packages(
+ array(
+ $this->package_offering( array( 'flat_rate:1', 'local_pickup:3' ) ),
+ $this->package_offering( array( 'flat_rate:2' ) ),
+ )
+ );
+
+ $this->assertSame(
+ array( 'flat_rate:1' ),
+ array_keys( $packages[0]['rates'] ),
+ 'Collection should go from the classic cart as well, since this filter does not ask which cart it is.'
+ );
+ $this->assertSame(
+ array( 'flat_rate:2' ),
+ array_keys( $packages[1]['rates'] ),
+ 'The package that was never collectable is left as it was.'
+ );
+ }
}