Commit 9095f377e1e for woocommerce
commit 9095f377e1e0dee54819119a7c00bed9a59d1f22
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Tue Sep 15 15:00:59 2026 +0300
[tests] Move order confirmation policy from 6 E2E tests to PHPUnit (#68593)
* test(blocks): Move order confirmation policy from 6 E2E tests to PHPUnit
The Order Confirmation block spec ran six browser titles. Most of
what they checked is decided on the server before any HTML reaches
the browser: who may see an order's details, which notice a missing
key or a missing order gets, whether the address blocks render for a
pickup or virtual order, and when downloads appear.
Add PHPUnit owners for that policy and output, built on real orders:
the shared permission gate, the billing and shipping address blocks
and their wrappers, the downloads table and its wrapper, and the
status notices. The notice tests join trunk's existing StatusTest
class. Keep three browser titles: a known shopper's details behind
the order key and the filter opt-out, the delayed account creation
form, and admin completion exposing named download links.
Consolidates the mega-branch slices:
- Slice 023: test(blocks): Move order confirmation policy below E2E
- test(blocks): Isolate downloads wrapper permission check
- refactor(e2e): simplify migrated Blocks test contracts (this spec
only)
Refs TESTOPS-234
Refs #68046
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Cover the Blocks pickup id and the processing download case
Two gaps the order-confirmation tests left open.
The four topology providers only ever used `local_pickup`, the classic
pickup id. `needs_shipping_address()` hides the address for the Blocks
`pickup_location` id too, so each provider gains a row for it.
That is not data only, as it first appeared.
`get_local_pickup_method_ids()` seeds its list with `local_pickup` and
then discovers the rest by asking the registered shipping methods which
support `local-pickup`. Core registers `pickup_location` only when the
Checkout block is the default checkout, which the test store is not, so
the new rows register it the same way core does and drop the memo
afterwards. Without that the rows fail, which is what makes them
coverage of the id rather than of the seed.
The downloads wrapper had no processing case. Processing permits
downloads only when the store grants access after payment, so the new
pair asserts both sides of that option.
Mutation confirms both. Making `supports( 'local-pickup' )` unreachable
fails the two new pickup rows and leaves the classic ones green.
Removing the processing branch of `is_download_permitted()` fails the
new wrapper case, alongside the table test that was previously its only
owner.
Refs #68593
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Stop the order confirmation tests restoring base-class state
The review on this PR flagged that the manual $_GET, $_POST and
current-user restores duplicate what the PHPUnit base class already
does. The same redundancy ran through the rest of the eight classes,
so remove it everywhere rather than only where it was pointed out.
clean_up_global_scope() empties $_GET, $_POST and $_REQUEST before
every test; tear_down() rolls back the transaction, then restores the
hooks and calls wp_set_current_user( 0 ). That covers the request
globals, the current user, the added filters, and every option and
fixture write, so none of it needed repeating here.
What stays is the state the base class does not own: the Store API
draft-order session value and the shipping-method memo on the WC()
singleton.
The four topology helpers returned an order and a product only so the
teardown could delete the product, so they now return the order alone.
Same 60 tests and 153 assertions before and after; the whole
tests/php/src/Blocks tree is green at 538 tests.
Refs TESTOPS-234
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(blocks): Bring the pickup fixture inside the try that unregisters it
All four topology tests created their order before the try block whose
finally drops the WC_Shipping memo. The helper registers PickupLocation
partway through its own body, several lines before the order is saved, so
a throw after that point skipped the finally and left pickup_location
registered for the rest of the process. The finally keys on the $topology
parameter, not on the order, so moving the call inside it needs nothing
else.
Also drop four assertions that cannot fail. Both wrapper tests assert
assertSame( $content, $rendered ) and then assert that $rendered contains
two substrings of $content -- true by construction once the first
assertion has passed. The risk is a later reader weakening the assertSame
believing the substring checks still cover the pass-through.
OrderConfirmation suite green at 60 tests. No full-suite run: this only
widens which code the existing finally protects and removes assertions,
so it cannot leak anything the previous run did not already cover.
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-order-confirmation-blocks b/plugins/woocommerce/changelog/testops-234-order-confirmation-blocks
new file mode 100644
index 00000000000..51eab3ddcf3
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-order-confirmation-blocks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Move order confirmation policy from 6 E2E tests to PHPUnit; the Order Confirmation block spec keeps 3 browser titles.
+
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/order-confirmation.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/order-confirmation.block_theme.spec.ts
index 19e19b6fc9e..81705c691c1 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/order-confirmation.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/order-confirmation.block_theme.spec.ts
@@ -42,7 +42,7 @@ test.describe( 'Shopper (logged-in) → Order Confirmation', () => {
await editor.transformIntoBlocks();
} );
- test( 'Place order', async ( {
+ test( 'Known shopper details require owner and key unless the filter opts out', async ( {
frontendUtils,
checkoutPageObject,
page,
@@ -61,103 +61,33 @@ test.describe( 'Shopper (logged-in) → Order Confirmation', () => {
await checkoutPageObject.fillInCheckoutWithTestData( TEST_ADDRESS );
await checkoutPageObject.placeOrder();
- // Confirm Order Confirmation Block sections are visible when logged in
- // order data are visible and correct
await checkoutPageObject.verifyOrderConfirmationDetails();
- // Store order received URL to use later
const orderReceivedURL = page.url();
-
- // Confirm downloads section is visible when logged in
- // Open order we created
- const orderId = checkoutPageObject.getOrderId();
- await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` );
- // Update order status to 'Processing'
- await page.locator( '#order_status' ).selectOption( 'wc-processing' );
- await page.locator( 'button.save_order' ).click();
- // Go back to order received page
- await page.goto( orderReceivedURL );
- // Confirm downloads section is visible
- const DownloadSection = page.locator(
- '[data-block-name="woocommerce/order-confirmation-downloads"]'
- );
- await expect( DownloadSection ).toBeVisible();
-
- // Access page without order key (test visibility of default message, no order details)
- const urlObj = new URL( orderReceivedURL );
- urlObj.searchParams.delete( 'key' );
- await page.goto( urlObj.toString() );
- // Confirm default message is visible
- await expect(
- page.getByText(
- 'Great news! Your order has been received, and a confirmation will be sent to your email address. Have an account with us?'
- )
- ).toBeVisible();
- // Confirm order details are not visible
- await checkoutPageObject.verifyOrderConfirmationDetails( false );
-
- // Access page without order ID or key (test visibility of default message)
- await page.goto( '/checkout/order-received' );
- // Confirm default message is visible
- await expect(
- page.getByText(
- "If you've just placed an order, give your email a quick check for the confirmation. Have an account with us?"
- )
- ).toBeVisible();
- // Confirm order details are not visible
- await checkoutPageObject.verifyOrderConfirmationDetails( false );
-
- await test.step( 'Logout the user and revisit the order received page to verify that details are displayed when woocommerce_order_received_verify_known_shoppers is disabled', async () => {
- await requestUtils.activatePlugin(
- 'woocommerce-blocks-test-order-confirmation-filters'
- );
- await page.goto( '/my-account' );
- await page
- .locator(
- 'li.woocommerce-MyAccount-navigation-link--customer-logout a'
- )
- .click();
- await expect(
- page.getByRole( 'button', { name: 'Log in' } )
- ).toBeVisible();
- await page.goto( orderReceivedURL );
- await checkoutPageObject.verifyOrderConfirmationDetails();
- } );
- } );
-} );
-
-test.describe( 'Shopper (guest) → Order Confirmation', () => {
- test.use( { storageState: guestFile } );
-
- test( 'Place order', async ( {
- frontendUtils,
- checkoutPageObject,
- page,
- } ) => {
+ await page.context().clearCookies();
await page.goto( '/my-account' );
-
await expect(
- page.getByRole( 'heading', { name: 'Login' } ),
- 'User is not logged out'
+ page.getByRole( 'button', { name: 'Log in' } )
).toBeVisible();
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
+ await page.goto( orderReceivedURL );
+ await checkoutPageObject.verifyOrderConfirmationDetails( false );
- expect(
- await checkoutPageObject.selectAndVerifyShippingOption(
- FREE_SHIPPING_NAME,
- FREE_SHIPPING_PRICE
- )
- ).toBe( true );
+ const missingKeyURL = new URL( orderReceivedURL );
+ missingKeyURL.searchParams.delete( 'key' );
+ await page.goto( missingKeyURL.toString() );
+ await checkoutPageObject.verifyOrderConfirmationDetails( false );
- await checkoutPageObject.fillInCheckoutWithTestData( TEST_ADDRESS );
- await checkoutPageObject.placeOrder();
+ const wrongKeyURL = new URL( orderReceivedURL );
+ wrongKeyURL.searchParams.set( 'key', 'wc_order_wrong' );
+ await page.goto( wrongKeyURL.toString() );
+ await checkoutPageObject.verifyOrderConfirmationDetails( false );
- await expect(
- page.getByText( 'Thank you. Your order has been received.' )
- ).toBeVisible();
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-order-confirmation-filters'
+ );
+ await page.goto( orderReceivedURL );
+ await checkoutPageObject.verifyOrderConfirmationDetails();
} );
} );
@@ -236,55 +166,6 @@ test.describe( 'Shopper (guest) → Order Confirmation → Create Account', () =
} );
} );
-test.describe( 'Shopper → Order Confirmation → Local Pickup', () => {
- test( 'Confirm shipping address section is hidden, but billing is visible', async ( {
- checkoutPageObject,
- frontendUtils,
- admin,
- } ) => {
- await admin.visitAdminPage(
- 'admin.php',
- 'page=wc-settings&tab=shipping§ion=pickup_location'
- );
- await admin.page.getByLabel( 'Enable local pickup' ).uncheck();
- await admin.page.getByLabel( 'Enable local pickup' ).check();
- await admin.page
- .getByRole( 'button', { name: 'Add pickup location' } )
- .click();
- await admin.page.getByLabel( 'Location name' ).fill( 'Testing' );
- await admin.page.getByPlaceholder( 'Address' ).fill( 'Test Address' );
- await admin.page.getByPlaceholder( 'City' ).fill( 'Test City' );
- await admin.page.getByPlaceholder( 'Postcode / ZIP' ).fill( '90210' );
- await admin.page
- .getByLabel( 'Pickup details' )
- .fill( 'Pickup method.' );
- await admin.page.getByRole( 'button', { name: 'Done' } ).click();
- await admin.page
- .getByRole( 'button', { name: 'Save changes' } )
- .click();
- await admin.page
- .getByRole( 'button', { name: 'Dismiss this notice' } )
- .click();
- await frontendUtils.emptyCart();
- await frontendUtils.goToShop();
- await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
- await frontendUtils.goToCheckout();
- await checkoutPageObject.selectDeliveryOption( 'Pickup' );
- await checkoutPageObject.fillInCheckoutWithTestData();
- await checkoutPageObject.placeOrder();
- await expect(
- checkoutPageObject.page.getByRole( 'heading', {
- name: 'Shipping address',
- } )
- ).toBeHidden();
- await expect(
- checkoutPageObject.page.getByRole( 'heading', {
- name: 'Billing address',
- } )
- ).toBeVisible();
- } );
-} );
-
test.describe( 'Shopper → Order Confirmation → Downloadable Products', () => {
let confirmationPageUrl: string;
@@ -297,22 +178,7 @@ test.describe( 'Shopper → Order Confirmation → Downloadable Products', () =>
confirmationPageUrl = checkoutPageObject.page.url();
} );
- test( 'Confirm shipping address section is hidden, but billing is visible', async ( {
- checkoutPageObject,
- } ) => {
- await expect(
- checkoutPageObject.page.getByRole( 'heading', {
- name: 'Shipping address',
- } )
- ).toBeHidden();
- await expect(
- checkoutPageObject.page.getByRole( 'heading', {
- name: 'Billing address',
- } )
- ).toBeVisible();
- } );
-
- test( 'Confirm order downloads are visible', async ( {
+ test( 'Completed downloadable order exposes named entitlement links', async ( {
checkoutPageObject,
admin,
} ) => {
@@ -343,5 +209,17 @@ test.describe( 'Shopper → Order Confirmation → Downloadable Products', () =>
name: 'Downloads',
} )
).toBeVisible();
+
+ const downloadsSection = checkoutPageObject.page.locator(
+ '[data-block-name="woocommerce/order-confirmation-downloads"]'
+ );
+ for ( const downloadName of [ 'Single 1', 'Single 2' ] ) {
+ const downloadLink = downloadsSection.getByRole( 'link', {
+ name: downloadName,
+ exact: true,
+ } );
+ await expect( downloadLink ).toBeVisible();
+ await expect( downloadLink ).toHaveAttribute( 'href', /.+/ );
+ }
} );
} );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlockTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlockTest.php
new file mode 100644
index 00000000000..4f9b282784d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/AbstractOrderConfirmationBlockTest.php
@@ -0,0 +1,180 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\AbstractOrderConfirmationBlock;
+use WC_Order;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for order-confirmation permission decisions.
+ */
+final class AbstractOrderConfirmationBlockTest extends WC_Unit_Test_Case {
+ /**
+ * The System Under Test.
+ *
+ * @var AbstractOrderConfirmationBlock
+ */
+ private $sut;
+
+ /**
+ * Set up the test proxy.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->sut = new class() extends AbstractOrderConfirmationBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function get_view_order_permissions_proxy( ?WC_Order $order ) {
+ return $this->get_view_order_permissions( $order );
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function email_verification_permitted_proxy( WC_Order $order ): bool {
+ return $this->email_verification_permitted( $order );
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ protected function render_content( $order, $permission = false, $attributes = array(), $content = '' ) {
+ return '';
+ }
+ };
+ }
+
+ /**
+ * @testdox Order details require the correct key and the appropriate owner, session, grace-period, or verified-email context.
+ * @dataProvider view_permission_cases
+ *
+ * @param string $scenario Scenario to arrange.
+ * @param string|false $expected Expected permission.
+ */
+ public function test_get_view_order_permissions( string $scenario, $expected ): void {
+ // The session is the only piece of state here the base class does not own:
+ // it clears the request globals before every test and the current user after.
+ $original_draft_order = WC()->session->get( 'store_api_draft_order' );
+ $order = null;
+
+ try {
+ $_GET = array();
+ $_POST = array();
+ wp_set_current_user( 0 );
+ WC()->session->set( 'store_api_draft_order', null );
+
+ if ( 'null order' !== $scenario ) {
+ $is_customer_order = in_array(
+ $scenario,
+ array( 'owner valid key', 'owner missing key', 'owner wrong key', 'wrong logged-in user', 'known shopper logged out', 'known shopper filter disabled' ),
+ true
+ );
+ $customer_id = $is_customer_order ? self::factory()->user->create( array( 'role' => 'customer' ) ) : 0;
+ $order = $this->create_order( $customer_id, ! in_array( $scenario, array( 'guest draft session', 'guest expired', 'guest verified email' ), true ) );
+ $_GET['key'] = $order->get_order_key();
+
+ switch ( $scenario ) {
+ case 'owner valid key':
+ wp_set_current_user( $customer_id );
+ break;
+ case 'owner missing key':
+ wp_set_current_user( $customer_id );
+ unset( $_GET['key'] );
+ break;
+ case 'owner wrong key':
+ wp_set_current_user( $customer_id );
+ $_GET['key'] = 'wc_order_wrong';
+ break;
+ case 'wrong logged-in user':
+ wp_set_current_user( self::factory()->user->create( array( 'role' => 'customer' ) ) );
+ break;
+ case 'known shopper filter disabled':
+ add_filter( 'woocommerce_order_received_verify_known_shoppers', '__return_false' );
+ break;
+ case 'guest draft session':
+ WC()->session->set( 'store_api_draft_order', $order->get_id() );
+ break;
+ case 'guest verified email':
+ $_POST['email'] = $order->get_billing_email();
+ $_POST['_wpnonce'] = wp_create_nonce( 'wc_verify_email' );
+ break;
+ }
+ }
+
+ $this->assertSame( $expected, $this->sut->get_view_order_permissions_proxy( $order ) );
+ } finally {
+ WC()->session->set( 'store_api_draft_order', $original_draft_order );
+ }
+ }
+
+ /**
+ * Named order permission cases.
+ *
+ * @return array<string, array{string, string|false}>
+ */
+ public static function view_permission_cases(): array {
+ return array(
+ 'null order' => array( 'null order', false ),
+ 'owner with valid key' => array( 'owner valid key', 'full' ),
+ 'owner without key' => array( 'owner missing key', false ),
+ 'owner with wrong key' => array( 'owner wrong key', false ),
+ 'different logged-in customer' => array( 'wrong logged-in user', false ),
+ 'known shopper logged out' => array( 'known shopper logged out', false ),
+ 'known shopper filter disabled' => array( 'known shopper filter disabled', 'full' ),
+ 'guest Store API draft session' => array( 'guest draft session', 'full' ),
+ 'guest within grace period' => array( 'guest grace period', 'full' ),
+ 'guest after grace period' => array( 'guest expired', false ),
+ 'guest with verified email' => array( 'guest verified email', 'full' ),
+ );
+ }
+
+ /**
+ * @testdox Email verification is offered only to guest orders with guest checkout enabled and a valid key.
+ * @dataProvider email_verification_permission_cases
+ *
+ * @param bool $guest_checkout_enabled Whether guest checkout is enabled.
+ * @param bool $customer_order Whether the order belongs to a customer.
+ * @param bool $valid_key Whether the request has the order key.
+ * @param bool $expected Expected result.
+ */
+ public function test_email_verification_permitted( bool $guest_checkout_enabled, bool $customer_order, bool $valid_key, bool $expected ): void {
+ $customer_id = $customer_order ? self::factory()->user->create( array( 'role' => 'customer' ) ) : 0;
+ $order = $this->create_order( $customer_id );
+
+ update_option( 'woocommerce_enable_guest_checkout', $guest_checkout_enabled ? 'yes' : 'no' );
+ $_GET = array( 'key' => $valid_key ? $order->get_order_key() : 'wc_order_wrong' );
+
+ $this->assertSame( $expected, $this->sut->email_verification_permitted_proxy( $order ) );
+ }
+
+ /**
+ * Named email-verification eligibility cases.
+ *
+ * @return array<string, array{bool, bool, bool, bool}>
+ */
+ public static function email_verification_permission_cases(): array {
+ return array(
+ 'guest checkout and valid key' => array( true, false, true, true ),
+ 'guest checkout disabled' => array( false, false, true, false ),
+ 'guest order with wrong key' => array( true, false, false, false ),
+ 'known customer order with key' => array( true, true, true, false ),
+ 'known customer without guest mode' => array( false, true, false, false ),
+ );
+ }
+
+ /**
+ * Create a persisted order for a guest or registered customer.
+ *
+ * @param int $customer_id Customer ID, or zero for a guest.
+ * @param bool $within_grace_period Whether the order is recent.
+ * @return WC_Order
+ */
+ private function create_order( int $customer_id, bool $within_grace_period = true ): WC_Order {
+ $order = wc_create_order( array( 'customer_id' => $customer_id ) );
+ $order->set_billing_email( 'shopper-' . $order->get_id() . '@example.com' );
+ if ( ! $within_grace_period ) {
+ $order->set_date_created( time() - ( 20 * MINUTE_IN_SECONDS ) );
+ }
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingAddress.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingAddress.php
index 8e5c63cc48d..f58e9290773 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingAddress.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingAddress.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\BillingAddress as BillingAddressBlock;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Blocks\Shipping\PickupLocation;
/**
* Test BillingAddress block class.
@@ -60,6 +61,50 @@ final class BillingAddress extends \WP_UnitTestCase {
$this->assertStringContainsString( 'visible value', $this->render( $order ) );
}
+ /**
+ * @testdox Billing details follow view permission but remain present for shipped, pickup, and virtual orders.
+ * @dataProvider billing_address_topology_cases
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param string|false $permission View permission.
+ * @param bool $expected_visible Whether billing content should render.
+ */
+ public function test_billing_address_topology( string $topology, $permission, bool $expected_visible ): void {
+ try {
+ // Inside the try: create_topology_order() registers PickupLocation partway
+ // through, so a throw after that point must still reach the finally below.
+ $order = $this->create_topology_order( $topology );
+ update_option( 'woocommerce_calc_shipping', 'yes' );
+ $content = $this->render( $order, $permission );
+
+ if ( $expected_visible ) {
+ $this->assertStringContainsString( 'Billing Marker', $content, 'Billing details should be non-empty for an authorized order.' );
+ } else {
+ $this->assertSame( '', $content, 'Billing details should be empty without view permission.' );
+ }
+ } finally {
+ if ( 'pickup-location' === $topology ) {
+ // Drop the memo so the next test reloads the ambient method list.
+ WC()->shipping()->unregister_shipping_methods();
+ }
+ }
+ }
+
+ /**
+ * Named billing-address topology cases.
+ *
+ * @return array<string, array{string, string|false, bool}>
+ */
+ public static function billing_address_topology_cases(): array {
+ return array(
+ 'physical order' => array( 'physical', 'full', true ),
+ 'classic local pickup order' => array( 'local-pickup', 'full', true ),
+ 'blocks pickup location order' => array( 'pickup-location', 'full', true ),
+ 'virtual order' => array( 'virtual', 'full', true ),
+ 'no view permission' => array( 'physical', false, false ),
+ );
+ }
+
/**
* Create an order placed via the Store API with a value persisted for the billing group of the test field.
*
@@ -78,10 +123,11 @@ final class BillingAddress extends \WP_UnitTestCase {
/**
* Render the billing address block content for the given order with full view permissions.
*
- * @param \WC_Order $order Order object.
+ * @param \WC_Order $order Order object.
+ * @param string|false $permission View permission.
* @return string
*/
- private function render( \WC_Order $order ): string {
+ private function render( \WC_Order $order, $permission = 'full' ): string {
$proxy = new class() extends BillingAddressBlock {
// phpcs:ignore Squiz.Commenting.FunctionComment.Missing
public function __construct() {
@@ -92,6 +138,65 @@ final class BillingAddress extends \WP_UnitTestCase {
}
};
- return $proxy->render_content_proxy( $order, 'full' );
+ return $proxy->render_content_proxy( $order, $permission );
+ }
+
+ /**
+ * Create an order with billing data and a representative fulfillment topology.
+ *
+ * @param string $topology Order fulfillment topology.
+ * @return \WC_Order
+ */
+ private function create_topology_order( string $topology ): \WC_Order {
+ $product = \WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'downloadable' => 'virtual' === $topology,
+ 'virtual' => 'virtual' === $topology,
+ )
+ );
+ $product->set_virtual( 'virtual' === $topology );
+ $product->save();
+
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new \WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+ $order->set_billing_first_name( 'Billing' );
+ $order->set_billing_last_name( 'Marker' );
+ $order->set_billing_address_1( '500 Billing Avenue' );
+ $order->set_billing_city( 'San Francisco' );
+ $order->set_billing_state( 'CA' );
+ $order->set_billing_postcode( '94105' );
+ $order->set_billing_country( 'US' );
+
+ if ( 'virtual' !== $topology ) {
+ $shipping_item = new \WC_Order_Item_Shipping();
+ // `local_pickup` is the classic pickup method, `pickup_location` the Blocks one.
+ // `needs_shipping_address()` hides the address for both, but it finds
+ // `pickup_location` only by asking the registered shipping methods which of them
+ // support `local-pickup`. Core registers that method when the Checkout block is
+ // the default checkout, which the test store is not, so register it the same way.
+ $pickup_methods = array(
+ 'local-pickup' => array( 'Local pickup', 'local_pickup' ),
+ 'pickup-location' => array( 'Pickup Location', 'pickup_location' ),
+ );
+
+ if ( 'pickup-location' === $topology ) {
+ WC()->shipping()->get_shipping_methods();
+ WC()->shipping()->register_shipping_method( new PickupLocation() );
+ }
+
+ list( $method_title, $method_id ) = $pickup_methods[ $topology ] ?? array( 'Free shipping', 'free_shipping' );
+
+ $shipping_item->set_method_title( $method_title );
+ $shipping_item->set_method_id( $method_id );
+ $order->add_item( $shipping_item );
+ }
+
+ $order->save();
+
+ return $order;
}
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingWrapperTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingWrapperTest.php
new file mode 100644
index 00000000000..a560b19d877
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/BillingWrapperTest.php
@@ -0,0 +1,146 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\BillingWrapper as BillingWrapperBlock;
+use Automattic\WooCommerce\Blocks\Shipping\PickupLocation;
+use WC_Order;
+use WC_Order_Item_Product;
+use WC_Order_Item_Shipping;
+use WC_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the order-confirmation billing wrapper.
+ */
+final class BillingWrapperTest extends WC_Unit_Test_Case {
+ /**
+ * @testdox Billing wrapper headings and content require an address and permission across fulfillment topologies.
+ * @dataProvider billing_wrapper_cases
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param string|false $permission View permission.
+ * @param bool $has_address Whether the order has a billing address.
+ * @param bool $expected_visible Whether wrapper content should render.
+ */
+ public function test_billing_wrapper_topology( string $topology, $permission, bool $has_address, bool $expected_visible ): void {
+ $content = '<h2>Billing address</h2><p>Billing wrapper marker</p>';
+
+ try {
+ // Inside the try: create_order() registers PickupLocation partway through,
+ // so a throw after that point must still reach the finally below.
+ $order = $this->create_order( $topology, $has_address );
+ $rendered = $this->render( $order, $permission, $content );
+
+ if ( $expected_visible ) {
+ $this->assertSame( $content, $rendered, 'An authorized order with billing data should preserve the wrapper heading and content.' );
+ } else {
+ $this->assertSame( '', $rendered, 'The billing wrapper should be empty without permission or an address.' );
+ }
+ } finally {
+ if ( 'pickup-location' === $topology ) {
+ // Drop the memo so the next test reloads the ambient method list.
+ WC()->shipping()->unregister_shipping_methods();
+ }
+ }
+ }
+
+ /**
+ * Named billing-wrapper cases.
+ *
+ * @return array<string, array{string, string|false, bool, bool}>
+ */
+ public static function billing_wrapper_cases(): array {
+ return array(
+ 'physical free-shipping order' => array( 'physical', 'full', true, true ),
+ 'classic local pickup order' => array( 'local-pickup', 'full', true, true ),
+ 'blocks pickup location order' => array( 'pickup-location', 'full', true, true ),
+ 'virtual downloadable order' => array( 'virtual', 'full', true, true ),
+ 'no view permission' => array( 'physical', false, true, false ),
+ 'no billing address' => array( 'physical', 'full', false, false ),
+ );
+ }
+
+ /**
+ * Render wrapper content through a public proxy.
+ *
+ * @param WC_Order $order Order object.
+ * @param string|false $permission View permission.
+ * @param string $content Wrapper content.
+ * @return string
+ */
+ private function render( WC_Order $order, $permission, string $content ): string {
+ $proxy = new class() extends BillingWrapperBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_content_proxy( $order, $permission, $content ) {
+ return $this->render_content( $order, $permission, array(), $content );
+ }
+ };
+
+ return $proxy->render_content_proxy( $order, $permission, $content );
+ }
+
+ /**
+ * Create a persisted order for one fulfillment topology.
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param bool $has_address Whether to add billing data.
+ * @return WC_Order
+ */
+ private function create_order( string $topology, bool $has_address ): WC_Order {
+ $product = \WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'downloadable' => 'virtual' === $topology,
+ 'virtual' => 'virtual' === $topology,
+ )
+ );
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+
+ if ( $has_address ) {
+ $order->set_billing_first_name( 'Billing' );
+ $order->set_billing_last_name( 'Marker' );
+ $order->set_billing_address_1( '500 Billing Avenue' );
+ $order->set_billing_city( 'San Francisco' );
+ $order->set_billing_state( 'CA' );
+ $order->set_billing_postcode( '94105' );
+ $order->set_billing_country( 'US' );
+ }
+
+ if ( 'virtual' !== $topology ) {
+ $shipping_item = new WC_Order_Item_Shipping();
+ // `local_pickup` is the classic pickup method, `pickup_location` the Blocks one.
+ // `needs_shipping_address()` hides the address for both, but it finds
+ // `pickup_location` only by asking the registered shipping methods which of them
+ // support `local-pickup`. Core registers that method when the Checkout block is
+ // the default checkout, which the test store is not, so register it the same way.
+ $pickup_methods = array(
+ 'local-pickup' => array( 'Local pickup', 'local_pickup' ),
+ 'pickup-location' => array( 'Pickup Location', 'pickup_location' ),
+ );
+
+ if ( 'pickup-location' === $topology ) {
+ WC()->shipping()->get_shipping_methods();
+ WC()->shipping()->register_shipping_method( new PickupLocation() );
+ }
+
+ list( $method_title, $method_id ) = $pickup_methods[ $topology ] ?? array( 'Free shipping', 'free_shipping' );
+
+ $shipping_item->set_method_title( $method_title );
+ $shipping_item->set_method_id( $method_id );
+ $order->add_item( $shipping_item );
+ }
+
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsTest.php
new file mode 100644
index 00000000000..4d45a6677c6
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsTest.php
@@ -0,0 +1,150 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\Downloads as DownloadsBlock;
+use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
+use WC_Order;
+use WC_Order_Item_Product;
+use WC_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the order-confirmation downloads table.
+ */
+final class DownloadsTest extends WC_Unit_Test_Case {
+ /**
+ * @testdox Download rows render for authorized processing and completed orders with the exact names, URLs, and table classes.
+ */
+ public function test_download_table_requires_entitlement_and_permission(): void {
+ $download_directories = wc_get_container()->get( Download_Directories::class );
+
+ $download_directories->set_mode( Download_Directories::MODE_DISABLED );
+ update_option( 'woocommerce_downloads_grant_access_after_payment', 'yes' );
+ add_filter(
+ 'woocommerce_downloadable_file_exists',
+ static function (): bool {
+ return true;
+ }
+ );
+
+ $product = \WC_Helper_Product::create_downloadable_product(
+ array(
+ array(
+ 'name' => 'Single 1',
+ 'file' => 'https://example.com/single-1.txt',
+ ),
+ array(
+ 'name' => 'Single 2',
+ 'file' => 'https://example.com/single-2.txt',
+ ),
+ )
+ );
+ $order = $this->create_order( $product );
+
+ $this->assertSame( '', $this->render( $order, 'full' ), 'Pending orders should not render a downloads table.' );
+
+ $order->set_status( 'processing' );
+ $order->save();
+ wc_downloadable_product_permissions( $order->get_id(), true );
+ $order = $this->reload_order( $order );
+
+ $this->assert_download_table( $order, 'processing' );
+
+ $order->set_status( 'completed' );
+ $order->save();
+ $order = $this->reload_order( $order );
+
+ $this->assert_download_table( $order, 'completed' );
+
+ $this->assertSame( '', $this->render( $order, false ), 'Order details permission is required even after entitlement is granted.' );
+ }
+
+ /**
+ * Assert the rendered download table for one entitled order status.
+ *
+ * @param WC_Order $order Order object.
+ * @param string $status Expected order status.
+ */
+ private function assert_download_table( WC_Order $order, string $status ): void {
+ $downloads = $order->get_downloadable_items();
+ $download_names = array_column( $downloads, 'download_name' );
+ sort( $download_names );
+
+ $this->assertSame( $status, $order->get_status() );
+ $this->assertCount( 2, $downloads, 'The real order should expose both granted download entitlements.' );
+ $this->assertSame( array( 'Single 1', 'Single 2' ), $download_names, 'The exact two unique fixture download names should be granted.' );
+
+ foreach ( $downloads as $download ) {
+ $this->assertNotSame( '', $download['download_url'], 'A granted entitlement must have a non-empty URL.' );
+ }
+
+ $content = $this->render( $order, 'full' );
+
+ $this->assertStringContainsString( 'wc-block-order-confirmation-downloads__table', $content );
+ $this->assertSame( 3, substr_count( $content, '<tr>' ), 'The table should contain one header row and two entitlement rows.' );
+ $this->assertSame( 2, substr_count( $content, '<td class="download-file"' ), 'Each entitlement should render a file cell.' );
+ $this->assertSame( 2, substr_count( $content, 'class="woocommerce-MyAccount-downloads-file button alt"' ), 'Each entitlement should render the expected download-link classes.' );
+
+ foreach ( $downloads as $download ) {
+ $escaped_download_url = esc_url( $download['download_url'] );
+
+ $this->assertNotSame( '', $escaped_download_url, 'A granted entitlement must retain a non-empty URL after escaping.' );
+ $this->assertStringContainsString( esc_html( $download['download_name'] ), $content );
+ $this->assertStringContainsString( $escaped_download_url, $content );
+ }
+ }
+
+ /**
+ * Reload an order from persistence.
+ *
+ * @param WC_Order $order Order object.
+ * @return WC_Order
+ */
+ private function reload_order( WC_Order $order ): WC_Order {
+ $reloaded_order = wc_get_order( $order->get_id() );
+ $this->assertInstanceOf( WC_Order::class, $reloaded_order );
+
+ return $reloaded_order;
+ }
+
+ /**
+ * Render the downloads table through a public proxy.
+ *
+ * @param WC_Order $order Order object.
+ * @param string|false $permission View permission.
+ * @return string
+ */
+ private function render( WC_Order $order, $permission ): string {
+ $proxy = new class() extends DownloadsBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_content_proxy( $order, $permission ) {
+ return $this->render_content( $order, $permission );
+ }
+ };
+
+ return $proxy->render_content_proxy( $order, $permission );
+ }
+
+ /**
+ * Create a guest order containing the downloadable product.
+ *
+ * @param WC_Product $product Product object.
+ * @return WC_Order
+ */
+ private function create_order( WC_Product $product ): WC_Order {
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+ $order->set_billing_email( 'downloads-shopper@example.com' );
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
index f44bb093cbb..6c731b65b5e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
@@ -3,6 +3,7 @@
namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\DownloadsWrapper as DownloadsWrapperClass;
+use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
/**
* Test DownloadsWrapper class.
@@ -117,4 +118,92 @@ final class DownloadsWrapper extends \WP_UnitTestCase {
\WC_Helper_Product::create_simple_product( true, array( 'downloadable' => true ) );
$this->assertFalse( $proxy->store_has_downloadable_products_proxy() );
}
+
+ /**
+ * @testdox Runtime wrapper content requires permission, a downloadable order item, and a download-permitted status.
+ */
+ public function test_runtime_render_requires_download_permission(): void {
+ $download_directories = wc_get_container()->get( Download_Directories::class );
+
+ $download_directories->set_mode( Download_Directories::MODE_DISABLED );
+ add_filter(
+ 'woocommerce_downloadable_file_exists',
+ static function (): bool {
+ return true;
+ }
+ );
+
+ $downloadable_product = \WC_Helper_Product::create_downloadable_product(
+ array(
+ array(
+ 'name' => 'Wrapper download',
+ 'file' => 'https://example.com/wrapper-download.txt',
+ ),
+ )
+ );
+ $plain_product = \WC_Helper_Product::create_simple_product();
+ $downloadable_order = $this->create_order_with_product( $downloadable_product );
+ $plain_order = $this->create_order_with_product( $plain_product );
+
+ $this->assertSame( '', $this->render( $downloadable_order, 'full' ), 'A pending order should not expose downloads.' );
+
+ // Processing permits downloads only when the store grants access after payment,
+ // so the wrapper has to follow that option rather than the status on its own.
+ $downloadable_order->set_status( 'processing' );
+ $downloadable_order->save();
+
+ update_option( 'woocommerce_downloads_grant_access_after_payment', 'no' );
+ $this->assertSame( '', $this->render( $downloadable_order, 'full' ), 'A processing order should not expose downloads while access is granted only on completion.' );
+
+ update_option( 'woocommerce_downloads_grant_access_after_payment', 'yes' );
+ $this->assertSame( '<p>Download marker</p>', $this->render( $downloadable_order, 'full' ), 'A processing order should expose downloads once the store grants access after payment.' );
+
+ $plain_order->set_status( 'completed' );
+ $plain_order->save();
+ $this->assertSame( '', $this->render( $plain_order, 'full' ), 'An order without a downloadable item should not render the wrapper.' );
+
+ $downloadable_order->set_status( 'completed' );
+ $downloadable_order->save();
+ $this->assertSame( '', $this->render( $downloadable_order, false ), 'View permission is required.' );
+ $this->assertSame( '<p>Download marker</p>', $this->render( $downloadable_order, 'full' ), 'A completed downloadable order should render non-empty wrapper content.' );
+ }
+
+ /**
+ * Render runtime wrapper content through a public proxy.
+ *
+ * @param \WC_Order $order Order object.
+ * @param string|false $permission View permission.
+ * @return string
+ */
+ private function render( \WC_Order $order, $permission ): string {
+ $proxy = new class() extends DownloadsWrapperClass {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_content_proxy( $order, $permission, $content ) {
+ return $this->render_content( $order, $permission, array(), $content );
+ }
+ };
+
+ return $proxy->render_content_proxy( $order, $permission, '<p>Download marker</p>' );
+ }
+
+ /**
+ * Create a persisted order containing one product.
+ *
+ * @param \WC_Product $product Product object.
+ * @return \WC_Order
+ */
+ private function create_order_with_product( \WC_Product $product ): \WC_Order {
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new \WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+ $order->set_billing_email( 'wrapper-shopper@example.com' );
+ $order->save();
+
+ return $order;
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingAddress.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingAddress.php
index 51df23985eb..c797af98fbe 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingAddress.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingAddress.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\ShippingAddress as ShippingAddressBlock;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Blocks\Shipping\PickupLocation;
/**
* Test ShippingAddress block class.
@@ -60,6 +61,55 @@ final class ShippingAddress extends \WP_UnitTestCase {
$this->assertStringContainsString( 'visible value', $this->render( $order ) );
}
+ /**
+ * @testdox Shipping details render only for authorized orders that require a shipping address.
+ * @dataProvider shipping_address_topology_cases
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param string|false $permission View permission.
+ * @param bool $expected_visible Whether shipping content should render.
+ */
+ public function test_shipping_address_topology( string $topology, $permission, bool $expected_visible ): void {
+ try {
+ // Inside the try: create_topology_order() registers PickupLocation partway
+ // through, so a throw after that point must still reach the finally below.
+ $order = $this->create_topology_order( $topology );
+ update_option( 'woocommerce_calc_shipping', 'yes' );
+ $content = $this->render( $order, $permission );
+ if ( 'physical' === $topology ) {
+ $this->assertTrue( $order->needs_shipping_address(), 'A free-shipping order should require a shipping address.' );
+ } else {
+ $this->assertFalse( $order->needs_shipping_address(), 'Pickup and virtual orders should not require a shipping address.' );
+ }
+
+ if ( $expected_visible ) {
+ $this->assertStringContainsString( 'Shipping Marker', $content, 'Shipping details should be non-empty for a shipped order.' );
+ } else {
+ $this->assertSame( '', $content, 'Shipping details should be empty for this topology or permission.' );
+ }
+ } finally {
+ if ( 'pickup-location' === $topology ) {
+ // Drop the memo so the next test reloads the ambient method list.
+ WC()->shipping()->unregister_shipping_methods();
+ }
+ }
+ }
+
+ /**
+ * Named shipping-address topology cases.
+ *
+ * @return array<string, array{string, string|false, bool}>
+ */
+ public static function shipping_address_topology_cases(): array {
+ return array(
+ 'physical free-shipping order' => array( 'physical', 'full', true ),
+ 'classic local pickup order' => array( 'local-pickup', 'full', false ),
+ 'blocks pickup location order' => array( 'pickup-location', 'full', false ),
+ 'virtual downloadable order' => array( 'virtual', 'full', false ),
+ 'no view permission' => array( 'physical', false, false ),
+ );
+ }
+
/**
* Create an order placed via the Store API, with a shipping address and a value persisted for the shipping group of the test field.
*
@@ -85,10 +135,11 @@ final class ShippingAddress extends \WP_UnitTestCase {
/**
* Render the shipping address block content for the given order with full view permissions.
*
- * @param \WC_Order $order Order object.
+ * @param \WC_Order $order Order object.
+ * @param string|false $permission View permission.
* @return string
*/
- private function render( \WC_Order $order ): string {
+ private function render( \WC_Order $order, $permission = 'full' ): string {
$proxy = new class() extends ShippingAddressBlock {
// phpcs:ignore Squiz.Commenting.FunctionComment.Missing
public function __construct() {
@@ -99,6 +150,65 @@ final class ShippingAddress extends \WP_UnitTestCase {
}
};
- return $proxy->render_content_proxy( $order, 'full' );
+ return $proxy->render_content_proxy( $order, $permission );
+ }
+
+ /**
+ * Create an order with a persisted shipping address and fulfillment topology.
+ *
+ * @param string $topology Order fulfillment topology.
+ * @return \WC_Order
+ */
+ private function create_topology_order( string $topology ): \WC_Order {
+ $product = \WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'downloadable' => 'virtual' === $topology,
+ 'virtual' => 'virtual' === $topology,
+ )
+ );
+ $product->set_virtual( 'virtual' === $topology );
+ $product->save();
+
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new \WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+ $order->set_shipping_first_name( 'Shipping' );
+ $order->set_shipping_last_name( 'Marker' );
+ $order->set_shipping_address_1( '500 Shipping Avenue' );
+ $order->set_shipping_city( 'San Francisco' );
+ $order->set_shipping_state( 'CA' );
+ $order->set_shipping_postcode( '94105' );
+ $order->set_shipping_country( 'US' );
+
+ if ( 'virtual' !== $topology ) {
+ $shipping_item = new \WC_Order_Item_Shipping();
+ // `local_pickup` is the classic pickup method, `pickup_location` the Blocks one.
+ // `needs_shipping_address()` hides the address for both, but it finds
+ // `pickup_location` only by asking the registered shipping methods which of them
+ // support `local-pickup`. Core registers that method when the Checkout block is
+ // the default checkout, which the test store is not, so register it the same way.
+ $pickup_methods = array(
+ 'local-pickup' => array( 'Local pickup', 'local_pickup' ),
+ 'pickup-location' => array( 'Pickup Location', 'pickup_location' ),
+ );
+
+ if ( 'pickup-location' === $topology ) {
+ WC()->shipping()->get_shipping_methods();
+ WC()->shipping()->register_shipping_method( new PickupLocation() );
+ }
+
+ list( $method_title, $method_id ) = $pickup_methods[ $topology ] ?? array( 'Free shipping', 'free_shipping' );
+
+ $shipping_item->set_method_title( $method_title );
+ $shipping_item->set_method_id( $method_id );
+ $order->add_item( $shipping_item );
+ }
+
+ $order->save();
+
+ return $order;
}
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingWrapperTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingWrapperTest.php
new file mode 100644
index 00000000000..cb48f9a52db
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/ShippingWrapperTest.php
@@ -0,0 +1,153 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\ShippingWrapper as ShippingWrapperBlock;
+use Automattic\WooCommerce\Blocks\Shipping\PickupLocation;
+use WC_Order;
+use WC_Order_Item_Product;
+use WC_Order_Item_Shipping;
+use WC_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the order-confirmation shipping wrapper.
+ */
+final class ShippingWrapperTest extends WC_Unit_Test_Case {
+ /**
+ * @testdox Shipping wrapper headings and content render only for an authorized shipped order.
+ * @dataProvider shipping_wrapper_cases
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param string|false $permission View permission.
+ * @param bool $has_address Whether the order has a shipping address.
+ * @param bool $expected_visible Whether wrapper content should render.
+ */
+ public function test_shipping_wrapper_topology( string $topology, $permission, bool $has_address, bool $expected_visible ): void {
+ $content = '<h2>Shipping address</h2><p>Shipping wrapper marker</p>';
+
+ try {
+ // Inside the try: create_order() registers PickupLocation partway through,
+ // so a throw after that point must still reach the finally below.
+ $order = $this->create_order( $topology, $has_address );
+ update_option( 'woocommerce_calc_shipping', 'yes' );
+ $rendered = $this->render( $order, $permission, $content );
+
+ if ( 'physical' === $topology ) {
+ $this->assertTrue( $order->needs_shipping_address(), 'A free-shipping order should require a shipping address.' );
+ } else {
+ $this->assertFalse( $order->needs_shipping_address(), 'Pickup and virtual orders should not require a shipping address.' );
+ }
+
+ if ( $expected_visible ) {
+ $this->assertSame( $content, $rendered, 'An authorized shipped order should preserve the wrapper heading and content.' );
+ } else {
+ $this->assertSame( '', $rendered, 'The shipping wrapper should be empty for pickup, virtual, missing-address, or unauthorized orders.' );
+ }
+ } finally {
+ if ( 'pickup-location' === $topology ) {
+ // Drop the memo so the next test reloads the ambient method list.
+ WC()->shipping()->unregister_shipping_methods();
+ }
+ }
+ }
+
+ /**
+ * Named shipping-wrapper cases.
+ *
+ * @return array<string, array{string, string|false, bool, bool}>
+ */
+ public static function shipping_wrapper_cases(): array {
+ return array(
+ 'physical free-shipping order' => array( 'physical', 'full', true, true ),
+ 'classic local pickup order' => array( 'local-pickup', 'full', true, false ),
+ 'blocks pickup location order' => array( 'pickup-location', 'full', true, false ),
+ 'virtual downloadable order' => array( 'virtual', 'full', true, false ),
+ 'no view permission' => array( 'physical', false, true, false ),
+ 'no shipping address' => array( 'physical', 'full', false, false ),
+ );
+ }
+
+ /**
+ * Render wrapper content through a public proxy.
+ *
+ * @param WC_Order $order Order object.
+ * @param string|false $permission View permission.
+ * @param string $content Wrapper content.
+ * @return string
+ */
+ private function render( WC_Order $order, $permission, string $content ): string {
+ $proxy = new class() extends ShippingWrapperBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_content_proxy( $order, $permission, $content ) {
+ return $this->render_content( $order, $permission, array(), $content );
+ }
+ };
+
+ return $proxy->render_content_proxy( $order, $permission, $content );
+ }
+
+ /**
+ * Create a persisted order for one fulfillment topology.
+ *
+ * @param string $topology Order fulfillment topology.
+ * @param bool $has_address Whether to add shipping data.
+ * @return WC_Order
+ */
+ private function create_order( string $topology, bool $has_address ): WC_Order {
+ $product = \WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'downloadable' => 'virtual' === $topology,
+ 'virtual' => 'virtual' === $topology,
+ )
+ );
+ $order = wc_create_order( array( 'customer_id' => 0 ) );
+ $item = new WC_Order_Item_Product();
+ $item->set_product( $product );
+ $item->set_quantity( 1 );
+ $order->add_item( $item );
+
+ if ( $has_address ) {
+ $order->set_shipping_first_name( 'Shipping' );
+ $order->set_shipping_last_name( 'Marker' );
+ $order->set_shipping_address_1( '500 Shipping Avenue' );
+ $order->set_shipping_city( 'San Francisco' );
+ $order->set_shipping_state( 'CA' );
+ $order->set_shipping_postcode( '94105' );
+ $order->set_shipping_country( 'US' );
+ }
+
+ if ( 'virtual' !== $topology ) {
+ $shipping_item = new WC_Order_Item_Shipping();
+ // `local_pickup` is the classic pickup method, `pickup_location` the Blocks one.
+ // `needs_shipping_address()` hides the address for both, but it finds
+ // `pickup_location` only by asking the registered shipping methods which of them
+ // support `local-pickup`. Core registers that method when the Checkout block is
+ // the default checkout, which the test store is not, so register it the same way.
+ $pickup_methods = array(
+ 'local-pickup' => array( 'Local pickup', 'local_pickup' ),
+ 'pickup-location' => array( 'Pickup Location', 'pickup_location' ),
+ );
+
+ if ( 'pickup-location' === $topology ) {
+ WC()->shipping()->get_shipping_methods();
+ WC()->shipping()->register_shipping_method( new PickupLocation() );
+ }
+
+ list( $method_title, $method_id ) = $pickup_methods[ $topology ] ?? array( 'Free shipping', 'free_shipping' );
+
+ $shipping_item->set_method_title( $method_title );
+ $shipping_item->set_method_id( $method_id );
+ $order->add_item( $shipping_item );
+ }
+
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/StatusTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/StatusTest.php
index 4deea90017a..53e4e39effe 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/StatusTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/StatusTest.php
@@ -272,4 +272,98 @@ class StatusTest extends WC_Unit_Test_Case {
return $sut->render_content_proxy( $order, 'full' );
}
+
+ /**
+ * @testdox A missing order directs the shopper to email and account confirmation paths.
+ */
+ public function test_missing_order_notice(): void {
+ $my_account_url = wc_get_page_permalink( 'myaccount' );
+ $content = $this->render_confirmation_notice( null );
+
+ $this->assertNotSame( '', $my_account_url, 'The fixture must expose the My account login destination.' );
+ $this->assertStringContainsString( 'If you've just placed an order, give your email a quick check for the confirmation.', $content );
+ $this->assertStringContainsString( 'Have an account with us?', $content );
+ $this->assertStringContainsString( 'Log in here to view your order details', $content );
+ $this->assertStringContainsString( 'href="' . esc_url( $my_account_url ) . '"', $content );
+ }
+
+ /**
+ * @testdox An existing customer order without a valid key directs the shopper to email and account confirmation paths.
+ * @dataProvider invalid_order_key_cases
+ *
+ * @param string $key_mode Missing or invalid key mode.
+ */
+ public function test_invalid_key_notice_uses_real_permission_result( string $key_mode ): void {
+ $customer_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+ $order = wc_create_order( array( 'customer_id' => $customer_id ) );
+ $order->set_billing_email( 'status-shopper@example.com' );
+ $order->save();
+ wp_set_current_user( $customer_id );
+ $_GET = 'wrong' === $key_mode ? array( 'key' => 'wc_order_wrong' ) : array();
+
+ $permission = $this->get_view_order_permissions( $order );
+ $content = $this->render_confirmation_notice( $order );
+ $my_account_url = wc_get_page_permalink( 'myaccount' );
+
+ $this->assertFalse( $permission, 'The real permission decision must reject a missing or invalid order key.' );
+ $this->assertNotSame( '', $my_account_url, 'The fixture must expose the My account login destination.' );
+ $this->assertStringContainsString( 'Great news! Your order has been received, and a confirmation will be sent to your email address.', $content );
+ $this->assertStringContainsString( 'Have an account with us?', $content );
+ $this->assertStringContainsString( 'Log in here', $content );
+ $this->assertStringContainsString( 'to view your order.', $content );
+ $this->assertStringContainsString( 'href="' . esc_url( $my_account_url ) . '"', $content );
+ }
+
+ /**
+ * Named invalid-key cases.
+ *
+ * @return array<string, array{string}>
+ */
+ public static function invalid_order_key_cases(): array {
+ return array(
+ 'missing order key' => array( 'missing' ),
+ 'wrong order key' => array( 'wrong' ),
+ );
+ }
+
+ /**
+ * Render a status notice through the real Status block owner.
+ *
+ * @param WC_Order|null $order Order object, or null when none exists.
+ * @return string
+ */
+ private function render_confirmation_notice( ?WC_Order $order ): string {
+ return $this->create_proxy()->render_confirmation_notice_proxy( $order );
+ }
+
+ /**
+ * Resolve view permission through the real Status block owner.
+ *
+ * @param WC_Order $order Order object.
+ * @return string|false
+ */
+ private function get_view_order_permissions( WC_Order $order ) {
+ return $this->create_proxy()->get_view_order_permissions_proxy( $order );
+ }
+
+ /**
+ * Create a public proxy over the Status block's protected owners.
+ *
+ * @return StatusBlock
+ */
+ private function create_proxy(): StatusBlock {
+ return new class() extends StatusBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_confirmation_notice_proxy( ?WC_Order $order ): string {
+ return $this->render_confirmation_notice( $order );
+ }
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function get_view_order_permissions_proxy( WC_Order $order ) {
+ return $this->get_view_order_permissions( $order );
+ }
+ };
+ }
}