Commit 3e14ba7e61e for woocommerce
commit 3e14ba7e61ea99c3afd7708c94ced4a6864ca03b
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date: Mon Jun 22 16:40:44 2026 +0300
test: downgrade restricted-coupon e2e to PHPUnit validation + wiring tests (#65897)
Move restricted-coupon validation-rule coverage from Playwright E2E down to
PHPUnit: add four cart-based is_coupon_valid rejection cases (below-minimum
spend, non-included product, excluded product, disallowed email) and a
tearDown that resets the in-memory cart/current-user globals.
Collapse cart-checkout-restricted-coupons.spec.ts from 12 tests to 3 genuine
E2E tests (rejection wiring, acceptance wiring, full-stack email-restricted
order placement + per-user usage limit). This also closes the gap left by the
old "excluded product/category" test, which never applied the excluded coupon.
No production code changes.
Fixes TESTOPS-185
diff --git a/plugins/woocommerce/changelog/testops-185-restricted-coupon-e2e-downgrade b/plugins/woocommerce/changelog/testops-185-restricted-coupon-e2e-downgrade
new file mode 100644
index 00000000000..66160019e07
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-185-restricted-coupon-e2e-downgrade
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Move restricted-coupon validation-rule coverage from e2e to PHPUnit; reduce restricted-coupon cart/checkout e2e to wiring + order-placement tests; fix the excluded-coupon test that never applied its coupon.
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/coupons/cart-checkout-restricted-coupons.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/coupons/cart-checkout-restricted-coupons.spec.ts
index 1f37190da3c..69f9a05de19 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/coupons/cart-checkout-restricted-coupons.spec.ts
+++ b/plugins/woocommerce/tests/e2e-pw/tests/coupons/cart-checkout-restricted-coupons.spec.ts
@@ -7,6 +7,7 @@ import {
getOrderIdFromUrl,
WC_API_PATH,
} from '@woocommerce/e2e-utils-playwright';
+
/**
* Internal dependencies
*/
@@ -19,11 +20,8 @@ import {
} from '../../utils/pages';
const includedProductName = 'Included test product';
-const excludedProductName = 'Excluded test product';
const includedCategoryName = 'Included Category';
-const excludedCategoryName = 'Excluded Category';
-// This applies a coupon and waits for the result to prevent flakiness.
const applyCoupon = async ( page: Page, couponCode: string ) => {
const responsePromise = page.waitForResponse(
( response ) =>
@@ -37,72 +35,52 @@ const applyCoupon = async ( page: Page, couponCode: string ) => {
const expandCouponForm = async ( page: Page ) => {
await page
- .getByRole( 'button', {
- name: 'Enter your coupon code',
- } )
+ .getByRole( 'button', { name: 'Enter your coupon code' } )
.click();
- // This is to wait for the expand animation to finish, it avoids flakiness.
- await expect(
- page.locator( 'form.woocommerce-form-coupon' )
- ).toHaveAttribute( 'style', '' );
+ await expect( page.getByPlaceholder( 'Coupon code' ) ).toBeVisible();
+};
+
+const fillBillingDetails = async ( page: Page, email: string ) => {
+ await page.getByLabel( 'First name' ).first().fill( 'Homer' );
+ await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
+ await page
+ .getByLabel( 'Street address' )
+ .first()
+ .fill( '123 Evergreen Terrace' );
+ await page.getByLabel( 'Town / City' ).first().fill( 'Springfield' );
+ await page.getByLabel( 'ZIP Code' ).first().fill( '55555' );
+ await page.getByLabel( 'Phone' ).first().fill( '555-555-5555' );
+ await page.getByLabel( 'Email address' ).first().fill( email );
};
test.describe(
'Cart & Checkout Restricted Coupons',
- {
- tag: [
- tags.PAYMENTS,
- tags.SERVICES,
- tags.HPOS,
- tags.COULD_BE_LOWER_LEVEL_TEST,
- ],
- },
+ { tag: [ tags.PAYMENTS, tags.SERVICES, tags.HPOS ] },
() => {
- let firstProductId: number,
- secondProductId: number,
- firstCategoryId: number,
- secondCategoryId: number,
- shippingZoneId: number;
+ let firstProductId: number;
+ let firstCategoryId: number;
+ let shippingZoneId: number;
const couponBatchId: number[] = [];
test.beforeAll( async ( { restApi } ) => {
- // Make sure the classic cart and checkout pages exist
await createClassicCartPage();
await createClassicCheckoutPage();
- // make sure the store address is US
await restApi.post( `${ WC_API_PATH }/settings/general/batch`, {
update: [
- {
- id: 'woocommerce_store_address',
- value: 'addr 1',
- },
- {
- id: 'woocommerce_store_city',
- value: 'San Francisco',
- },
- {
- id: 'woocommerce_default_country',
- value: 'US:CA',
- },
- {
- id: 'woocommerce_store_postcode',
- value: '94107',
- },
+ { id: 'woocommerce_store_address', value: 'addr 1' },
+ { id: 'woocommerce_store_city', value: 'San Francisco' },
+ { id: 'woocommerce_default_country', value: 'US:CA' },
+ { id: 'woocommerce_store_postcode', value: '94107' },
],
} );
- // make sure the currency is USD
await restApi.put(
`${ WC_API_PATH }/settings/general/woocommerce_currency`,
- {
- value: 'USD',
- }
+ { value: 'USD' }
);
- // enable COD
await restApi.put( `${ WC_API_PATH }/payment_gateways/cod`, {
enabled: true,
} );
- // add a shipping zone and method
await restApi
.post( `${ WC_API_PATH }/shipping/zones`, {
name: 'Free Shipping',
@@ -112,11 +90,8 @@ test.describe(
} );
await restApi.post(
`${ WC_API_PATH }/shipping/zones/${ shippingZoneId }/methods`,
- {
- method_id: 'free_shipping',
- }
+ { method_id: 'free_shipping' }
);
- // add categories
await restApi
.post( `${ WC_API_PATH }/products/categories`, {
name: includedCategoryName,
@@ -124,14 +99,6 @@ test.describe(
.then( ( response: { data: { id: number } } ) => {
firstCategoryId = response.data.id;
} );
- await restApi
- .post( `${ WC_API_PATH }/products/categories`, {
- name: excludedCategoryName,
- } )
- .then( ( response: { data: { id: number } } ) => {
- secondCategoryId = response.data.id;
- } );
- // add product
await restApi
.post( `${ WC_API_PATH }/products`, {
name: includedProductName,
@@ -142,134 +109,71 @@ test.describe(
.then( ( response: { data: { id: number } } ) => {
firstProductId = response.data.id;
} );
- await restApi
- .post( `${ WC_API_PATH }/products`, {
- name: excludedProductName,
- type: 'simple',
- regular_price: '20.00',
- sale_price: '15.00',
- categories: [ { id: secondCategoryId } ],
- } )
- .then( ( response: { data: { id: number } } ) => {
- secondProductId = response.data.id;
- } );
- const restrictedCoupons = [
+ const residualCoupons = [
{
code: 'expired-coupon',
discount_type: 'fixed_cart',
amount: '10.00',
- description:
- 'This coupon has expired and should not be usable by anyone.',
date_expires: '2020-01-01T00:00:00',
},
- {
- code: 'min-max-spend-individual',
- discount_type: 'fixed_cart',
- amount: '20.00',
- description:
- 'This coupon requires an order amount between 50 and 200 dollars. It can only be used by itself.',
- minimum_amount: '50.00',
- maximum_amount: '200.00',
- individual_use: true,
- },
- {
- code: 'no-sale-use-limit',
- discount_type: 'fixed_cart',
- amount: '15.00',
- description:
- 'This coupon can only be used twice, and only on items that are not on sale.',
- exclude_sale_items: true,
- usage_limit: 2,
- },
{
code: 'product-and-category-included',
discount_type: 'fixed_cart',
amount: '10.00',
- description:
- 'This coupon can only be used for the specific products and categories.',
product_ids: [ firstProductId ],
product_categories: [ firstCategoryId ],
},
- {
- code: 'product-and-category-excluded',
- discount_type: 'fixed_cart',
- amount: '20.00',
- description:
- 'This coupon can not be used for specific products and categories.',
- excluded_product_ids: [ secondProductId ],
- excluded_product_categories: [ secondCategoryId ],
- },
{
code: 'email-restricted',
discount_type: 'fixed_cart',
amount: '25.00',
- description:
- 'This coupon can only be used once by a specified user (email).',
email_restrictions: [ 'homer@example.com' ],
usage_limit_per_user: 1,
},
];
-
- // add coupons
await restApi
.post( `${ WC_API_PATH }/coupons/batch`, {
- create: restrictedCoupons,
+ create: residualCoupons,
} )
.then( ( response: { data: { create: { id: number }[] } } ) => {
- for ( let i = 0; i < response.data.create.length; i++ ) {
- couponBatchId.push( response.data.create[ i ].id );
+ for ( const created of response.data.create ) {
+ couponBatchId.push( created.id );
}
} );
} );
test.beforeEach( async ( { context } ) => {
- // Shopping cart is very sensitive to cookies, so be explicit
+ // Shopping cart is very sensitive to cookies, so be explicit.
await context.clearCookies();
} );
test.afterAll( async ( { restApi } ) => {
await restApi.delete(
`${ WC_API_PATH }/products/${ firstProductId }`,
- {
- force: true,
- }
- );
- await restApi.delete(
- `${ WC_API_PATH }/products/${ secondProductId }`,
- {
- force: true,
- }
+ { force: true }
);
await restApi.delete(
`${ WC_API_PATH }/products/categories/${ firstCategoryId }`,
- {
- force: true,
- }
- );
- await restApi.delete(
- `${ WC_API_PATH }/products/categories/${ secondCategoryId }`,
- {
- force: true,
- }
+ { force: true }
);
await restApi.post( `${ WC_API_PATH }/coupons/batch`, {
delete: [ ...couponBatchId ],
} );
-
await restApi.put( `${ WC_API_PATH }/payment_gateways/cod`, {
enabled: false,
} );
await restApi.delete(
`${ WC_API_PATH }/shipping/zones/${ shippingZoneId }`,
- {
- force: true,
- }
+ { force: true }
);
} );
- test( 'expired coupon cannot be used', async ( { page, context } ) => {
- await test.step( 'Load cart page and try expired coupon usage', async () => {
+ test( 'rejected coupon surfaces its error in cart and checkout', async ( {
+ page,
+ context,
+ } ) => {
+ await test.step( 'cart', async () => {
await addAProductToCart( page, firstProductId );
await page.goto( CLASSIC_CART_PAGE.slug );
await applyCoupon( page, 'expired-coupon' );
@@ -280,7 +184,7 @@ test.describe(
await context.clearCookies();
- await test.step( 'Load checkout page and try expired coupon usage', async () => {
+ await test.step( 'checkout', async () => {
await addAProductToCart( page, firstProductId );
await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
await expandCouponForm( page );
@@ -291,288 +195,14 @@ test.describe(
} );
} );
- test( 'coupon requiring min and max amounts and can only be used alone can only be used within limits', async ( {
+ test( 'accepted coupon surfaces success in cart and checkout', async ( {
page,
context,
} ) => {
- await test.step( 'Load cart page and try limited coupon usage', async () => {
+ await test.step( 'cart', async () => {
await addAProductToCart( page, firstProductId );
await page.goto( CLASSIC_CART_PAGE.slug );
- await applyCoupon( page, 'min-max-spend-individual' );
- // failed because we need to have at least $50 in cart (single product is only $20)
- await expect(
- page
- .getByRole( 'alert' )
- .getByText(
- 'The minimum spend for coupon "min-max-spend-individual" is $50.00.'
- )
- ).toBeVisible();
-
- // add a couple more in order to hit minimum spend
- await addAProductToCart( page, firstProductId, 2 );
-
- // passed because we're between 50 and 200 dollars
- await page.goto( CLASSIC_CART_PAGE.slug );
- await applyCoupon( page, 'min-max-spend-individual' );
- await expect(
- page.getByText( 'Coupon code applied successfully.' )
- ).toBeVisible();
-
- // fail because the min-max coupon can only be used by itself
- await page.goto( CLASSIC_CART_PAGE.slug );
- await applyCoupon( page, 'no-sale-use-limit' );
- await expect(
- page.getByText(
- 'Sorry, coupon "min-max-spend-individual" has already been applied and cannot be used in conjunction with other coupons.'
- )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try limited coupon usage', async () => {
- await addAProductToCart( page, firstProductId );
-
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'min-max-spend-individual' );
- // failed because we need to have at least $50 in cart (single product is only $20)
- await expect(
- page.getByText(
- 'The minimum spend for coupon "min-max-spend-individual" is $50.00.'
- )
- ).toBeVisible();
-
- // add a couple more in order to hit minimum spend
- await addAProductToCart( page, firstProductId, 2 );
-
- // passed because we're between 50 and 200 dollars
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'min-max-spend-individual' );
- await expect(
- page.getByText( 'Coupon code applied successfully.' )
- ).toBeVisible();
-
- // fail because the min-max coupon can only be used by itself
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'no-sale-use-limit' );
- await expect(
- page.getByText(
- 'Sorry, coupon "min-max-spend-individual" has already been applied and cannot be used in conjunction with other coupons.'
- )
- ).toBeVisible();
- } );
- } );
-
- test( 'coupon cannot be used on sale item', async ( {
- page,
- context,
- } ) => {
- await test.step( 'Load cart page and try coupon usage on sale item', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'no-sale-use-limit' );
- // failed because this product is on sale.
- await expect(
- page.getByText(
- 'Sorry, coupon "no-sale-use-limit" is not valid for sale items.'
- )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try coupon usage on sale item', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'no-sale-use-limit' );
- // failed because this product is on sale
- await expect(
- page.getByText(
- 'Sorry, coupon "no-sale-use-limit" is not valid for sale items.'
- )
- ).toBeVisible();
- } );
- } );
-
- test( 'coupon can only be used twice', async ( {
- page,
- context,
- restApi,
- } ) => {
- const orderIds: number[] = [];
-
- // create 2 orders using the limited coupon
- for ( let i = 0; i < 2; i++ ) {
- await restApi
- .post( `${ WC_API_PATH }/orders`, {
- status: 'completed',
- billing: {
- first_name: 'Marge',
- last_name: 'Simpson',
- email: 'marge.simpson@example.org',
- },
- line_items: [
- {
- product_id: firstProductId,
- quantity: 5,
- },
- ],
- coupon_lines: [
- {
- code: 'no-sale-use-limit',
- },
- ],
- } )
- .then( ( response: { data: { id: number } } ) => {
- orderIds.push( response.data.id );
- } );
- }
-
- await test.step( 'Load cart page and try over limit coupon usage', async () => {
- await addAProductToCart( page, firstProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'no-sale-use-limit' );
- // failed because this coupon code has been used too much
- await expect(
- page.getByText(
- 'Usage limit for coupon "no-sale-use-limit" has been reached. Please try again after some time, or contact us for help.'
- )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try over limit coupon usage', async () => {
- await addAProductToCart( page, firstProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'no-sale-use-limit' );
- // failed because this coupon code has been used too much
- await expect(
- page.getByText(
- 'Usage limit for coupon "no-sale-use-limit" has been reached. Please try again after some time, or contact us for help.'
- )
- ).toBeVisible();
- } );
-
- // clean up the orders
- await restApi.post( `${ WC_API_PATH }/orders/batch`, {
- delete: [ ...orderIds ],
- } );
- } );
-
- test( 'coupon cannot be used on certain products/categories (included product/category)', async ( {
- page,
- context,
- } ) => {
- await test.step( 'Load cart page and try included certain items coupon usage', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // failed because this product is not included for coupon
- await expect(
- page.getByText(
- 'Sorry, coupon "product-and-category-included" is not applicable to selected products.'
- )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try included certain items coupon usage', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // failed because this product is not included for coupon
- await expect(
- page.getByText(
- 'Sorry, coupon "product-and-category-included" is not applicable to selected products.'
- )
- ).toBeVisible();
- } );
- } );
-
- test( 'coupon can be used on certain products/categories', async ( {
- page,
- context,
- } ) => {
- await test.step( 'Load cart page and try on certain products coupon usage', async () => {
- await addAProductToCart( page, firstProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // succeeded
- await expect(
- page.getByText( 'Coupon code applied successfully.' )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try on certain products coupon usage', async () => {
- await addAProductToCart( page, firstProductId );
-
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // succeeded
- await expect(
- page.getByText( 'Coupon code applied successfully.' )
- ).toBeVisible();
- } );
- } );
-
- test( 'coupon cannot be used on specific products/categories (excluded product/category)', async ( {
- page,
- context,
- } ) => {
- await test.step( 'Load cart page and try excluded items coupon usage', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
await applyCoupon( page, 'product-and-category-included' );
- // failed because this product is excluded from coupon
- await expect(
- page.getByText(
- 'Sorry, coupon "product-and-category-included" is not applicable to selected products.'
- )
- ).toBeVisible();
- } );
-
- await context.clearCookies();
-
- await test.step( 'Load checkout page and try excluded items coupon usage', async () => {
- await addAProductToCart( page, secondProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // failed because this product is excluded from coupon
- await expect(
- page.getByText(
- 'Sorry, coupon "product-and-category-included" is not applicable to selected products.'
- )
- ).toBeVisible();
- } );
- } );
-
- test( 'coupon can be used on other products/categories', async ( {
- page,
- context,
- } ) => {
- await test.step( 'Load cart page and try coupon usage on other items', async () => {
- await addAProductToCart( page, firstProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'product-and-category-included' );
- // succeeded
await expect(
page.getByText( 'Coupon code applied successfully.' )
).toBeVisible();
@@ -580,91 +210,24 @@ test.describe(
await context.clearCookies();
- await test.step( 'Load checkout page and try coupon usage on other items', async () => {
+ await test.step( 'checkout', async () => {
await addAProductToCart( page, firstProductId );
-
await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
await expandCouponForm( page );
await applyCoupon( page, 'product-and-category-included' );
- // succeeded
await expect(
page.getByText( 'Coupon code applied successfully.' )
).toBeVisible();
} );
} );
- test( 'coupon cannot be used by any customer on cart (email restricted)', async ( {
- page,
- } ) => {
- await addAProductToCart( page, firstProductId );
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
- await expandCouponForm( page );
- await applyCoupon( page, 'email-restricted' );
- await expect(
- page.getByText(
- 'Please enter a valid email to use coupon code "email-restricted".'
- )
- ).toBeVisible();
- } );
-
- test( 'coupon cannot be used by any customer on checkout (email restricted)', async ( {
- page,
- } ) => {
- await addAProductToCart( page, firstProductId );
-
- await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
-
- await page.getByLabel( 'First name' ).first().fill( 'Marge' );
- await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
- await page
- .getByLabel( 'Street address' )
- .first()
- .fill( '123 Evergreen Terrace' );
- await page
- .getByLabel( 'Town / City' )
- .first()
- .fill( 'Springfield' );
- await page.getByLabel( 'ZIP Code' ).first().fill( '55555' );
- await page.getByLabel( 'Phone' ).first().fill( '555-555-5555' );
- await page
- .getByLabel( 'Email address' )
- .first()
- .fill( 'marge.simpson@example.org' );
-
- await expandCouponForm( page );
- await applyCoupon( page, 'email-restricted' );
- await expect(
- page.getByText(
- 'Please enter a valid email to use coupon code "email-restricted".'
- )
- ).toBeVisible();
- } );
-
- test( 'coupon can be used by the right customer (email restricted) but only once', async ( {
+ test( 'email-restricted coupon can be used by the right customer but only once', async ( {
page,
restApi,
} ) => {
await addAProductToCart( page, firstProductId );
-
await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
-
- await page.getByLabel( 'First name' ).first().fill( 'Homer' );
- await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
- await page
- .getByLabel( 'Street address' )
- .first()
- .fill( '123 Evergreen Terrace' );
- await page
- .getByLabel( 'Town / City' )
- .first()
- .fill( 'Springfield' );
- await page.getByLabel( 'ZIP Code' ).first().fill( '55555' );
- await page.getByLabel( 'Phone' ).first().fill( '555-555-5555' );
- await page
- .getByLabel( 'Email address' )
- .first()
- .fill( 'homer@example.com' );
-
+ await fillBillingDetails( page, 'homer@example.com' );
await expandCouponForm( page );
await applyCoupon( page, 'email-restricted' );
await expect(
@@ -672,34 +235,14 @@ test.describe(
).toBeVisible();
await page.getByRole( 'button', { name: 'Place order' } ).click();
-
await expect(
page.getByText( 'Your order has been received' )
).toBeVisible();
const newOrderId = getOrderIdFromUrl( page );
- // try to order a second time, but should get an error
await addAProductToCart( page, firstProductId );
-
await page.goto( CLASSIC_CHECKOUT_PAGE.slug );
-
- await page.getByLabel( 'First name' ).first().fill( 'Homer' );
- await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
- await page
- .getByLabel( 'Street address' )
- .first()
- .fill( '123 Evergreen Terrace' );
- await page
- .getByLabel( 'Town / City' )
- .first()
- .fill( 'Springfield' );
- await page.getByLabel( 'ZIP Code' ).first().fill( '55555' );
- await page.getByLabel( 'Phone' ).first().fill( '555-555-5555' );
- await page
- .getByLabel( 'Email address' )
- .first()
- .fill( 'homer@example.com' );
-
+ await fillBillingDetails( page, 'homer@example.com' );
await expandCouponForm( page );
await applyCoupon( page, 'email-restricted' );
await expect(
@@ -707,14 +250,12 @@ test.describe(
).toBeVisible();
await page.getByRole( 'button', { name: 'Place order' } ).click();
-
await expect(
page.getByText(
'Usage limit for coupon "email-restricted" has been reached.'
)
).toBeVisible();
- // clean up the order we just made
await restApi.delete( `${ WC_API_PATH }/orders/${ newOrderId }`, {
force: true,
} );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
index cfc84129413..ccbd8141b10 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
@@ -12,6 +12,18 @@ use Automattic\WooCommerce\Enums\OrderStatus;
*/
class WC_Discounts_Tests extends WC_Unit_Test_Case {
+ /**
+ * Tear down test fixtures.
+ *
+ * The cart and current user are in-memory globals that the per-test DB transaction
+ * does not roll back, so reset them explicitly to avoid leaking state into other tests.
+ */
+ public function tearDown(): void {
+ WC()->cart->empty_cart();
+ wp_set_current_user( 0 );
+ parent::tearDown();
+ }
+
/**
* Helper method to create limited coupon.
*/
@@ -114,4 +126,117 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
$this->assertInstanceOf( WP_Error::class, $result, 'Once trashed, the coupon is no longer valid.' );
$this->assertEquals( 'invalid_coupon', $result->get_error_code(), 'We receive an appropriate WP_Error.' );
}
+
+ /**
+ * @testdox is_coupon_valid rejects a coupon when the cart subtotal is below its minimum spend.
+ */
+ public function test_is_coupon_valid_rejects_below_minimum_spend() {
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ WC()->cart->empty_cart();
+
+ $product = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => 20 ) );
+ $coupon = new WC_Coupon();
+ $coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 10,
+ 'minimum_amount' => 50,
+ )
+ );
+ $coupon->save();
+
+ // $20 < $50 minimum.
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $result = $discounts->is_coupon_valid( $coupon );
+ $this->assertWPError( $result, 'coupon below minimum spend should be invalid' );
+ $this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET ), $result->get_error_message() );
+ }
+
+ /**
+ * @testdox is_coupon_valid rejects a product/category-restricted coupon when the cart has none of its products.
+ */
+ public function test_is_coupon_valid_rejects_non_included_product() {
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ WC()->cart->empty_cart();
+
+ $included = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => 20 ) );
+ $other = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => 20 ) );
+ $coupon = new WC_Coupon();
+ $coupon->set_props(
+ array(
+ 'code' => 'included-only',
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 10,
+ 'product_ids' => array( $included->get_id() ),
+ )
+ );
+ $coupon->save();
+
+ // Not the included product.
+ WC()->cart->add_to_cart( $other->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $result = $discounts->is_coupon_valid( $coupon );
+ $this->assertWPError( $result, 'coupon should not apply to non-included products' );
+ // The product_ids rule throws its own inline message (class-wc-discounts.php) rather than
+ // routing through WC_Coupon::get_coupon_error(), so assert that stable phrase directly.
+ $this->assertStringContainsString( 'is not applicable to selected products.', $result->get_error_message() );
+ }
+
+ /**
+ * @testdox is_coupon_valid rejects a coupon when the cart contains one of its excluded products.
+ *
+ * Closes the gap left by the e2e "excluded product/category" test, which never applied the excluded coupon.
+ */
+ public function test_is_coupon_valid_rejects_excluded_product() {
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ WC()->cart->empty_cart();
+
+ $excluded = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => 20 ) );
+ $coupon = new WC_Coupon();
+ $coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 20,
+ 'excluded_product_ids' => array( $excluded->get_id() ),
+ )
+ );
+ $coupon->save();
+
+ WC()->cart->add_to_cart( $excluded->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $result = $discounts->is_coupon_valid( $coupon );
+ $this->assertWPError( $result, 'coupon should be rejected when an excluded product is in the cart' );
+ $this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_EXCLUDED_PRODUCTS ), $result->get_error_message() );
+ }
+
+ /**
+ * @testdox is_coupon_valid rejects an email-restricted coupon for a non-matching customer.
+ */
+ public function test_is_coupon_valid_rejects_disallowed_email() {
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ WC()->cart->empty_cart();
+
+ $product = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => 20 ) );
+ $coupon = new WC_Coupon();
+ $coupon->set_props(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'amount' => 25,
+ 'email_restrictions' => array( 'allowed@example.com' ),
+ )
+ );
+ $coupon->save();
+
+ wp_set_current_user( 0 );
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ $discounts = new WC_Discounts( WC()->cart );
+
+ $result = $discounts->is_coupon_valid( $coupon );
+ $this->assertWPError( $result, 'email-restricted coupon should be invalid for a non-matching customer' );
+ $this->assertEquals( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ), $result->get_error_message() );
+ }
}