Commit f4b5b620295 for woocommerce

commit f4b5b620295ef0db74f72e6de404709ad7066ef6
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Wed Oct 7 11:02:24 2026 +0200

    Cover product blocks in a sent email with an e2e test (#69443)

    The test was extended to cover a regression in which the product collection block was skipped in the email during sending.

diff --git a/plugins/woocommerce/tests/e2e/tests/email/order-emails-block-editor.spec.ts b/plugins/woocommerce/tests/e2e/tests/email/order-emails-block-editor.spec.ts
index dd5dcf75b8d..a5829ec7e53 100644
--- a/plugins/woocommerce/tests/e2e/tests/email/order-emails-block-editor.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email/order-emails-block-editor.spec.ts
@@ -25,12 +25,18 @@ import { accessTheEmailEditor, expectEmail } from '../../utils/email';
  *
  * Uses the customer "Processing order" email; delivery is asserted through the
  * WP Mail Logging inbox like the classic `order-emails.spec.ts`.
+ *
+ * The customization includes a Product Collection block, so the sent email also
+ * covers WooCommerce registering its block types for the render. Orders created
+ * through the REST API are one of the requests that skip that registration, and
+ * the block used to render empty in the emails they send.
  */

 const EMAIL_LISTING_TITLE = 'Order confirmation';
 const EMAIL_TYPE = 'customer_processing_order';
 const SUBJECT_REGEX = /Your .+ order has been received!/;
 const DRAFT_MARKER = 'WOOPLUG6171_DRAFT_ONLY_MARKER';
+const PRODUCT_NAME = 'WOOPLUG7795 Email product';
 const EMAIL_POST_MAPPING_OPTION =
 	'woocommerce_email_templates_customer_processing_order_post_id';

@@ -61,6 +67,47 @@ const deleteEmailTypePosts = async ( baseURL: string ) => {
 };

 const orderIds: number[] = [];
+let productId: number;
+
+/**
+ * Block markup for a product collection listing just the given product.
+ *
+ * @param {number} handPickedProductId The product to list.
+ * @return {string} The serialized block markup.
+ */
+const productCollectionBlock = ( handPickedProductId: number ) => {
+	const attributes = {
+		queryId: 0,
+		query: {
+			perPage: 1,
+			pages: 1,
+			offset: 0,
+			postType: 'product',
+			order: 'asc',
+			orderBy: 'title',
+			search: '',
+			exclude: [],
+			inherit: false,
+			taxQuery: {},
+			isProductCollectionBlock: true,
+			woocommerceOnSale: false,
+			woocommerceStockStatus: [ 'instock', 'outofstock', 'onbackorder' ],
+			woocommerceAttributes: [],
+			woocommerceHandPickedProducts: [ String( handPickedProductId ) ],
+		},
+		tagName: 'div',
+		displayLayout: { type: 'flex', columns: 1 },
+		collection: 'woocommerce/product-collection/hand-picked',
+	};
+
+	return `<!-- wp:woocommerce/product-collection ${ JSON.stringify(
+		attributes
+	) } -->
+<div class="wp-block-woocommerce-product-collection"><!-- wp:woocommerce/product-template -->
+<!-- wp:post-title {"isLink":true,"__woocommerceNamespace":"woocommerce/product-collection/product-title"} /-->
+<!-- /wp:woocommerce/product-template --></div>
+<!-- /wp:woocommerce/product-collection -->`;
+};

 test.beforeAll( async ( { baseURL } ) => {
 	await setOption(
@@ -75,6 +122,17 @@ test.beforeAll( async ( { baseURL } ) => {
 	// after an environment restore reuses the numeric ID. Remove this email
 	// type's mapping so the real Edit action creates a fresh `woo_email` draft.
 	await deleteOption( request, baseURL, EMAIL_POST_MAPPING_OPTION );
+
+	const productResponse = await createAdminApiClient( baseURL ).post(
+		`${ WC_API_PATH }/products`,
+		{
+			name: PRODUCT_NAME,
+			type: 'simple',
+			regular_price: '10',
+			status: 'publish',
+		}
+	);
+	productId = productResponse.data.id;
 } );

 test.afterAll( async ( { baseURL } ) => {
@@ -84,6 +142,11 @@ test.afterAll( async ( { baseURL } ) => {
 			force: true,
 		} );
 	}
+	if ( productId ) {
+		await apiClient.delete( `${ WC_API_PATH }/products/${ productId }`, {
+			force: true,
+		} );
+	}
 	// Delete posts while the feature is still enabled so the
 	// `before_delete_post` hook also clears the email type → post mapping.
 	await deleteEmailTypePosts( baseURL );
@@ -150,7 +213,11 @@ test(
 		expect( draft ).toBeTruthy();
 		expect( Number.isSafeInteger( draft.id ) && draft.id > 0 ).toBe( true );
 		await restApi.post( `${ WP_API_PATH }/woo_email/${ draft.id }`, {
-			content: `${ draft.content.raw }\n<!-- wp:paragraph --><p>${ DRAFT_MARKER }</p><!-- /wp:paragraph -->`,
+			content: `${
+				draft.content.raw
+			}\n<!-- wp:paragraph --><p>${ DRAFT_MARKER }</p><!-- /wp:paragraph -->\n${ productCollectionBlock(
+				productId
+			) }`,
 		} );

 		// The real listing must reopen that same edited scratchpad before Save.
@@ -193,5 +260,11 @@ test(
 		await expect( emailBody.locator( 'body' ) ).toContainText(
 			DRAFT_MARKER
 		);
+		// The order was created through the REST API, which skips registering
+		// WooCommerce's block types. Without the on-demand registration the
+		// product collection renders empty in the sent email.
+		await expect( emailBody.locator( 'body' ) ).toContainText(
+			PRODUCT_NAME
+		);
 	}
 );