Commit f45f0ff7a90 for woocommerce

commit f45f0ff7a903605290819545c6de2211a2f18a5b
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Mon Aug 17 11:58:32 2026 +0200

    Fix duplicate Mini-Cart wrappers in pattern-backed templates (#67431)

    * Fix duplicate Mini-Cart wrappers in pattern-backed templates

    * Add changelog entry for Mini-Cart pattern wrapper fix

    * Simplify Mini-Cart pattern wrapper detection

    * Preserve Mini-Cart pattern rendering

    * Simplify Mini-Cart pattern rendering test

    * Fix Mini-Cart rendered button assertions

    * Fix Mini-Cart pattern uniqueness assertions

    * Stabilize Mini-Cart pattern rendering test

    * Fix Mini-Cart duplicate wrapper assertion

diff --git a/plugins/woocommerce/changelog/fix-mini-cart-pattern-wrappers b/plugins/woocommerce/changelog/fix-mini-cart-pattern-wrappers
new file mode 100644
index 00000000000..7121342cc32
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-mini-cart-pattern-wrappers
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent duplicate Mini-Cart markup when a template part references a block pattern.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
index fce4b28a622..7fc3a35cd7b 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
@@ -304,7 +304,7 @@ class MiniCart extends AbstractBlock {
 			$wrapper_styles           = $classes_styles['styles'];
 			// Pre-render the template part so nested blocks enqueue their assets before the overlay is printed in wp_footer.
 			$template_part_contents           = $this->get_template_part_contents( false );
-			$template_part_contents           = do_blocks( $this->process_template_contents( $template_part_contents ) );
+			$template_part_contents           = $this->render_template_part_contents( $template_part_contents );
 			$cart_item_count                  = $cart ? $cart->get_cart_contents_count() : 0;
 			$display_cart_price_including_tax = get_option( 'woocommerce_tax_display_cart' ) === TaxDisplayMode::INCLUSIVE;
 			$cart_item_count                  = $cart ? $cart->get_cart_contents_count() : 0;
@@ -445,7 +445,7 @@ class MiniCart extends AbstractBlock {
 	 */
 	public function render_mini_cart_overlay() {
 		$template_part_contents = $this->get_template_part_contents( false );
-		$template_part_contents = do_blocks( $this->process_template_contents( $template_part_contents ) );
+		$template_part_contents = $this->render_template_part_contents( $template_part_contents );
 		ob_start();
 		?>
 		<div
@@ -479,6 +479,34 @@ class MiniCart extends AbstractBlock {
 		echo wp_interactivity_process_directives( ob_get_clean() );
 	}

+	/**
+	 * Render Mini-Cart template contents while removing legacy saved wrappers.
+	 *
+	 * @param string $template_contents The template contents to render.
+	 * @return string The rendered template contents.
+	 */
+	private function render_template_part_contents( $template_contents ) {
+		$process_legacy_wrappers = function ( $parsed_block ) {
+			if ( 'woocommerce/mini-cart-contents' !== ( $parsed_block['blockName'] ?? null ) ) {
+				return $parsed_block;
+			}
+
+			$processed_blocks = parse_blocks(
+				$this->process_template_contents( serialize_block( $parsed_block ) )
+			);
+
+			return $processed_blocks[0] ?? $parsed_block;
+		};
+
+		add_filter( 'render_block_data', $process_legacy_wrappers, 10, 1 );
+
+		try {
+			return do_blocks( $template_contents );
+		} finally {
+			remove_filter( 'render_block_data', $process_legacy_wrappers, 10 );
+		}
+	}
+
 	/**
 	 * Process template contents to remove unwanted div wrappers.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
index 9aae16102e9..17fcba41ea6 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/MiniCart.php
@@ -92,6 +92,19 @@ class MiniCart extends \WP_UnitTestCase {
 						<img class="wp-image-block" src="https://example.com/image.jpg" alt="Example Image" />
 					<!-- /wp:image -->
 				</div>
+				<!-- /wp:woocommerce/mini-cart-title-block -->
+
+				<!-- wp:woocommerce/mini-cart-footer-block -->
+				<div class="wp-block-woocommerce-mini-cart-footer-block">
+					<!-- wp:woocommerce/mini-cart-cart-button-block -->
+					<div class="wp-block-woocommerce-mini-cart-cart-button-block"></div>
+					<!-- /wp:woocommerce/mini-cart-cart-button-block -->
+
+					<!-- wp:woocommerce/mini-cart-checkout-button-block -->
+					<div class="wp-block-woocommerce-mini-cart-checkout-button-block"></div>
+					<!-- /wp:woocommerce/mini-cart-checkout-button-block -->
+				</div>
+				<!-- /wp:woocommerce/mini-cart-footer-block -->
 			</div>
 			<!-- /wp:woocommerce/filled-mini-cart-contents-block -->
 		</div>
@@ -235,6 +248,55 @@ class MiniCart extends \WP_UnitTestCase {
 		}
 	}

+	/**
+	 * @testdox Should process legacy wrappers while preserving the pattern rendering lifecycle.
+	 */
+	public function test_render_pattern_backed_template(): void {
+		$pattern_name = 'woocommerce-tests/mini-cart-template-part';
+		register_block_pattern(
+			$pattern_name,
+			array(
+				'title'   => 'Mini Cart template part',
+				'content' => $this->current_template_with_user_edits,
+			)
+		);
+
+		$pattern_filter_calls = 0;
+		$pattern_filter       = static function ( $content ) use ( &$pattern_filter_calls ) {
+			++$pattern_filter_calls;
+			return $content . '<span data-pattern-filter-ran></span>';
+		};
+
+		add_filter( 'render_block_core/pattern', $pattern_filter, 10, 1 );
+
+		try {
+			$method = new \ReflectionMethod( MiniCartBlock::class, 'render_template_part_contents' );
+			$method->setAccessible( true );
+			$rendered_template = $method->invoke(
+				$this->mock,
+				'<!-- wp:pattern {"slug":"' . $pattern_name . '"} /-->'
+			);
+		} finally {
+			remove_filter( 'render_block_core/pattern', $pattern_filter, 10 );
+			unregister_block_pattern( $pattern_name );
+		}
+
+		$this->assertSame( 1, $pattern_filter_calls, 'The core pattern render filter should run exactly once.' );
+		$this->assertStringContainsString(
+			'data-pattern-filter-ran',
+			$rendered_template,
+			'The filtered pattern output should be preserved.'
+		);
+
+		$p                    = new \WP_HTML_Tag_Processor( $rendered_template );
+		$footer_wrapper_count = 0;
+		while ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-mini-cart-footer-block' ) ) ) {
+			++$footer_wrapper_count;
+		}
+
+		$this->assertSame( 1, $footer_wrapper_count, 'The rendered template should contain exactly one footer wrapper.' );
+	}
+
 	/**
 	 * Checks that process_template_contents removes the wrapper divs from the
 	 * current template with user edits, but preserves the user edits.