Commit 95e2578742 for woocommerce

commit 95e2578742366730fa513f9673bde0d2933d529c
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date:   Mon Jun 30 16:35:28 2025 +0100

    Cherry pick #59221 into `trunk` (#59278)

    Restore `DiscountMeta` slot in Cart when coupons missing and improve CSS specificity (#59221)

    * Restore DiscountMeta slot fill in Cart when coupons missing

    * Add changelog

    * Add tests to verify DiscountsMeta Slot is rendered

    * Ensure plugins using the panel in the .slot-wrapper are not impacting by the padding reset

    * Lint fixes

    * Fix comment wording

    * Fix test import order

    * Remove ts any

    ---------

    Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
    Co-authored-by: Raluca Stan <ralucastn@gmail.com>

diff --git a/plugins/woocommerce/changelog/fix-cart-discount-meta b/plugins/woocommerce/changelog/fix-cart-discount-meta
new file mode 100644
index 0000000000..9b4ea06808
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-cart-discount-meta
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Ensure discount meta slot fill shows when no coupons are applied to the cart.
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/sidebar-layout/style.scss b/plugins/woocommerce/client/blocks/assets/js/base/components/sidebar-layout/style.scss
index 59550c27c6..8a87483e33 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/sidebar-layout/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/sidebar-layout/style.scss
@@ -52,6 +52,9 @@
 	.wc-block-components-sidebar {
 		.wc-block-components-totals-item,
 		.wc-block-components-panel,
+		// Increase specificity to overwrite the rule from the TotalsWrapper component
+		// https://github.com/woocommerce/woocommerce/blob/74e823c0324289cff7361629ff8cc677c45dce0f/plugins/woocommerce/client/blocks/packages/components/totals-wrapper/style.scss#L24
+		.slot-wrapper .wc-block-components-panel,
 		.wc-block-components-totals-coupon {
 			padding-left: $gap;
 			padding-right: $gap;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/block.tsx
index ae169b133c..c38adaacb6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/block.tsx
@@ -27,9 +27,9 @@ const Block = ( { className }: { className: string } ) => {
 	const { cartTotals, cartCoupons } = useStoreCart();
 	const { removeCoupon, isRemovingCoupon } = useStoreCartCoupons( 'wc/cart' );

-	// Hide if there are no coupons to show.
+	// Hide all but the slot/fill if there are no coupons to show.
 	if ( ! cartCoupons.length ) {
-		return null;
+		return <DiscountSlotFill />;
 	}

 	const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/test/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/test/block.tsx
new file mode 100644
index 0000000000..8e2ccec7db
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-order-summary-discount/test/block.tsx
@@ -0,0 +1,220 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { SlotFillProvider } from '@wordpress/components';
+import {
+	useStoreCart,
+	useStoreCartCoupons,
+} from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import Block from '../block';
+
+// We only need to mock the hooks since we're using real components
+jest.mock( '@woocommerce/base-context/hooks', () => ( {
+	useStoreCart: jest.fn(),
+	useStoreCartCoupons: jest.fn(),
+} ) );
+
+// Mock the ExperimentalDiscountsMeta to track when slot is rendered
+const mockSlotRender = jest.fn();
+jest.mock( '@woocommerce/blocks-checkout', () => {
+	const actual = jest.requireActual( '@woocommerce/blocks-checkout' );
+	return {
+		...actual,
+		ExperimentalDiscountsMeta: {
+			...actual.ExperimentalDiscountsMeta,
+			Slot: ( props: unknown ) => {
+				mockSlotRender( props );
+				// Return a testable element that represents the slot
+				return <div data-testid="discount-slot" />;
+			},
+		},
+	};
+} );
+
+const mockCartTotals = {
+	currency_code: 'USD',
+	currency_symbol: '$',
+	currency_minor_unit: 2,
+	currency_decimal_separator: '.',
+	currency_thousand_separator: ',',
+	currency_prefix: '$',
+	currency_suffix: '',
+	total_discount: '0',
+	total_discount_tax: '0',
+};
+
+const mockCartData = {
+	cartTotals: mockCartTotals,
+	cartCoupons: [],
+	extensions: { some: 'data' },
+	receiveCart: jest.fn(),
+	otherCartData: { test: 'value' },
+};
+
+const mockCouponHooks = {
+	removeCoupon: jest.fn(),
+	isRemovingCoupon: false,
+};
+
+describe( 'Cart Order Summary Discount Block', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		( useStoreCart as jest.Mock ).mockReturnValue( mockCartData );
+		( useStoreCartCoupons as jest.Mock ).mockReturnValue( mockCouponHooks );
+	} );
+
+	const renderWithProviders = ( ui: React.ReactElement ) => {
+		return render( <SlotFillProvider>{ ui }</SlotFillProvider> );
+	};
+
+	it( 'renders only the DiscountSlotFill when there are no coupons', () => {
+		renderWithProviders( <Block className="test-class" /> );
+
+		// Verify the slot is rendered
+		expect( screen.getByTestId( 'discount-slot' ) ).toBeInTheDocument();
+
+		// Since we're using real TotalsWrapper, it won't render when there are no children
+		// TotalsDiscount should not be present when there are no coupons
+		expect( screen.queryByText( /discount/i ) ).not.toBeInTheDocument();
+
+		// Verify the slot was called with correct props
+		expect( mockSlotRender ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				context: 'woocommerce/cart',
+				extensions: { some: 'data' },
+				cart: expect.objectContaining( {
+					cartTotals: mockCartTotals,
+					otherCartData: { test: 'value' },
+				} ),
+			} )
+		);
+
+		// Verify receiveCart was not passed to the slot
+		const slotProps = mockSlotRender.mock.calls[ 0 ][ 0 ];
+		expect( slotProps.cart ).not.toHaveProperty( 'receiveCart' );
+	} );
+
+	it( 'renders both TotalsDiscount and DiscountSlotFill when there are coupons', () => {
+		const mockCoupons = [
+			{
+				code: 'TEST10',
+				label: 'TEST10', // The label is what gets displayed
+				discount_type: 'percent',
+				amount: '10',
+				totals: {
+					total_discount: '1000',
+					total_discount_tax: '0',
+				},
+			},
+		];
+
+		( useStoreCart as jest.Mock ).mockReturnValue( {
+			...mockCartData,
+			cartCoupons: mockCoupons,
+			cartTotals: {
+				...mockCartTotals,
+				total_discount: '1000',
+			},
+		} );
+
+		renderWithProviders( <Block className="test-class" /> );
+
+		// With real components, look for the discount text/coupon code
+		expect( screen.getByText( 'TEST10' ) ).toBeInTheDocument();
+
+		// Verify the slot is still rendered
+		expect( screen.getByTestId( 'discount-slot' ) ).toBeInTheDocument();
+
+		// The wrapper should have the provided class
+		const wrapper = screen.getByText( 'TEST10' ).closest( '.test-class' );
+		expect( wrapper ).toBeInTheDocument();
+	} );
+
+	it( 'calls useStoreCartCoupons with correct context', () => {
+		renderWithProviders( <Block className="test-class" /> );
+
+		expect( useStoreCartCoupons ).toHaveBeenCalledWith( 'wc/cart' );
+	} );
+
+	it( 'always renders the ExperimentalDiscountsMeta.Slot regardless of coupon state', () => {
+		// Test with no coupons
+		const { rerender } = renderWithProviders(
+			<Block className="test-class" />
+		);
+		expect( screen.getByTestId( 'discount-slot' ) ).toBeInTheDocument();
+		expect( mockSlotRender ).toHaveBeenCalledTimes( 1 );
+
+		// Clean up
+		jest.clearAllMocks();
+
+		// Test with coupons
+		( useStoreCart as jest.Mock ).mockReturnValue( {
+			...mockCartData,
+			cartCoupons: [
+				{
+					code: 'TEST',
+					label: 'TEST', // Add label for display
+					totals: {
+						total_discount: '500',
+						total_discount_tax: '0',
+					},
+				},
+			],
+		} );
+
+		rerender(
+			<SlotFillProvider>
+				<Block className="test-class" />
+			</SlotFillProvider>
+		);
+
+		// Should still have the slot
+		expect( screen.getByTestId( 'discount-slot' ) ).toBeInTheDocument();
+
+		// Verify it was called with correct props both times
+		expect( mockSlotRender ).toHaveBeenCalledTimes( 1 );
+
+		// Check the props structure
+		expect( mockSlotRender ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				context: 'woocommerce/cart',
+				extensions: expect.any( Object ),
+				cart: expect.objectContaining( {
+					cartTotals: expect.any( Object ),
+				} ),
+			} )
+		);
+
+		// Verify receiveCart is not passed
+		const slotProps = mockSlotRender.mock.calls[ 0 ][ 0 ];
+		expect( slotProps.cart ).not.toHaveProperty( 'receiveCart' );
+	} );
+
+	it( 'handles missing className prop gracefully', () => {
+		( useStoreCart as jest.Mock ).mockReturnValue( {
+			...mockCartData,
+			cartCoupons: [
+				{
+					code: 'TEST',
+					label: 'TEST', // Add label for display
+					totals: {
+						total_discount: '500',
+						total_discount_tax: '0',
+					},
+				},
+			],
+		} );
+
+		// @ts-expect-error Testing without required prop
+		renderWithProviders( <Block /> );
+
+		// Should still render without errors
+		expect( screen.getByText( 'TEST' ) ).toBeInTheDocument();
+		expect( screen.getByTestId( 'discount-slot' ) ).toBeInTheDocument();
+	} );
+} );