Commit aaa282deb36 for woocommerce

commit aaa282deb36b8794f1087af5bff6ac2e86afb51d
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Thu Oct 22 14:47:14 2026 +0300

    Run 6 Blocks e2e Single Product template specs in parallel (#70040)

    Each spec copies a sample product and saves a template for that copy
    only, so it no longer changes the product pages other workers read.

diff --git a/plugins/woocommerce/changelog/dev-testops-306-blocks-e2e-product-templates b/plugins/woocommerce/changelog/dev-testops-306-blocks-e2e-product-templates
new file mode 100644
index 00000000000..3565c13ab57
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-testops-306-blocks-e2e-product-templates
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Move 6 Blocks e2e specs that save the Single Product template to the parallel pool; tests only.
diff --git a/plugins/woocommerce/tests/e2e/playwright.config.ts b/plugins/woocommerce/tests/e2e/playwright.config.ts
index be4601da9c3..8d3e773c9c5 100644
--- a/plugins/woocommerce/tests/e2e/playwright.config.ts
+++ b/plugins/woocommerce/tests/e2e/playwright.config.ts
@@ -208,7 +208,6 @@ const blocksSerialSpecs = [
 	// archive, header, cart, checkout), or the
 	// `wc_blocks_use_blockified_product_grid_block_as_template` option. Every
 	// spec that visits those pages reads them.
-	'**/tests/blocks/add-to-cart-form/add-to-cart-form.block_theme.spec.ts',
 	'**/tests/blocks/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts',
 	'**/tests/blocks/all-products/all-products.block_theme.spec.ts',
 	'**/tests/blocks/attributes-filter/attribute-filter.block_theme.spec.ts',
@@ -217,19 +216,14 @@ const blocksSerialSpecs = [
 	'**/tests/blocks/classic-template/classic-template.block_theme.spec.ts',
 	'**/tests/blocks/mini-cart/mini-cart-block.shopper.block_theme.spec.ts',
 	'**/tests/blocks/mini-cart/mini-cart.block_theme.spec.ts',
-	'**/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts',
 	'**/tests/blocks/page-content-wrapper/page-content-wrapper.block_theme.spec.ts',
 	'**/tests/blocks/price-filter/price-filter.block_theme.spec.ts',
 	'**/tests/blocks/product-collection/product-collection.block_theme.spec.ts',
 	'**/tests/blocks/product-filters/active-filter-frontend.block_theme.spec.ts',
 	'**/tests/blocks/product-filters/attribute-filter-frontend.block_theme.spec.ts',
 	'**/tests/blocks/product-filters/product-filters-frontend.block_theme.spec.ts',
-	'**/tests/blocks/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.spec.ts',
-	'**/tests/blocks/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts',
-	'**/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts',
 	'**/tests/blocks/products/products.block_theme.spec.ts',
 	'**/tests/blocks/rating-filter/rating-filter.block_theme.spec.ts',
-	'**/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts',
 	'**/tests/blocks/stock-filter/stock-filter.block_theme.spec.ts',
 	'**/tests/blocks/templates/legacy-templates.block_theme.spec.ts',

diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/add-to-cart-form/add-to-cart-form.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/add-to-cart-form/add-to-cart-form.block_theme.spec.ts
index f031fb1317f..47772ebfd53 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/add-to-cart-form/add-to-cart-form.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/add-to-cart-form/add-to-cart-form.block_theme.spec.ts
@@ -1,8 +1,9 @@
 /**
  * External dependencies
  */
+import { randomUUID } from 'crypto';
 import { Page } from '@playwright/test';
-import { test as base, expect, Editor, wpCLI } from '@woocommerce/e2e-utils';
+import { test as base, expect, Editor } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -73,24 +74,47 @@ class BlockUtils {
 		await ( await this.editor.getBlockByName( blockData.slug ) ).click();
 		await this.page.getByLabel( 'Stepper' ).click();
 	}
-
-	async createSoldIndividuallyProduct() {
-		await wpCLI(
-			'wc product create --name="Sold Individually" --regular_price=10 --sold_individually=true --user=admin'
-		);
-	}
-
-	async createManagedStockProduct() {
-		await wpCLI(
-			'wc product create --name="Managed Stock" --regular_price=10 --manage_stock=true --stock_quantity=1 --user=admin'
-		);
-	}
 }

-const test = base.extend< { blockUtils: BlockUtils } >( {
+const test = base.extend< {
+	blockUtils: BlockUtils;
+	createHiddenProduct: (
+		name: string,
+		data: Record< string, unknown >
+	) => Promise< string >;
+} >( {
 	blockUtils: async ( { editor, page }, use ) => {
 		await use( new BlockUtils( { editor, page } ) );
 	},
+	createHiddenProduct: async ( { requestUtils }, use ) => {
+		const productIds: number[] = [];
+
+		await use( async ( name, data ) => {
+			const uniqueName = `${ name } ${ randomUUID().slice( 0, 8 ) }`;
+			const { id } = await requestUtils.rest< { id: number } >( {
+				method: 'POST',
+				path: 'wc/v3/products',
+				data: {
+					name: uniqueName,
+					regular_price: '10',
+					catalog_visibility: 'hidden',
+					...data,
+				},
+			} );
+			productIds.push( id );
+			return uniqueName;
+		} );
+
+		await Promise.allSettled(
+			productIds.map( ( id ) =>
+				requestUtils.rest( {
+					method: 'DELETE',
+					path: `wc/v3/products/${ id }`,
+					params: { force: true },
+				} )
+			)
+		);
+	},
 } );

 test.describe( `${ blockData.name } Block`, () => {
@@ -127,47 +151,47 @@ test.describe( `${ blockData.name } Block`, () => {
 		// inner block of the Single Product Block except for the Single Product
 		// Template
 		const template = await requestUtils.createTemplate( 'wp_template', {
-			slug: 'product-catalog',
-			title: 'Custom Product Catalog',
+			slug: `add-to-cart-form-test-${ test.info().repeatEachIndex }`,
+			title: `Add to Cart Form Test ${ test.info().repeatEachIndex }`,
 			content: 'placeholder',
 		} );

-		await admin.visitSiteEditor( {
-			postId: template.id,
-			postType: 'wp_template',
-			canvas: 'edit',
-		} );
-
-		await expect(
-			editor.getCustomHtmlBlockContentLocator( 'placeholder' )
-		).toBeVisible();
+		try {
+			await admin.visitSiteEditor( {
+				postId: template.id,
+				postType: 'wp_template',
+				canvas: 'edit',
+			} );

-		await editor.insertBlock( { name: 'woocommerce/single-product' } );
+			await expect(
+				editor.getCustomHtmlBlockContentLocator( 'placeholder' )
+			).toBeVisible();

-		await blockUtils.configureSingleProductBlock();
+			await editor.insertBlock( { name: 'woocommerce/single-product' } );

-		await expect(
-			await editor.getBlockByName( blockData.slug )
-		).toBeVisible();
+			await blockUtils.configureSingleProductBlock();

-		// When the block is registered as ancestor, the function doesn't throw an error, but the block is not added.
-		// So we check that only one instance of the block is present.
-		await editor.insertBlock( { name: blockData.slug } );
-		await expect(
-			await editor.getBlockByName( blockData.slug )
-		).toBeVisible();
+			await expect(
+				await editor.getBlockByName( blockData.slug )
+			).toBeVisible();
+
+			// When the block is registered as ancestor, the function doesn't throw an error, but the block is not added.
+			// So we check that only one instance of the block is present.
+			await editor.insertBlock( { name: blockData.slug } );
+			await expect(
+				await editor.getBlockByName( blockData.slug )
+			).toBeVisible();
+		} finally {
+			await requestUtils.revertTemplate( 'wp_template', template.id );
+		}
 	} );

 	test( 'can be added in the Post Editor - Single Product Template', async ( {
 		admin,
 		editor,
-		requestUtils,
+		createProductTemplate,
 	} ) => {
-		const template = await requestUtils.createTemplate( 'wp_template', {
-			slug: 'single-product',
-			title: 'Custom Single Product',
-			content: 'placeholder',
-		} );
+		const { template } = await createProductTemplate( 'beanie' );

 		await admin.visitSiteEditor( {
 			postId: template.id,
@@ -259,13 +283,15 @@ test.describe( `${ blockData.name } Block`, () => {
 			editor,
 			blockUtils,
 			page,
+			createHiddenProduct,
 		} ) => {
-			await blockUtils.createSoldIndividuallyProduct();
+			const productName = await createHiddenProduct(
+				'Sold Individually',
+				{ sold_individually: true }
+			);
 			await admin.createNewPost();
 			await editor.insertBlock( { name: 'woocommerce/single-product' } );

-			const productName = 'Sold Individually';
-
 			await blockUtils.configureSingleProductBlock( productName );
 			await blockUtils.enableStepperMode();

@@ -283,13 +309,15 @@ test.describe( `${ blockData.name } Block`, () => {
 			editor,
 			blockUtils,
 			page,
+			createHiddenProduct,
 		} ) => {
-			await blockUtils.createManagedStockProduct();
+			const productName = await createHiddenProduct( 'Managed Stock', {
+				manage_stock: true,
+				stock_quantity: 1,
+			} );
 			await admin.createNewPost();
 			await editor.insertBlock( { name: 'woocommerce/single-product' } );

-			const productName = 'Managed Stock';
-
 			await blockUtils.configureSingleProductBlock( productName );
 			await blockUtils.enableStepperMode();

diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts
index 42fbf71a8ce..d49406c58c7 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/on-sale-badge/on-sale-badge-single-product-template.block_theme.spec.ts
@@ -2,14 +2,18 @@
  * External dependencies
  */
 import type { FrameLocator, Page } from '@playwright/test';
-import { test, expect, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';
+import { test as base, expect, ProductTemplate } from '@woocommerce/e2e-utils';

 const blockData = {
 	name: 'woocommerce/product-sale-badge',
-	slug: 'single-product',
-	productPage: '/product/hoodie/',
 };

+const test = base.extend< { hoodieTemplate: ProductTemplate } >( {
+	hoodieTemplate: async ( { createProductTemplate }, use ) => {
+		await use( await createProductTemplate( 'hoodie' ) );
+	},
+} );
+
 const badgeSelector = '.wc-block-components-product-sale-badge';
 const badgeContainerSelector = '.wp-block-woocommerce-product-sale-badge';

@@ -55,9 +59,9 @@ const getAlignmentDelta = async (

 test.describe( `${ blockData.name }`, () => {
 	test.describe( `On the Single Product Template`, () => {
-		test.beforeEach( async ( { admin, editor } ) => {
+		test.beforeEach( async ( { admin, editor, hoodieTemplate } ) => {
 			await admin.visitSiteEditor( {
-				postId: `${ BLOCK_THEME_SLUG }//${ blockData.slug }`,
+				postId: hoodieTemplate.template.id,
 				postType: 'wp_template',
 				canvas: 'edit',
 			} );
@@ -67,6 +71,7 @@ test.describe( `${ blockData.name }`, () => {
 		test( 'renders and aligns the sale badge in editor and frontend', async ( {
 			editor,
 			page,
+			hoodieTemplate,
 		} ) => {
 			const context = page.context();
 			const baselinePageCount = context.pages().length;
@@ -96,7 +101,7 @@ test.describe( `${ blockData.name }`, () => {
 					isOnlyCurrentEntityDirty: true,
 				} );

-				await frontendPage.goto( blockData.productPage );
+				await frontendPage.goto( hoodieTemplate.productUrl );
 				await expect(
 					frontendPage.locator( badgeSelector ).first()
 				).toBeVisible();
@@ -124,7 +129,7 @@ test.describe( `${ blockData.name }`, () => {
 						isOnlyCurrentEntityDirty: true,
 					} );

-					await frontendPage.goto( blockData.productPage );
+					await frontendPage.goto( hoodieTemplate.productUrl );
 					await expect(
 						frontendPage.locator( badgeSelector ).first()
 					).toBeVisible();
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.spec.ts
index a3b6ecd3f79..60f76323534 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-large-image/product-gallery-large-image.block_theme.spec.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { test as base, expect, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';
+import { test as base, expect, ProductTemplate } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -14,11 +14,13 @@ const blockData = {
 		frontend: {},
 		editor: {},
 	},
-	slug: 'single-product',
-	productPage: '/product/hoodie/',
 };

-const test = base.extend< { pageObject: ProductGalleryPage } >( {
+const test = base.extend< {
+	pageObject: ProductGalleryPage;
+	sourceProductSlug: string;
+	productTemplate: ProductTemplate;
+} >( {
 	pageObject: async ( { page, editor, frontendUtils }, use ) => {
 		const pageObject = new ProductGalleryPage( {
 			page,
@@ -27,12 +29,19 @@ const test = base.extend< { pageObject: ProductGalleryPage } >( {
 		} );
 		await use( pageObject );
 	},
+	sourceProductSlug: [ 'hoodie', { option: true } ],
+	productTemplate: async (
+		{ createProductTemplate, sourceProductSlug },
+		use
+	) => {
+		await use( await createProductTemplate( sourceProductSlug ) );
+	},
 } );

 test.describe( `${ blockData.name }`, () => {
-	test.beforeEach( async ( { admin, editor } ) => {
+	test.beforeEach( async ( { admin, editor, productTemplate } ) => {
 		await admin.visitSiteEditor( {
-			postId: `${ BLOCK_THEME_SLUG }//${ blockData.slug }`,
+			postId: productTemplate.template.id,
 			postType: 'wp_template',
 			canvas: 'edit',
 		} );
@@ -52,6 +61,7 @@ test.describe( `${ blockData.name }`, () => {
 			pageObject,
 			editor,
 			page,
+			productTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );
 			await pageObject.toggleZoomWhileHoveringSetting( true );
@@ -59,7 +69,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( productTemplate.productUrl );

 			const viewerBlock = await pageObject.getViewerBlock( {
 				page: 'frontend',
@@ -106,6 +116,7 @@ test.describe( `${ blockData.name }`, () => {
 			pageObject,
 			editor,
 			page,
+			productTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );
 			await pageObject.toggleZoomWhileHoveringSetting( false );
@@ -117,7 +128,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( productTemplate.productUrl );

 			const viewerBlock = await pageObject.getViewerBlock( {
 				page: 'frontend',
@@ -142,6 +153,7 @@ test.describe( `${ blockData.name }`, () => {
 		page,
 		editor,
 		pageObject,
+		productTemplate,
 	} ) => {
 		await pageObject.addProductGalleryBlock( { cleanContent: true } );
 		await pageObject.addClassicAddToCartFormBlock();
@@ -155,7 +167,7 @@ test.describe( `${ blockData.name }`, () => {
 			isOnlyCurrentEntityDirty: true,
 		} );

-		await page.goto( blockData.productPage );
+		await page.goto( productTemplate.productUrl );

 		// Initial state: only the parent's featured image is visible.
 		const featuredImageId = await pageObject.getViewerImageId();
@@ -189,6 +201,7 @@ test.describe( `${ blockData.name }`, () => {
 		page,
 		editor,
 		pageObject,
+		productTemplate,
 	} ) => {
 		await pageObject.addProductGalleryBlock( { cleanContent: true } );
 		await pageObject.addClassicAddToCartFormBlock();
@@ -197,7 +210,7 @@ test.describe( `${ blockData.name }`, () => {
 			isOnlyCurrentEntityDirty: true,
 		} );

-		await page.goto( blockData.productPage );
+		await page.goto( productTemplate.productUrl );
 		const featuredImageId = await pageObject.getViewerImageId();
 		expect( featuredImageId ).not.toBeNull();
 		const parentImageIds = await pageObject.getVisibleViewerImageIds();
@@ -272,23 +285,28 @@ test.describe( `${ blockData.name }`, () => {

 	test.describe( 'Frontend navigation', () => {
 		test.use( {
+			sourceProductSlug: 'beanie',
 			hasTouch: true,
 			contextOptions: { reducedMotion: 'no-preference' },
 		} );

-		test.beforeEach( async ( { pageObject, editor, page } ) => {
-			await pageObject.addProductGalleryBlock( { cleanContent: true } );
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
+		test.beforeEach(
+			async ( { pageObject, editor, page, productTemplate } ) => {
+				await pageObject.addProductGalleryBlock( {
+					cleanContent: true,
+				} );
+				await editor.saveSiteEditorEntities( {
+					isOnlyCurrentEntityDirty: true,
+				} );

-			await page.goto( '/product/beanie/' );
+				await page.goto( productTemplate.productUrl );

-			await page.setViewportSize( {
-				height: 667,
-				width: 390, // iPhone 12 Pro
-			} );
-		} );
+				await page.setViewportSize( {
+					height: 667,
+					width: 390, // iPhone 12 Pro
+				} );
+			}
+		);

 		test( 'updates thumbnail selection during a native swipe', async ( {
 			pageObject,
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts
index 298db9e5604..ce037faf21a 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts
@@ -1,18 +1,18 @@
 /**
  * External dependencies
  */
-import { test, expect } from '@woocommerce/e2e-utils';
+import { test as base, expect, ProductTemplate } from '@woocommerce/e2e-utils';

-test.describe( 'Product Gallery Thumbnails block', () => {
-	test.beforeEach( async ( { admin, editor, requestUtils } ) => {
-		const template = await requestUtils.createTemplate( 'wp_template', {
-			slug: 'single-product',
-			title: 'Custom Single Product',
-			content: 'placeholder',
-		} );
+const test = base.extend< { beanieTemplate: ProductTemplate } >( {
+	beanieTemplate: async ( { createProductTemplate }, use ) => {
+		await use( await createProductTemplate( 'beanie' ) );
+	},
+} );

+test.describe( 'Product Gallery Thumbnails block', () => {
+	test.beforeEach( async ( { admin, editor, beanieTemplate } ) => {
 		await admin.visitSiteEditor( {
-			postId: template.id,
+			postId: beanieTemplate.template.id,
 			postType: 'wp_template',
 			canvas: 'edit',
 		} );
@@ -30,7 +30,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 		} );
 	} );

-	test( 'renders as expected', async ( { page, editor } ) => {
+	test( 'renders as expected', async ( { page, editor, beanieTemplate } ) => {
 		await test.step( 'in editor', async () => {
 			const productGalleryBlock = editor.canvas.locator(
 				'[data-type="woocommerce/product-gallery"]'
@@ -52,7 +52,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 		} );

 		await test.step( 'in frontend', async () => {
-			await page.goto( '/product/beanie/' );
+			await page.goto( beanieTemplate.productUrl );

 			const productGalleryBlock = page.locator(
 				'[data-block-name="woocommerce/product-gallery"]'
@@ -83,6 +83,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 	test( 'thumbnail size settings work correctly', async ( {
 		page,
 		editor,
+		beanieTemplate,
 	} ) => {
 		await test.step( 'in editor', async () => {
 			const viewerBlock = editor.canvas.locator(
@@ -125,7 +126,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 		} );

 		await test.step( 'in frontend', async () => {
-			await page.goto( '/product/beanie/' );
+			await page.goto( beanieTemplate.productUrl );

 			const thumbnailsBlock = page.locator(
 				'[data-block-name="woocommerce/product-gallery-thumbnails"]'
@@ -150,6 +151,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 	test( 'thumbnails are scrollable and last thumbnail is reachable', async ( {
 		page,
 		editor,
+		beanieTemplate,
 	} ) => {
 		await test.step( 'in editor', async () => {
 			const viewerBlock = editor.canvas.locator(
@@ -183,7 +185,7 @@ test.describe( 'Product Gallery Thumbnails block', () => {
 		} );

 		await test.step( 'in frontend', async () => {
-			await page.goto( '/product/beanie/' );
+			await page.goto( beanieTemplate.productUrl );

 			const thumbnailsContainer = page.locator(
 				'[data-block-name="woocommerce/product-gallery-thumbnails"]'
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts
index 3329c3a3bc4..d1bcdb10dc0 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-gallery/product-gallery.block_theme.spec.ts
@@ -2,7 +2,7 @@
  * External dependencies
  */
 import { Locator } from '@playwright/test';
-import { test as base, expect } from '@woocommerce/e2e-utils';
+import { test as base, expect, ProductTemplate } from '@woocommerce/e2e-utils';

 /**
  * Internal dependencies
@@ -12,11 +12,12 @@ import { ProductGalleryPage } from './product-gallery.page';
 const blockData = {
 	name: 'woocommerce/product-gallery',
 	title: 'Product Gallery',
-	slug: 'single-product',
-	productPage: '/product/beanie/',
 };

-const test = base.extend< { pageObject: ProductGalleryPage } >( {
+const test = base.extend< {
+	pageObject: ProductGalleryPage;
+	beanieTemplate: ProductTemplate;
+} >( {
 	pageObject: async ( { page, editor, frontendUtils }, use ) => {
 		const pageObject = new ProductGalleryPage( {
 			page,
@@ -25,6 +26,9 @@ const test = base.extend< { pageObject: ProductGalleryPage } >( {
 		} );
 		await use( pageObject );
 	},
+	beanieTemplate: async ( { createProductTemplate }, use ) => {
+		await use( await createProductTemplate( 'beanie' ) );
+	},
 } );

 /**
@@ -67,15 +71,9 @@ const getThumbnailImageIdByNth = async (
 };

 test.describe( `${ blockData.name }`, () => {
-	test.beforeEach( async ( { admin, editor, requestUtils } ) => {
-		const template = await requestUtils.createTemplate( 'wp_template', {
-			slug: blockData.slug,
-			title: 'Custom Single Product',
-			content: 'placeholder',
-		} );
-
+	test.beforeEach( async ( { admin, editor, beanieTemplate } ) => {
 		await admin.visitSiteEditor( {
-			postId: template.id,
+			postId: beanieTemplate.template.id,
 			postType: 'wp_template',
 			canvas: 'edit',
 		} );
@@ -90,6 +88,7 @@ test.describe( `${ blockData.name }`, () => {
 			page,
 			editor,
 			pageObject,
+			beanieTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );

@@ -97,7 +96,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( beanieTemplate.productUrl );

 			const thumbnailsBlock = await pageObject.getThumbnailsBlock( {
 				page: 'frontend',
@@ -141,6 +140,7 @@ test.describe( `${ blockData.name }`, () => {
 			page,
 			editor,
 			pageObject,
+			beanieTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );

@@ -148,7 +148,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( beanieTemplate.productUrl );

 			const initialViewerImageId = await pageObject.getViewerImageId();

@@ -195,6 +195,7 @@ test.describe( `${ blockData.name }`, () => {
 			page,
 			editor,
 			pageObject,
+			beanieTemplate,
 		} ) => {
 			await page.setViewportSize( { width: 800, height: 800 } );

@@ -204,7 +205,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( beanieTemplate.productUrl );

 			const initialViewerImageId = await pageObject.getViewerImageId();

@@ -260,6 +261,7 @@ test.describe( `${ blockData.name }`, () => {
 			pageObject,
 			page,
 			editor,
+			beanieTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );
 			await editor.openDocumentSettingsSidebar();
@@ -270,7 +272,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( beanieTemplate.productUrl );

 			const viewerBlock = await pageObject.getViewerBlock( {
 				page: 'frontend',
@@ -287,6 +289,7 @@ test.describe( `${ blockData.name }`, () => {
 			pageObject,
 			page,
 			editor,
+			beanieTemplate,
 		} ) => {
 			await pageObject.addProductGalleryBlock( { cleanContent: true } );
 			await editor.openDocumentSettingsSidebar();
@@ -295,7 +298,7 @@ test.describe( `${ blockData.name }`, () => {
 				isOnlyCurrentEntityDirty: true,
 			} );

-			await page.goto( blockData.productPage );
+			await page.goto( beanieTemplate.productUrl );

 			await expect( page.locator( 'dialog' ) ).toBeHidden();

@@ -359,6 +362,7 @@ test.describe( `${ blockData.name }`, () => {
 		editor,
 		pageObject,
 		page,
+		beanieTemplate,
 	} ) => {
 		await pageObject.addProductGalleryBlock( { cleanContent: true } );
 		await editor.saveSiteEditorEntities( {
@@ -373,7 +377,9 @@ test.describe( `${ blockData.name }`, () => {
 		await page.getByLabel( 'Open Navigation' ).click();

 		await page
-			.getByRole( 'button', { name: 'Custom Single Product' } )
+			.getByRole( 'button', {
+				name: beanieTemplate.template.title.rendered,
+			} )
 			.first()
 			.click();

@@ -388,13 +394,14 @@ test.describe( `${ blockData.name }`, () => {
 		page,
 		pageObject,
 		editor,
+		beanieTemplate,
 	} ) => {
 		await pageObject.addProductGalleryBlock( { cleanContent: true } );
 		await editor.saveSiteEditorEntities( {
 			isOnlyCurrentEntityDirty: true,
 		} );

-		await page.goto( blockData.productPage );
+		await page.goto( beanieTemplate.productUrl );

 		await page.setViewportSize( {
 			height: 667,
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
index 99c3f7660f6..472697d8347 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/single-product-template/single-product-template-compatibility-layer.spec.ts
@@ -182,10 +182,20 @@ test.describe( 'Compatibility Layer in Single Product template', () => {
 		page,
 		admin,
 		editor,
+		requestUtils,
+		createProductTemplate,
 	} ) => {
+		const singleProductTemplate = await requestUtils.getTemplate(
+			'wp_template',
+			`${ BLOCK_THEME_SLUG }//single-product`
+		);
+		const capTemplate = await createProductTemplate( 'cap', {
+			content: singleProductTemplate.content.raw,
+		} );
+
 		/* Switch to the blockified Add to Cart + Options block to be able to test all hooks */
 		await admin.visitSiteEditor( {
-			postId: `${ BLOCK_THEME_SLUG }//single-product`,
+			postId: capTemplate.template.id,
 			postType: 'wp_template',
 			canvas: 'edit',
 		} );
@@ -207,7 +217,15 @@ test.describe( 'Compatibility Layer in Single Product template', () => {
 			isOnlyCurrentEntityDirty: true,
 		} );

-		await page.goto( '/product/cap/' );
+		const { content } = await requestUtils.getTemplate(
+			'wp_template',
+			capTemplate.template.id
+		);
+		const hoodieTemplate = await createProductTemplate( 'hoodie', {
+			content: content.raw,
+		} );
+
+		await page.goto( capTemplate.productUrl );

 		for ( const scenario of simpleProductAddToCartWithOptionsBlockHooks ) {
 			const hooks = page.getByTestId( scenario.dataTestId );
@@ -222,7 +240,7 @@ test.describe( 'Compatibility Layer in Single Product template', () => {
 			).toHaveText( scenario.content );
 		}

-		await page.goto( '/product/hoodie/' );
+		await page.goto( hoodieTemplate.productUrl );

 		for ( const scenario of variableProductAddToCartWithOptionsBlockHooks ) {
 			const hooks = page.getByTestId( scenario.dataTestId );
diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/index.ts b/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/index.ts
index c8ba4a883aa..7fe300e2727 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/index.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/index.ts
@@ -14,6 +14,7 @@ import {
 	revertTemplate,
 	createTemplateFromFile,
 	TemplateCompiler,
+	WPTemplate,
 } from './templates';
 import { resetFeatureFlag, setFeatureFlag } from './feature-flag';

@@ -39,4 +40,4 @@ export class RequestUtils extends CoreRequestUtils {
 	resetFeatureFlag: typeof resetFeatureFlag = resetFeatureFlag.bind( this );
 }

-export { TemplateCompiler, PostCompiler };
+export { TemplateCompiler, PostCompiler, WPTemplate };
diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/templates.ts b/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/templates.ts
index e7d61ba3df2..78fd87fb318 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/templates.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/request-utils/templates.ts
@@ -22,6 +22,10 @@ export interface WPTemplate {
 	slug: string;
 	type: WPTemplateType;
 	origin: 'plugin' | 'theme' | null;
+	title: {
+		raw: string;
+		rendered: string;
+	};
 	content: {
 		raw: string;
 	};
diff --git a/plugins/woocommerce/tests/e2e/utils/blocks/test.ts b/plugins/woocommerce/tests/e2e/utils/blocks/test.ts
index d13531aa7ac..9c16b364215 100644
--- a/plugins/woocommerce/tests/e2e/utils/blocks/test.ts
+++ b/plugins/woocommerce/tests/e2e/utils/blocks/test.ts
@@ -2,6 +2,7 @@
 /**
  * External dependencies
  */
+import { randomUUID } from 'crypto';
 import { test as base, expect, ConsoleMessage } from '@playwright/test';
 import {
 	STORAGE_STATE_PATH,
@@ -16,6 +17,7 @@ import {
 	PerformanceUtils,
 	RequestUtils,
 	ShippingUtils,
+	WPTemplate,
 } from '@woocommerce/e2e-utils';

 /**
@@ -106,6 +108,20 @@ function observeConsoleLogging( message: ConsoleMessage ) {
 	console[ logFunction ]( text );
 }

+export type ProductTemplate = { template: WPTemplate; productUrl: string };
+
+type WCProduct = {
+	id: number;
+	name: string;
+	type: string;
+	regular_price: string;
+	sale_price: string;
+	attributes: unknown[];
+	default_attributes: unknown[];
+	images: { id: number }[];
+	image?: { id: number } | null;
+};
+
 const test = base.extend<
 	{
 		admin: Admin;
@@ -118,6 +134,10 @@ const test = base.extend<
 		localPickupUtils: LocalPickupUtils;
 		miniCartUtils: MiniCartUtils;
 		resetDatabaseAfterEachTest: boolean;
+		createProductTemplate: (
+			sourceProductSlug: string,
+			options?: { content?: string }
+		) => Promise< ProductTemplate >;
 	},
 	{
 		requestUtils: RequestUtils;
@@ -191,6 +211,97 @@ const test = base.extend<
 	miniCartUtils: async ( { page, frontendUtils }, use ) => {
 		await use( new MiniCartUtils( page, frontendUtils ) );
 	},
+	// Copies a sample product and creates a `single-product-<slug>` template
+	// for the copy, so a spec can save a Single Product template without
+	// changing the pages other workers visit. The copy is hidden from the
+	// catalog. Both are deleted after the test.
+	createProductTemplate: async ( { requestUtils }, use ) => {
+		const productIds: number[] = [];
+		const templateIds: string[] = [];
+
+		await use( async ( sourceProductSlug, options = {} ) => {
+			const [ source ] = await requestUtils.rest< WCProduct[] >( {
+				path: 'wc/v3/products',
+				params: { slug: sourceProductSlug },
+			} );
+			if ( ! source ) {
+				throw new Error(
+					`No product with slug ${ sourceProductSlug }`
+				);
+			}
+			const suffix = randomUUID().slice( 0, 8 );
+			// Copied field by field rather than with the duplicate endpoint, and
+			// with a random slug: concurrent duplicates generate the same
+			// "unique" SKU or slug and fail. The name leaves out the source name,
+			// so specs that pick "Cap" or "Hoodie" by name never match a copy.
+			const product = await requestUtils.rest<
+				WCProduct & { slug: string; permalink: string }
+			>( {
+				method: 'POST',
+				path: 'wc/v3/products',
+				data: {
+					name: `E2E copy ${ suffix }`,
+					slug: `${ sourceProductSlug }-${ suffix }`,
+					type: source.type,
+					regular_price: source.regular_price,
+					sale_price: source.sale_price,
+					attributes: source.attributes,
+					default_attributes: source.default_attributes,
+					images: source.images.map( ( { id } ) => ( { id } ) ),
+					catalog_visibility: 'hidden',
+				},
+			} );
+			productIds.push( product.id );
+
+			if ( source.type === 'variable' ) {
+				const variations = await requestUtils.rest< WCProduct[] >( {
+					path: `wc/v3/products/${ source.id }/variations`,
+					params: { per_page: 100 },
+				} );
+				await requestUtils.rest( {
+					method: 'POST',
+					path: `wc/v3/products/${ product.id }/variations/batch`,
+					data: {
+						create: variations.map( ( variation ) => ( {
+							regular_price: variation.regular_price,
+							sale_price: variation.sale_price,
+							attributes: variation.attributes,
+							image: variation.image && {
+								id: variation.image.id,
+							},
+						} ) ),
+					},
+				} );
+			}
+
+			const template = ( await requestUtils.createTemplate(
+				'wp_template',
+				{
+					slug: `single-product-${ product.slug }`,
+					title: `Single Product ${ product.slug }`,
+					content: options.content ?? 'placeholder',
+				}
+			) ) as WPTemplate;
+			templateIds.push( template.id );
+
+			return { template, productUrl: product.permalink };
+		} );
+
+		await Promise.allSettled(
+			templateIds.map( ( templateId ) =>
+				requestUtils.revertTemplate( 'wp_template', templateId )
+			)
+		);
+		await Promise.allSettled(
+			productIds.map( ( productId ) =>
+				requestUtils.rest( {
+					method: 'DELETE',
+					path: `wc/v3/products/${ productId }`,
+					params: { force: true },
+				} )
+			)
+		);
+	},
 	requestUtils: [
 		async ( {}, use, workerInfo ) => {
 			const requestUtils = await RequestUtils.setup( {