Commit 35d860b774d for woocommerce

commit 35d860b774d27a1ed06b0a15a21c5a770686d4cd
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Sep 14 17:21:03 2026 +0300

    [tests] Reduce On-Sale Badge E2E tests from 6 to 1 (#68594)

    test(blocks): Reduce On-Sale Badge E2E tests from 6 to 1

    The On-Sale Badge spec ran six browser titles on the Single Product
    template. Two checked that the badge renders, one that it is absent
    for a product that is not on sale, and three its left, center, and
    right alignment, each one saving the template again.

    Whether the badge renders and which alignment class it gets are both
    decided in ProductSaleBadge::render. Add PHPUnit tests for the
    regular-price case and for the left, center, and right alignment
    classes, and keep one browser title that inserts the badge, saves the
    template, and measures its position in the editor and on the product
    page for all three alignments.

    Consolidates the mega-branch slices:
    - Slice 074: test(blocks): Move Sale Badge behavior below E2E
    - 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>

diff --git a/plugins/woocommerce/changelog/testops-234-on-sale-badge b/plugins/woocommerce/changelog/testops-234-on-sale-badge
new file mode 100644
index 00000000000..1bcc464b0aa
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-234-on-sale-badge
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Reduce On-Sale Badge E2E tests from 6 to 1; PHPUnit owns the sale check and the alignment classes.
+
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 14c9bf92175..42fbf71a8ce 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
@@ -1,93 +1,57 @@
 /**
  * External dependencies
  */
-import {
-	test as base,
-	expect,
-	Editor,
-	FrontendUtils,
-	BLOCK_THEME_SLUG,
-} from '@woocommerce/e2e-utils';
-
-/**
- * Internal dependencies
- */
-import { ProductGalleryPage } from '../product-gallery/product-gallery.page';
+import type { FrameLocator, Page } from '@playwright/test';
+import { test, expect, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';

 const blockData = {
 	name: 'woocommerce/product-sale-badge',
-	mainClass: '.wp-block-woocommerce-product-sale-badge',
-	selectors: {
-		frontend: {
-			badge: '.wc-block-components-product-sale-badge',
-			badgeContainer: '.wp-block-woocommerce-product-sale-badge',
-		},
-		editor: {
-			badge: '.wc-block-components-product-sale-badge',
-			badgeContainer: '.wp-block-woocommerce-product-sale-badge',
-		},
-	},
 	slug: 'single-product',
 	productPage: '/product/hoodie/',
-	productPageNotOnSale: '/product/album/',
 };

-class BlockUtils {
-	editor: Editor;
-	frontendUtils: FrontendUtils;
-
-	constructor( {
-		editor,
-		frontendUtils,
-	}: {
-		editor: Editor;
-		frontendUtils: FrontendUtils;
-	} ) {
-		this.editor = editor;
-		this.frontendUtils = frontendUtils;
-	}
-
-	async getSaleBadgeBoundingClientRect( isFrontend: boolean ): Promise< {
-		badge: DOMRect;
-		badgeContainer: DOMRect;
-	} > {
-		const page = isFrontend ? this.frontendUtils.page : this.editor.canvas;
-		return {
-			badge: await page
-				.locator(
-					blockData.selectors[ isFrontend ? 'frontend' : 'editor' ]
-						.badge
-				)
-				.first()
-				.evaluate( ( el ) => el.getBoundingClientRect() ),
-			badgeContainer: await page
-				.locator(
-					blockData.selectors[ isFrontend ? 'frontend' : 'editor' ]
-						.badgeContainer
-				)
-				.first()
-				.evaluate( ( el ) => el.getBoundingClientRect() ),
-		};
-	}
-}
-
-const test = base.extend< {
-	pageObject: ProductGalleryPage;
-	blockUtils: BlockUtils;
-} >( {
-	pageObject: async ( { page, editor, frontendUtils }, use ) => {
-		await use(
-			new ProductGalleryPage( {
-				page,
-				editor,
-				frontendUtils,
-			} )
+const badgeSelector = '.wc-block-components-product-sale-badge';
+const badgeContainerSelector = '.wp-block-woocommerce-product-sale-badge';
+
+type SaleBadgeAlignment = 'left' | 'center' | 'right';
+
+const getAlignmentDelta = async (
+	root: Page | FrameLocator,
+	alignment: SaleBadgeAlignment
+): Promise< number > =>
+	root
+		.locator( badgeContainerSelector )
+		.first()
+		.evaluate(
+			( container, evaluation ) => {
+				const badge = container.querySelector(
+					evaluation.badgeSelector
+				);
+
+				if ( ! badge ) {
+					return Number.POSITIVE_INFINITY;
+				}
+
+				const badgeRect = badge.getBoundingClientRect();
+				const containerRect = container.getBoundingClientRect();
+
+				if ( evaluation.alignment === 'left' ) {
+					return Math.abs( badgeRect.left - containerRect.left );
+				}
+
+				if ( evaluation.alignment === 'center' ) {
+					const badgeMidpoint =
+						( badgeRect.left + badgeRect.right ) / 2;
+					const containerMidpoint =
+						( containerRect.left + containerRect.right ) / 2;
+
+					return Math.abs( badgeMidpoint - containerMidpoint );
+				}
+
+				return Math.abs( containerRect.right - badgeRect.right );
+			},
+			{ alignment, badgeSelector }
 		);
-	},
-	blockUtils: async ( { editor, frontendUtils }, use ) => {
-		await use( new BlockUtils( { editor, frontendUtils } ) );
-	},
-} );

 test.describe( `${ blockData.name }`, () => {
 	test.describe( `On the Single Product Template`, () => {
@@ -100,195 +64,80 @@ test.describe( `${ blockData.name }`, () => {
 			await editor.setContent( '' );
 		} );

-		test( 'should be rendered on the editor side', async ( { editor } ) => {
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-
-			const block = await editor.getBlockByName( blockData.name );
-
-			await expect( block ).toBeVisible();
-		} );
-
-		test( 'should be rendered on the frontend side', async ( {
-			frontendUtils,
-			editor,
-			page,
-			pageObject,
-		} ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-
-			await pageObject.toggleFullScreenOnClickSetting( false );
-
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
-
-			await page.goto( blockData.productPage );
-
-			const block = await frontendUtils.getBlockByName( blockData.name );
-
-			await expect( block.first() ).toBeVisible();
-		} );
-
-		test( `should not render on the frontend when the product is not on sale`, async ( {
-			frontendUtils,
-			editor,
-			page,
-			pageObject,
-		} ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-
-			await pageObject.toggleFullScreenOnClickSetting( false );
-
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
-
-			await page.goto( blockData.productPageNotOnSale );
-
-			const block = await frontendUtils.getBlockByName( blockData.name );
-
-			await expect( block ).toBeHidden();
-		} );
-
-		test( 'should be aligned to the left', async ( {
-			editor,
-			page,
-			pageObject,
-			blockUtils,
-		} ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-
-			await pageObject.toggleFullScreenOnClickSetting( false );
-
-			const block = await editor.getBlockByName( blockData.name );
-
-			await block.click();
-
-			await page.locator( "button[aria-label='Align']" ).click();
-			await page.getByText( 'Align Left' ).click();
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect(
-							false
-						);
-
-					return badge.x - badgeContainer.x;
-				} )
-				.toEqual( 0 );
-
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
-
-			await page.goto( blockData.productPage );
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect( true );
-
-					return badge.x - badgeContainer.x;
-				} )
-				.toEqual( 0 );
-		} );
-
-		test( 'should be aligned to the center', async ( {
+		test( 'renders and aligns the sale badge in editor and frontend', async ( {
 			editor,
 			page,
-			pageObject,
-			blockUtils,
 		} ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-
-			await pageObject.toggleFullScreenOnClickSetting( false );
-
-			const block = await editor.getBlockByName( blockData.name );
-
-			await block.click();
-
-			await page.locator( "button[aria-label='Align']" ).click();
-			await page.getByText( 'Align Center' ).click();
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect(
-							false
-						);
-
-					return badge.right < badgeContainer.right;
-				} )
-				.toBe( true );
-
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
-
-			await page.goto( blockData.productPage );
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect( true );
-
-					return badge.right < badgeContainer.right;
-				} )
-				.toBe( true );
-		} );
-
-		test( 'should be aligned to the right by default', async ( {
-			editor,
-			page,
-			pageObject,
-			blockUtils,
-		} ) => {
-			await editor.openDocumentSettingsSidebar();
-			await editor.insertBlock( {
-				name: 'woocommerce/product-gallery',
-			} );
-			await pageObject.toggleFullScreenOnClickSetting( false );
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect(
-							false
-						);
-
-					return badgeContainer.right - badge.right;
-				} )
-				.toEqual( 0 );
-
-			await editor.saveSiteEditorEntities( {
-				isOnlyCurrentEntityDirty: true,
-			} );
-
-			await page.goto( blockData.productPage );
-
-			await expect
-				.poll( async () => {
-					const { badge, badgeContainer } =
-						await blockUtils.getSaleBadgeBoundingClientRect( true );
-
-					return badgeContainer.right - badge.right;
-				} )
-				.toEqual( 0 );
+			const context = page.context();
+			const baselinePageCount = context.pages().length;
+			const frontendPage = await context.newPage();
+
+			try {
+				expect( context.pages() ).toHaveLength( baselinePageCount + 1 );
+
+				await editor.openDocumentSettingsSidebar();
+				await editor.insertBlock( {
+					name: 'woocommerce/product-gallery',
+				} );
+				await page
+					.getByRole( 'checkbox', {
+						name: 'Open pop-up when clicked',
+						exact: true,
+					} )
+					.uncheck();
+
+				let block = await editor.getBlockByName( blockData.name );
+				await expect( block ).toBeVisible();
+				await expect
+					.poll( () => getAlignmentDelta( editor.canvas, 'right' ) )
+					.toBeLessThanOrEqual( 1 );
+
+				await editor.saveSiteEditorEntities( {
+					isOnlyCurrentEntityDirty: true,
+				} );
+
+				await frontendPage.goto( blockData.productPage );
+				await expect(
+					frontendPage.locator( badgeSelector ).first()
+				).toBeVisible();
+				await expect
+					.poll( () => getAlignmentDelta( frontendPage, 'right' ) )
+					.toBeLessThanOrEqual( 1 );
+
+				for ( const alignment of [ 'left', 'center' ] as const ) {
+					block = await editor.getBlockByName( blockData.name );
+					await block.click();
+					await page.getByRole( 'button', { name: 'Align' } ).click();
+					await page
+						.getByRole( 'menuitemradio', {
+							name: new RegExp( `Align ${ alignment }`, 'i' ),
+						} )
+						.click();
+
+					await expect
+						.poll( () =>
+							getAlignmentDelta( editor.canvas, alignment )
+						)
+						.toBeLessThanOrEqual( 1 );
+
+					await editor.saveSiteEditorEntities( {
+						isOnlyCurrentEntityDirty: true,
+					} );
+
+					await frontendPage.goto( blockData.productPage );
+					await expect(
+						frontendPage.locator( badgeSelector ).first()
+					).toBeVisible();
+					await expect
+						.poll( () =>
+							getAlignmentDelta( frontendPage, alignment )
+						)
+						.toBeLessThanOrEqual( 1 );
+				}
+			} finally {
+				await frontendPage.close();
+				expect( context.pages() ).toHaveLength( baselinePageCount );
+			}
 		} );
 	} );
 } );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSaleBadge.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSaleBadge.php
index 17c918b570b..b4c58ab1276 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSaleBadge.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductSaleBadge.php
@@ -9,6 +9,95 @@ namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
  */
 class ProductSaleBadge extends \WP_UnitTestCase {

+	/**
+	 * @testdox Product Sale Badge does not render for a regular-price product.
+	 */
+	public function test_product_sale_badge_does_not_render_for_regular_price(): void {
+		global $product;
+
+		$had_product      = array_key_exists( 'product', $GLOBALS );
+		$original_product = $had_product ? $product : null;
+		$product          = new \WC_Product_Simple();
+
+		try {
+			$product->set_name( 'Regular Product' );
+			$product->set_regular_price( '10' );
+			$product_id = $product->save();
+
+			$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/product-sale-badge /--><!-- /wp:woocommerce/single-product -->' );
+
+			$this->assertStringNotContainsString( 'wp-block-woocommerce-product-sale-badge', $markup, 'The outer Sale Badge block should be omitted.' );
+			$this->assertStringNotContainsString( 'wc-block-components-product-sale-badge', $markup, 'The Sale Badge component should be omitted.' );
+			$this->assertStringNotContainsString( 'Sale', $markup, 'Sale text should be omitted.' );
+		} finally {
+			if ( $product->get_id() ) {
+				$product->delete( true );
+			}
+
+			if ( $had_product ) {
+				$product = $original_product;
+			} else {
+				unset( $GLOBALS['product'] );
+			}
+		}
+	}
+
+	/**
+	 * @testdox Product Sale Badge renders the $align alignment class.
+	 *
+	 * @dataProvider provider_product_sale_badge_alignment
+	 * @param string $align Alignment value.
+	 */
+	public function test_product_sale_badge_renders_alignment_class( string $align ): void {
+		global $product;
+
+		$had_product      = array_key_exists( 'product', $GLOBALS );
+		$original_product = $had_product ? $product : null;
+		$product          = new \WC_Product_Simple();
+
+		try {
+			$product->set_name( 'Sale Product' );
+			$product->set_regular_price( '10' );
+			$product->set_sale_price( '5' );
+			$product_id = $product->save();
+
+			$markup         = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/product-sale-badge {"align":"' . $align . '"} /--><!-- /wp:woocommerce/single-product -->' );
+			$expected_class = 'wc-block-components-product-sale-badge--align-' . $align;
+
+			$this->assertStringContainsString( $expected_class, $markup );
+			foreach ( array_diff( array( 'left', 'center', 'right' ), array( $align ) ) as $other_align ) {
+				$this->assertStringNotContainsString(
+					'wc-block-components-product-sale-badge--align-' . $other_align,
+					$markup,
+					'The Sale Badge should contain only its requested alignment class.'
+				);
+			}
+		} finally {
+			if ( $product->get_id() ) {
+				$product->delete( true );
+			}
+
+			if ( $had_product ) {
+				$product = $original_product;
+			} else {
+				unset( $GLOBALS['product'] );
+			}
+		}
+	}
+
+	/**
+	 * Alignment values for the Sale Badge.
+	 *
+	 * @return array<string, array{string}>
+	 */
+	public function provider_product_sale_badge_alignment(): array {
+		return array(
+			'left'   => array( 'left' ),
+			'center' => array( 'center' ),
+			'right'  => array( 'right' ),
+		);
+	}
+
 	/**
 	 * Tests that the Product Sale Badge block is rendered correctly on the Single Product Block
 	 */