Commit ca2caf5c45d for woocommerce

commit ca2caf5c45ddac262916adc4ed9367e836788572
Author: Mike Jolley <mike.jolley@me.com>
Date:   Fri Jul 3 11:08:35 2026 +0100

    Fix My Account endpoint titles with a non-empty cart in block themes (#65949)

    * Fix My Account endpoint titles when cart is not empty in block themes

    * Add tests for wc_page_endpoint_title() endpoint-page guard

    * Fix PHPCS errors on phpcs:ignore reason annotation

    The inline reason was placed before the phpcs:ignore directive, which
    turned the line into a regular comment (tripping the InlineComment and
    PostStatementComment sniffs) and stopped the directive from suppressing
    the GlobalVariablesOverride error. Move the reason into the documented
    -- annotation suffix so it stays a pure directive.

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

    * Document intentional $post_id = 0 default on wc_page_endpoint_title()

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

    ---------

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

diff --git a/plugins/woocommerce/changelog/fix-orders-endpoint-title-cart b/plugins/woocommerce/changelog/fix-orders-endpoint-title-cart
new file mode 100644
index 00000000000..31b7fdf51e6
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-orders-endpoint-title-cart
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix My Account endpoint page titles showing the page title instead of the endpoint heading (e.g. "Orders") in block themes when the cart is not empty.
diff --git a/plugins/woocommerce/includes/wc-page-functions.php b/plugins/woocommerce/includes/wc-page-functions.php
index a0d433e280d..62cdef429b7 100644
--- a/plugins/woocommerce/includes/wc-page-functions.php
+++ b/plugins/woocommerce/includes/wc-page-functions.php
@@ -13,13 +13,18 @@ defined( 'ABSPATH' ) || exit;
 /**
  * Replace a page title with the endpoint title.
  *
- * @param  string $title Post title.
+ * @param  string $title   Post title.
+ * @param  int    $post_id Optional. The post ID the title belongs to. Passed by the `the_title` filter. Defaults to 0 for backwards compatibility with callers that pass only the title, in which case the queried-object check is skipped (matching the original single-argument behaviour).
  * @return string
  */
-function wc_page_endpoint_title( $title ) {
+function wc_page_endpoint_title( $title, $post_id = 0 ) {
 	global $wp_query;

-	if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && is_wc_endpoint_url() ) {
+	// In block themes the whole template (header, footer, content) renders inside the main
+	// loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
+	// server-rendered mini-cart) - not just the page's own heading. Only replace the title of
+	// the queried page so an earlier title doesn't consume this one-shot filter.
+	if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && is_wc_endpoint_url() && ( ! $post_id || get_queried_object_id() === $post_id ) ) {
 		$endpoint       = WC()->query->get_current_endpoint();
 		$action         = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
 		$endpoint_title = WC()->query->get_endpoint_title( $endpoint, $action );
@@ -31,7 +36,7 @@ function wc_page_endpoint_title( $title ) {
 	return $title;
 }

-add_filter( 'the_title', 'wc_page_endpoint_title' );
+add_filter( 'the_title', 'wc_page_endpoint_title', 10, 2 );

 /**
  * Replace the title part of the document title.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/page-functions/class-wc-tests-page-functions.php b/plugins/woocommerce/tests/legacy/unit-tests/page-functions/class-wc-tests-page-functions.php
index ef1109b8d4d..509babf506b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/page-functions/class-wc-tests-page-functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/page-functions/class-wc-tests-page-functions.php
@@ -43,4 +43,76 @@ class WC_Tests_Page_Functions extends WC_Unit_Test_Case {
 		$url = wc_get_endpoint_url( 'customer-logout', '', 'https://' . WP_TESTS_DOMAIN . '/' );
 		$this->assertEquals( 'https://' . WP_TESTS_DOMAIN . '/customer-logout', $url );
 	}
+
+	/**
+	 * Reset the query globals mutated by the endpoint-title tests and restore the filter
+	 * that wc_page_endpoint_title() removes from `the_title` once it matches.
+	 */
+	public function tearDown(): void {
+		global $wp, $wp_query, $wp_the_query, $post;
+		$wp           = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$wp_query     = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$wp_the_query = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$post         = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		delete_option( 'woocommerce_myaccount_page_id' );
+		add_filter( 'the_title', 'wc_page_endpoint_title', 10, 2 );
+
+		parent::tearDown();
+	}
+
+	/**
+	 * Set up the globals for a My Account "orders" endpoint request rendered inside the main
+	 * loop, so the conditional tags wc_page_endpoint_title() relies on are all genuinely true.
+	 *
+	 * @return int The My Account page ID, which is also the queried object.
+	 */
+	private function set_up_orders_endpoint_in_loop() {
+		$page_id = wp_insert_post(
+			array(
+				'post_type'   => 'page',
+				'post_status' => 'publish',
+				'post_title'  => 'My account',
+				'post_name'   => 'my-account',
+			)
+		);
+		update_option( 'woocommerce_myaccount_page_id', $page_id );
+
+		global $post, $wp, $wp_query, $wp_the_query;
+		$post = get_post( $page_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+		$wp             = new stdClass(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$wp->query_vars = array( 'orders' => '' );
+
+		$wp_query                    = new WP_Query(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$wp_query->is_page           = true;
+		$wp_query->in_the_loop       = true;
+		$wp_query->queried_object    = $post;
+		$wp_query->queried_object_id = $page_id;
+		$wp_the_query                = $wp_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- is_main_query() compares against this.
+
+		return $page_id;
+	}
+
+	/**
+	 * @testdox wc_page_endpoint_title() replaces the queried page's title with the endpoint title.
+	 */
+	public function test_wc_page_endpoint_title_replaces_queried_page_title() {
+		$page_id = $this->set_up_orders_endpoint_in_loop();
+
+		$this->assertSame( 'Orders', wc_page_endpoint_title( 'My account', $page_id ) );
+	}
+
+	/**
+	 * @testdox wc_page_endpoint_title() leaves titles of other posts untouched so an earlier title does not consume the one-shot the_title filter before the page heading renders.
+	 */
+	public function test_wc_page_endpoint_title_ignores_non_queried_titles() {
+		$this->set_up_orders_endpoint_in_loop();
+		$product = WC_Helper_Product::create_simple_product();
+
+		$this->assertSame(
+			$product->get_name(),
+			wc_page_endpoint_title( $product->get_name(), $product->get_id() ),
+			'A title belonging to another post (e.g. a product in a server-rendered mini-cart) must not be replaced with the endpoint title.'
+		);
+	}
 }