Commit bf948256984 for woocommerce
commit bf9482569840db35c063bd642fcd3aafb4a955a3
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date: Wed Jun 10 15:25:57 2026 +0200
[tests] Move redundant coupon e2e checks down to php unit (#65592)
* Reduce coupon creation E2E coverage
* add changelog
* Merge coupon E2E specs
* Keep one coupon E2E smoke
* Fix strict locator issue
* Add coupon description assertion to E2E test
* Use URLSearchParams for coupon post ID assertion
diff --git a/plugins/woocommerce/changelog/testops-139-woocommerce-core-move-redundant-coupon-e2e-down-to-php b/plugins/woocommerce/changelog/testops-139-woocommerce-core-move-redundant-coupon-e2e-down-to-php
new file mode 100644
index 00000000000..5df76d7534c
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-139-woocommerce-core-move-redundant-coupon-e2e-down-to-php
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+tests: move coupons test coverage from e2e to php unit
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/coupons/coupons.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/coupons/coupons.spec.ts
new file mode 100644
index 00000000000..01f6bed9476
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/coupons/coupons.spec.ts
@@ -0,0 +1,109 @@
+/**
+ * External dependencies
+ */
+import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
+
+/**
+ * Internal dependencies
+ */
+import { expect, tags, test as baseTest } from '../../fixtures/fixtures';
+import { ADMIN_STATE_PATH } from '../../playwright.config';
+
+const test = baseTest.extend( {
+ storageState: ADMIN_STATE_PATH,
+ coupon: async ( { restApi }, use ) => {
+ const coupon = {
+ code: `restricted-wiring-${ Date.now() }`,
+ description: `Restricted coupon wiring ${ Date.now() }`,
+ amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
+ };
+ await use( coupon );
+ if ( coupon.id ) {
+ await restApi.delete( `${ WC_API_PATH }/coupons/${ coupon.id }`, {
+ force: true,
+ } );
+ }
+ },
+
+ product: async ( { restApi }, use ) => {
+ let product = {};
+ const productName = `Product ${ Date.now() }`;
+
+ await restApi
+ .post( `${ WC_API_PATH }/products`, {
+ name: productName,
+ regular_price: '100',
+ } )
+ .then( ( response ) => {
+ product = response.data;
+ } );
+
+ await use( product );
+
+ await restApi.delete( `${ WC_API_PATH }/products/${ product.id }`, {
+ force: true,
+ } );
+ },
+} );
+
+test.describe( 'Coupon management', { tag: tags.SERVICES }, () => {
+ test( 'can create a product-restricted coupon through the admin form', async ( {
+ page,
+ coupon,
+ product,
+ } ) => {
+ await test.step( 'fill the rendered coupon form', async () => {
+ await page.goto( 'wp-admin/post-new.php?post_type=shop_coupon' );
+ await page.getByLabel( 'Coupon code' ).fill( coupon.code );
+ await page
+ .getByPlaceholder( 'Description (optional)' )
+ .fill( coupon.description );
+ await page.getByLabel( 'Coupon amount' ).fill( coupon.amount );
+ await page
+ .getByRole( 'link', { name: 'Usage restriction' } )
+ .click();
+ await page
+ .locator( '#usage_restriction_coupon_data p' )
+ .filter( {
+ has: page.getByText( 'Products', { exact: true } ),
+ } )
+ .getByPlaceholder( 'Search for a product…' )
+ .pressSequentially( product.name );
+ await page.getByRole( 'option', { name: product.name } ).click();
+ } );
+
+ await test.step( 'publish the coupon', async () => {
+ await expect( page.getByText( 'Move to Trash' ) ).toBeVisible();
+ await page
+ .getByRole( 'button', { name: 'Publish', exact: true } )
+ .click();
+ await expect( page.getByText( 'Coupon updated.' ) ).toBeVisible();
+
+ const searchParams = new URL( page.url() ).searchParams;
+ coupon.id = searchParams.get( 'post' );
+ expect( coupon.id ).toBeDefined();
+ expect( coupon.id ).toMatch( /^\d+$/ );
+ } );
+
+ await test.step( 'verify persisted product restriction', async () => {
+ await page.goto(
+ `wp-admin/post.php?post=${ coupon.id }&action=edit`
+ );
+ await expect( page.getByLabel( 'Coupon code' ) ).toHaveValue(
+ coupon.code
+ );
+ await expect(
+ page.getByPlaceholder( 'Description (optional)' )
+ ).toHaveValue( coupon.description );
+ await expect( page.getByLabel( 'Coupon amount' ) ).toHaveValue(
+ coupon.amount
+ );
+ await page
+ .getByRole( 'link', { name: 'Usage restriction' } )
+ .click();
+ await expect(
+ page.getByRole( 'listitem', { name: product.name } )
+ ).toBeVisible();
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-coupon.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-coupon.spec.ts
deleted file mode 100644
index 0976797e67b..00000000000
--- a/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-coupon.spec.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * External dependencies
- */
-import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { expect, tags, test as baseTest } from '../../fixtures/fixtures';
-import { ADMIN_STATE_PATH } from '../../playwright.config';
-
-const couponData = {
- fixedCart: {
- code: `fixedCart-${ Date.now() }`,
- description: `Simple fixed cart discount ${ Date.now() }`,
- amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
- },
- fixedProduct: {
- code: `fixedProduct-${ Date.now() }`,
- description: `Simple fixed product discount ${ Date.now() }`,
- amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
- },
- percentage: {
- code: `percentage-${ Date.now() }`,
- description: `Simple percentage discount ${ Date.now() }`,
- amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
- },
- expiryDate: {
- code: `expiryDate-${ Date.now() }`,
- description: `Simple expiry date discount ${ Date.now() }`,
- amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
- expiryDate: '2023-12-31',
- },
- freeShipping: {
- code: `freeShipping-${ Date.now() }`,
- description: `Simple free shipping discount ${ Date.now() }`,
- amount: `${ Math.floor( Math.random() * 50 ) + 1 }`,
- freeShipping: true,
- },
-};
-
-const test = baseTest.extend( {
- storageState: ADMIN_STATE_PATH,
- coupon: async ( { restApi }, use ) => {
- const coupon = {};
- await use( coupon );
- await restApi.delete( `${ WC_API_PATH }/coupons/${ coupon.id }`, {
- force: true,
- } );
- },
-} );
-
-test.describe( 'Coupon management', { tag: tags.SERVICES }, () => {
- for ( const couponType of Object.keys( couponData ) ) {
- test( `can create new ${ couponType } coupon`, async ( {
- page,
- coupon,
- } ) => {
- await test.step( 'add new coupon', async () => {
- await page.goto(
- 'wp-admin/post-new.php?post_type=shop_coupon'
- );
- await page
- .getByLabel( 'Coupon code' )
- .fill( couponData[ couponType ].code );
- await page
- .getByPlaceholder( 'Description (optional)' )
- .fill( couponData[ couponType ].description );
- await page
- .getByPlaceholder( '0' )
- .fill( couponData[ couponType ].amount );
-
- // set expiry date if it was provided
- if ( couponData[ couponType ].expiryDate ) {
- await page
- .getByPlaceholder( 'yyyy-mm-dd' )
- .fill( couponData[ couponType ].expiryDate );
- }
-
- // be explicit about whether free shipping is allowed
- if ( couponData[ couponType ].freeShipping ) {
- await page.getByLabel( 'Allow free shipping' ).check();
- } else {
- await page.getByLabel( 'Allow free shipping' ).uncheck();
- }
- } );
-
- // publish the coupon and retrieve the id
- await test.step( 'publish the coupon', async () => {
- await expect(
- page.getByRole( 'link', { name: 'Move to Trash' } )
- ).toBeVisible();
- await page
- .getByRole( 'button', { name: 'Publish', exact: true } )
- .click();
- await expect(
- page.getByText( 'Coupon updated.' )
- ).toBeVisible();
- coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
- expect( coupon.id ).toBeDefined();
- } );
-
- // verify the creation of the coupon and details
- await test.step( 'verify coupon creation', async () => {
- await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].code,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].description,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].amount,
- exact: true,
- } )
- ).toBeVisible();
- } );
-
- // check expiry date if it was set
- if ( couponData[ couponType ].expiryDate ) {
- await test.step( 'verify coupon expiry date', async () => {
- await page
- .getByText( couponData[ couponType ].code )
- .last()
- .click();
- await expect(
- page.getByPlaceholder( 'yyyy-mm-dd' )
- ).toHaveValue( couponData[ couponType ].expiryDate );
- } );
- }
-
- // if it was a free shipping coupon check that
- if ( couponData[ couponType ].freeShipping ) {
- await test.step( 'verify free shipping', async () => {
- await page
- .getByText( couponData[ couponType ].code )
- .last()
- .click();
- await expect(
- page.getByLabel( 'Allow free shipping' )
- ).toBeChecked();
- } );
- }
- } );
- }
-} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-restricted-coupons.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-restricted-coupons.spec.ts
deleted file mode 100644
index ee3b894d2e0..00000000000
--- a/plugins/woocommerce/tests/e2e-pw/tests/coupons/create-restricted-coupons.spec.ts
+++ /dev/null
@@ -1,534 +0,0 @@
-/**
- * External dependencies
- */
-import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
-
-/**
- * Internal dependencies
- */
-import { expect, tags, test as baseTest } from '../../fixtures/fixtures';
-import { ADMIN_STATE_PATH } from '../../playwright.config';
-
-const couponData = {
- minimumSpend: {
- code: `minSpend-${ new Date().getTime().toString() }`,
- description: 'Minimum spend coupon',
- amount: '10',
- minSpend: '100',
- },
- maximumSpend: {
- code: `maxSpend-${ new Date().getTime().toString() }`,
- description: 'Maximum spend coupon',
- amount: '20',
- maxSpend: '100',
- },
- individualUse: {
- code: `individualUse-${ new Date().getTime().toString() }`,
- description: 'Individual use coupon',
- amount: '30',
- individualUse: true,
- },
- excludeSaleItems: {
- code: `excludeSaleItems-${ new Date().getTime().toString() }`,
- description: 'Exclude sale items coupon',
- amount: '40',
- excludeSaleItems: true,
- },
- productCategories: {
- code: `productCategories-${ new Date().getTime().toString() }`,
- description: 'Product categories coupon',
- amount: '50',
- productCategories: [ 'Uncategorized' ],
- },
- excludeProductCategories: {
- code: `excludeProductCategories-${ new Date().getTime().toString() }`,
- description: 'Exclude product categories coupon',
- amount: '60',
- excludeProductCategories: [ 'Uncategorized' ],
- },
- excludeProductBrands: {
- code: `excludeProductBrands-${ new Date().getTime().toString() }`,
- description: 'Exclude product brands coupon',
- amount: '65',
- excludeProductBrands: [
- `WooCommerce Apparels ${ new Date().getTime().toString() }`,
- ],
- },
- products: {
- code: `products-${ new Date().getTime().toString() }`,
- description: 'Products coupon',
- amount: '70',
- products: [ 'Product' ],
- },
- excludeProducts: {
- code: `excludeProducts-${ new Date().getTime().toString() }`,
- description: 'Exclude products coupon',
- amount: '80',
- excludeProducts: [ 'Product' ],
- },
- allowedEmails: {
- code: `allowedEmails-${ new Date().getTime().toString() }`,
- description: 'Allowed emails coupon',
- amount: '90',
- allowedEmails: [ 'allowed@example.com' ],
- },
- usageLimitPerCoupon: {
- code: `usageLimit-${ new Date().getTime().toString() }`,
- description: 'Usage limit coupon',
- amount: '100',
- usageLimit: '1',
- },
- usageLimitPerUser: {
- code: `usageLimitPerUser-${ new Date().getTime().toString() }`,
- description: 'Usage limit per user coupon',
- amount: '110',
- usageLimitPerUser: '2',
- },
-};
-
-const test = baseTest.extend( {
- storageState: ADMIN_STATE_PATH,
- coupon: async ( { restApi }, use ) => {
- const coupon = {};
- await use( coupon );
- await restApi.delete( `${ WC_API_PATH }/coupons/${ coupon.id }`, {
- force: true,
- } );
- },
-
- product: async ( { restApi }, use ) => {
- let product = {};
- const productName = `Product ${ Date.now() }`;
-
- await restApi
- .post( `${ WC_API_PATH }/products`, {
- name: productName,
- regular_price: '100',
- } )
- .then( ( response ) => {
- product = response.data;
- } );
-
- await use( product );
-
- // Product cleanup
- await restApi.delete( `${ WC_API_PATH }/products/${ product.id }`, {
- force: true,
- } );
- },
-
- brand: async ( { restApi }, use ) => {
- let brand = {};
-
- await restApi
- .post( `${ WC_API_PATH }/products/brands`, {
- name: couponData.excludeProductBrands.excludeProductBrands[ 0 ],
- } )
- .then( ( response ) => {
- brand = response.data;
- } );
-
- await use( brand );
-
- // Brand cleanup
- await restApi.delete(
- `${ WC_API_PATH }/products/brands/${ brand.id }`,
- { force: true }
- );
- },
-} );
-
-test.describe( 'Restricted coupon management', { tag: tags.SERVICES }, () => {
- for ( const couponType of Object.keys( couponData ) ) {
- test( `can create new ${ couponType } coupon`, async ( {
- page,
- coupon,
- product,
- brand,
- } ) => {
- // create basics for the coupon
- await test.step( 'add new coupon', async () => {
- await page.goto(
- 'wp-admin/post-new.php?post_type=shop_coupon'
- );
- await page
- .getByLabel( 'Coupon code' )
- .fill( couponData[ couponType ].code );
- await page
- .getByPlaceholder( 'Description (optional)' )
- .fill( couponData[ couponType ].description );
- await page
- .getByPlaceholder( '0' )
- .fill( couponData[ couponType ].amount );
- await expect( page.getByText( 'Move to Trash' ) ).toBeVisible();
- } );
-
- // set up the restrictions for each coupon type
- // set minimum spend
- if ( couponType === 'minimumSpend' ) {
- await test.step( 'set minimum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No minimum' )
- .fill( couponData[ couponType ].minSpend );
- } );
- }
- // set maximum spend
- if ( couponType === 'maximumSpend' ) {
- await test.step( 'set maximum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No maximum' )
- .fill( couponData[ couponType ].maxSpend );
- } );
- }
- // set individual use
- if ( couponType === 'individualUse' ) {
- await test.step( 'set individual use coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page.getByLabel( 'Individual use only' ).check();
- } );
- }
- // set exclude sale items
- if ( couponType === 'excludeSaleItems' ) {
- await test.step( 'set exclude sale items coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page.getByLabel( 'Exclude sale items' ).check();
- } );
- }
- // set product categories
- if ( couponType === 'productCategories' ) {
- await test.step( 'set product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Any category' )
- .pressSequentially( 'Uncategorized' );
- await page
- .getByRole( 'option', { name: 'Uncategorized' } )
- .click();
- } );
- }
- // set exclude product categories
- if ( couponType === 'excludeProductCategories' ) {
- await test.step( 'set exclude product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No categories' )
- .pressSequentially( 'Uncategorized' );
- await page
- .getByRole( 'option', { name: 'Uncategorized' } )
- .click();
- } );
- }
-
- // set exclude product brands
- if ( couponType === 'excludeProductBrands' ) {
- await test.step( 'set exclude product brands coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No brands' )
- .pressSequentially( brand.name );
- await page
- .getByRole( 'option', {
- name: brand.name,
- } )
- .click();
- } );
- }
- // set products
- if ( couponType === 'products' ) {
- await test.step( 'set products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Search for a product…' )
- .first()
- .pressSequentially( product.name );
- await page
- .getByRole( 'option', { name: product.name } )
- .click();
- } );
- }
- // set exclude products
- if ( couponType === 'excludeProducts' ) {
- await test.step( 'set exclude products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'Search for a product…' )
- .last()
- .pressSequentially( product.name );
- await page
- .getByRole( 'option', { name: product.name } )
- .click();
- } );
- }
- // set allowed emails
- if ( couponType === 'allowedEmails' ) {
- await test.step( 'set allowed emails coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await page
- .getByPlaceholder( 'No restrictions' )
- .fill( couponData[ couponType ].allowedEmails[ 0 ] );
- } );
- }
- // set usage limit
- if ( couponType === 'usageLimitPerCoupon' ) {
- await test.step( 'set usage limit coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await page
- .getByLabel( 'Usage limit per coupon' )
- .fill( couponData[ couponType ].usageLimit );
- } );
- }
- // set usage limit per user
- if ( couponType === 'usageLimitPerUser' ) {
- await test.step( 'set usage limit per user coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await page
- .getByLabel( 'Usage limit per user' )
- .fill( couponData[ couponType ].usageLimitPerUser );
- } );
- }
-
- // publish the coupon and retrieve the id
- await test.step( 'publish the coupon', async () => {
- await page
- .getByRole( 'button', { name: 'Publish', exact: true } )
- .click();
- await expect(
- page.getByText( 'Coupon updated.' )
- ).toBeVisible();
- coupon.id = page.url().match( /(?<=post=)\d+/ )[ 0 ];
- expect( coupon.id ).toBeDefined();
- } );
-
- // verify the creation of the coupon and basic details
- await test.step( 'verify coupon creation', async () => {
- await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].code,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].description,
- } )
- ).toBeVisible();
- await expect(
- page.getByRole( 'cell', {
- name: couponData[ couponType ].amount,
- exact: true,
- } )
- ).toBeVisible();
-
- await page
- .getByRole( 'link', {
- name: couponData[ couponType ].code,
- } )
- .first()
- .click();
- } );
-
- // verify the restrictions for each coupon type
- // verify minimum spend
- if ( couponType === 'minimumSpend' ) {
- await test.step( 'verify minimum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No minimum' )
- ).toHaveValue( couponData[ couponType ].minSpend );
- } );
- }
-
- // verify maximum spend
- if ( couponType === 'maximumSpend' ) {
- await test.step( 'verify maximum spend coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No maximum' )
- ).toHaveValue( couponData[ couponType ].maxSpend );
- } );
- }
-
- // verify individual use
- if ( couponType === 'individualUse' ) {
- await test.step( 'verify individual use coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByLabel( 'Individual use only' )
- ).toBeChecked();
- } );
- }
-
- // verify exclude sale items
- if ( couponType === 'excludeSaleItems' ) {
- await test.step( 'verify exclude sale items coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByLabel( 'Exclude sale items' )
- ).toBeChecked();
- } );
- }
-
- // verify product categories
- if ( couponType === 'productCategories' ) {
- await test.step( 'verify product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', {
- name: 'Uncategorized',
- } )
- ).toBeVisible();
- } );
- }
-
- // verify exclude product categories
- if ( couponType === 'excludeProductCategories' ) {
- await test.step( 'verify exclude product categories coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', {
- name: 'Uncategorized',
- } )
- ).toBeVisible();
- } );
- }
-
- // verify products
- if ( couponType === 'products' ) {
- await test.step( 'verify products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', { name: product.name } )
- ).toBeVisible();
- } );
- }
-
- // verify exclude products
- if ( couponType === 'excludeProducts' ) {
- await test.step( 'verify exclude products coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByRole( 'listitem', { name: product.name } )
- ).toBeVisible();
- } );
- }
-
- // verify allowed emails
- if ( couponType === 'allowedEmails' ) {
- await test.step( 'verify allowed emails coupon', async () => {
- await page
- .getByRole( 'link', {
- name: 'Usage restriction',
- } )
- .click();
- await expect(
- page.getByPlaceholder( 'No restrictions' )
- ).toHaveValue(
- couponData[ couponType ].allowedEmails[ 0 ]
- );
- } );
- }
-
- // verify usage limit
- if ( couponType === 'usageLimitPerCoupon' ) {
- await test.step( 'verify usage limit coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await expect(
- page.getByLabel( 'Usage limit per coupon' )
- ).toHaveValue( couponData[ couponType ].usageLimit );
- } );
- }
-
- // verify usage limit per user
- if ( couponType === 'usageLimitPerUser' ) {
- await test.step( 'verify usage limit per user coupon', async () => {
- await page
- .getByRole( 'link', { name: 'Usage limits' } )
- .click();
- await expect(
- page.getByLabel( 'Usage limit per user' )
- ).toHaveValue( couponData[ couponType ].usageLimitPerUser );
- } );
- }
- } );
- }
-} );
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php
index 5ca5953daf5..60691f69c7a 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-brands-test.php
@@ -113,4 +113,42 @@ class WC_Admin_Brands_Test extends WC_Unit_Test_Case {
$output
);
}
+
+ /**
+ * @testdox save_coupon_brands() persists posted coupon brand restrictions.
+ */
+ public function test_save_coupon_brands_persists_posted_coupon_brand_restrictions(): void {
+ WC_Brands::init_taxonomy();
+
+ $included_brand_id = $this->factory()->term->create(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'name' => 'Included brand',
+ )
+ );
+ $excluded_brand_id = $this->factory()->term->create(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'name' => 'Excluded brand',
+ )
+ );
+ $coupon = WC_Helper_Coupon::create_coupon( 'brand-restrictions' );
+ $brands_admin = new WC_Brands_Admin();
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Preserves test fixture state.
+ $previous_post = $_POST;
+ $_POST = array(
+ 'product_brands' => array( (string) $included_brand_id ),
+ 'exclude_product_brands' => array( (string) $excluded_brand_id ),
+ );
+
+ try {
+ $brands_admin->save_coupon_brands( $coupon->get_id() );
+ } finally {
+ $_POST = $previous_post;
+ }
+
+ $this->assertSame( array( $included_brand_id ), get_post_meta( $coupon->get_id(), 'product_brands', true ), 'Expected included brand restrictions to persist.' );
+ $this->assertSame( array( $excluded_brand_id ), get_post_meta( $coupon->get_id(), 'exclude_product_brands', true ), 'Expected excluded brand restrictions to persist.' );
+ }
}
diff --git a/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-coupon-data-test.php b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-coupon-data-test.php
new file mode 100644
index 00000000000..182e55af8c1
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-coupon-data-test.php
@@ -0,0 +1,198 @@
+<?php
+
+declare( strict_types = 1 );
+
+/**
+ * Tests for the coupon data meta box.
+ */
+class WC_Meta_Box_Coupon_Data_Test extends WC_Unit_Test_Case {
+ /**
+ * Previously posted form data.
+ *
+ * @var array
+ */
+ private $previous_post;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function set_up(): void {
+ parent::set_up();
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Preserves test fixture state.
+ $this->previous_post = $_POST;
+
+ WC_Admin_Meta_Boxes::$meta_box_errors = array();
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tear_down(): void {
+ $_POST = $this->previous_post;
+
+ WC_Admin_Meta_Boxes::$meta_box_errors = array();
+
+ parent::tear_down();
+ }
+
+ /**
+ * @testdox save() registers coupon validation errors from posted metabox data.
+ */
+ public function test_save_registers_validation_errors_from_posted_data(): void {
+ $post_id = $this->create_coupon_post( 'metabox-invalid-amount' );
+ $post = $this->get_coupon_post( $post_id );
+
+ $this->set_coupon_post_data(
+ array(
+ 'coupon_amount' => '-1',
+ )
+ );
+
+ WC_Meta_Box_Coupon_Data::save( $post_id, $post );
+
+ $this->assertContains(
+ 'Invalid discount amount.',
+ WC_Admin_Meta_Boxes::$meta_box_errors,
+ 'Expected the metabox save handler to register model validation errors.'
+ );
+ }
+
+ /**
+ * @testdox save() registers an error when a coupon code already exists.
+ */
+ public function test_save_registers_duplicate_coupon_code_error(): void {
+ WC_Helper_Coupon::create_coupon( 'duplicate-metabox-code' );
+
+ $post_id = $this->create_coupon_post( 'duplicate-metabox-code' );
+ $post = $this->get_coupon_post( $post_id );
+
+ $this->set_coupon_post_data();
+
+ WC_Meta_Box_Coupon_Data::save( $post_id, $post );
+
+ $this->assertContains(
+ 'Coupon code already exists - customers will use the latest coupon with this code.',
+ WC_Admin_Meta_Boxes::$meta_box_errors,
+ 'Expected the metabox save handler to register duplicate coupon code errors.'
+ );
+ }
+
+ /**
+ * @testdox save() persists coupon fields from posted metabox data.
+ */
+ public function test_save_persists_posted_coupon_fields(): void {
+ $post_id = $this->create_coupon_post( 'metabox-persisted-fields' );
+ $post = $this->get_coupon_post( $post_id );
+ $product = WC_Helper_Product::create_simple_product();
+ $excluded_product = WC_Helper_Product::create_simple_product();
+ $product_category_id = $this->factory()->term->create(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'name' => 'Included coupon category',
+ )
+ );
+ $excluded_category_id = $this->factory()->term->create(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'name' => 'Excluded coupon category',
+ )
+ );
+
+ $this->set_coupon_post_data(
+ array(
+ 'discount_type' => 'fixed_product',
+ 'coupon_amount' => '25',
+ 'expiry_date' => '2026-12-31',
+ 'free_shipping' => 'yes',
+ 'individual_use' => 'yes',
+ 'exclude_sale_items' => 'yes',
+ 'minimum_amount' => '50',
+ 'maximum_amount' => '500',
+ 'usage_limit' => '7',
+ 'usage_limit_per_user' => '2',
+ 'limit_usage_to_x_items' => '3',
+ 'customer_email' => 'customer@example.com, vip@example.com',
+ 'product_ids' => array( (string) $product->get_id() ),
+ 'exclude_product_ids' => array( (string) $excluded_product->get_id() ),
+ 'product_categories' => array( (string) $product_category_id ),
+ 'exclude_product_categories' => array( (string) $excluded_category_id ),
+ )
+ );
+
+ WC_Meta_Box_Coupon_Data::save( $post_id, $post );
+
+ $coupon = new WC_Coupon( $post_id );
+ $date_expires = $coupon->get_date_expires();
+
+ $this->assertSame( 'fixed_product', $coupon->get_discount_type(), 'Expected the posted discount type to persist.' );
+ $this->assertSame( '25', $coupon->get_amount(), 'Expected the posted amount to persist.' );
+ $this->assertInstanceOf( WC_DateTime::class, $date_expires, 'Expected the posted expiry date to persist.' );
+ $this->assertSame( '2026-12-31', $date_expires->format( 'Y-m-d' ), 'Expected the posted expiry date to persist.' );
+ $this->assertTrue( $coupon->get_free_shipping(), 'Expected the posted free shipping flag to persist.' );
+ $this->assertTrue( $coupon->get_individual_use(), 'Expected the posted individual use flag to persist.' );
+ $this->assertTrue( $coupon->get_exclude_sale_items(), 'Expected the posted exclude sale items flag to persist.' );
+ $this->assertSame( '50', $coupon->get_minimum_amount(), 'Expected the posted minimum amount to persist.' );
+ $this->assertSame( '500', $coupon->get_maximum_amount(), 'Expected the posted maximum amount to persist.' );
+ $this->assertSame( 7, $coupon->get_usage_limit(), 'Expected the posted usage limit to persist.' );
+ $this->assertSame( 2, $coupon->get_usage_limit_per_user(), 'Expected the posted per-user usage limit to persist.' );
+ $this->assertSame( 3, $coupon->get_limit_usage_to_x_items(), 'Expected the posted item usage limit to persist.' );
+ $this->assertSame( array( 'customer@example.com', 'vip@example.com' ), $coupon->get_email_restrictions(), 'Expected the posted email restrictions to persist.' );
+ $this->assertSame( array( $product->get_id() ), $coupon->get_product_ids(), 'Expected the posted product restrictions to persist.' );
+ $this->assertSame( array( $excluded_product->get_id() ), $coupon->get_excluded_product_ids(), 'Expected the posted excluded product restrictions to persist.' );
+ $this->assertSame( array( $product_category_id ), $coupon->get_product_categories(), 'Expected the posted category restrictions to persist.' );
+ $this->assertSame( array( $excluded_category_id ), $coupon->get_excluded_product_categories(), 'Expected the posted excluded category restrictions to persist.' );
+ }
+
+ /**
+ * Create a coupon post for metabox save tests.
+ *
+ * @param string $coupon_code Coupon code.
+ * @return int
+ */
+ private function create_coupon_post( string $coupon_code ): int {
+ return $this->factory()->post->create(
+ array(
+ 'post_title' => $coupon_code,
+ 'post_type' => 'shop_coupon',
+ 'post_status' => 'publish',
+ )
+ );
+ }
+
+ /**
+ * Get a coupon post for metabox save tests.
+ *
+ * @param int $post_id Coupon post ID.
+ * @return WP_Post
+ */
+ private function get_coupon_post( int $post_id ): WP_Post {
+ $post = get_post( $post_id );
+
+ $this->assertInstanceOf( WP_Post::class, $post, 'Expected the coupon post fixture to exist.' );
+
+ return $post;
+ }
+
+ /**
+ * Set posted coupon metabox data.
+ *
+ * @param array $overrides Posted data overrides.
+ */
+ private function set_coupon_post_data( array $overrides = array() ): void {
+ $_POST = array_merge(
+ array(
+ 'discount_type' => 'fixed_cart',
+ 'coupon_amount' => '10',
+ 'expiry_date' => '',
+ 'usage_limit' => '',
+ 'usage_limit_per_user' => '',
+ 'limit_usage_to_x_items' => '',
+ 'minimum_amount' => '',
+ 'maximum_amount' => '',
+ 'customer_email' => '',
+ ),
+ $overrides
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php
index 1c53cc88c15..4a4c043e1b4 100644
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php
+++ b/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php
@@ -117,6 +117,37 @@ class CreateCouponTest extends WC_Unit_Test_Case {
$this->assertSame( 'percent', $wc_coupon->get_discount_type() );
}
+ /**
+ * @testdox execute() persists fixed discount type enums when provided.
+ *
+ * @dataProvider fixed_discount_type_provider
+ *
+ * @param DiscountType $discount_type The discount type enum.
+ * @param string $expected The expected stored discount type.
+ */
+ public function test_execute_persists_fixed_discount_type_enums( DiscountType $discount_type, string $expected ): void {
+ $input = new CreateCouponInput();
+ $input->code = 'coupon-' . str_replace( '_', '-', $expected );
+ $input->discount_type = $discount_type;
+
+ $result = $this->sut->execute( $input );
+
+ $wc_coupon = new WC_Coupon( $result->id );
+ $this->assertSame( $expected, $wc_coupon->get_discount_type() );
+ }
+
+ /**
+ * Data provider for fixed discount types.
+ *
+ * @return array<string, array{0: DiscountType, 1: string}>
+ */
+ public function fixed_discount_type_provider(): array {
+ return array(
+ 'fixed cart' => array( DiscountType::FixedCart, 'fixed_cart' ),
+ 'fixed product' => array( DiscountType::FixedProduct, 'fixed_product' ),
+ );
+ }
+
/**
* @testdox execute() skips set_discount_type() when discount_type is null.
*/