Validate and present custom error for not in allowed emails coupons (#43872)
* Removed deprecated WC_COUPON::is_valid() method usage from CartController.php
* Reverted wrongly changed line.
* Added validate_coupon_allowed_emails() to WC_Discounts
* Added soft validation for allowed emails coupons, with custom notice via WC_Coupon::add_coupon_message()
* Fixed log warning
* Refactored add_coupon_message()
* Prevent duplicate coupon notices.
* Changed coupon soft validation notice type.
* Tweaks
* Run coupon soft validations only on cart validation.
* Reverted soft validation, and added email information for coupon validation
* Removed unused coupon message
* PHP lint fixes.
* Added changelog.
* PHP lint fix
* Updated allowed coupon validation error message
* Updated PW tests
* Updated PW tests
* Updated email restricted coupon message.
* Small change for readability.
* Different error messages for shortcode cart and shortcode checkout
* Simplified CartApplyCoupon::get_post_route_response()
* Revert "Simplified CartApplyCoupon::get_post_route_response()"
This reverts commit 43f185b59a
.
* Expose additional error data in error API response
* Simplified AbstractCartRoute::get_route_error_response()
* Linting
* Restored comment deleted by mistake.
* Introduced API context based coupon errors
* Fixed Doc Block
* Linting
* Reverted deprecated method removal
* Reverted deprecated method removal
* WIP
* Display context based errors on cart and checkout for allowed emails coupons.
* Small code fixes.
* Removed coupon_error_code from api response.
* Tweaks and used 'details' on the API response
* Fixed indent.
* Set coupon errors using the validation store rather than local state
* Revert import to original state.
* Updated tests.
* Updated tests.
* Simplified comments
* Added testing for Cart page
* Lint fixes
---------
Co-authored-by: Alex Florisca <alex.florisca@automattic.com>
This commit is contained in:
parent
211d39d453
commit
0a3cf74c06
|
@ -3,9 +3,13 @@
|
|||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import {
|
||||
CART_STORE_KEY,
|
||||
VALIDATION_STORE_KEY,
|
||||
CHECKOUT_STORE_KEY,
|
||||
} from '@woocommerce/block-data';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import type { StoreCartCoupon } from '@woocommerce/types';
|
||||
import type { StoreCartCoupon, ApiErrorResponse } from '@woocommerce/types';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
|
@ -41,6 +45,19 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
|
|||
);
|
||||
|
||||
const { applyCoupon, removeCoupon } = useDispatch( CART_STORE_KEY );
|
||||
const orderId = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).getOrderId()
|
||||
);
|
||||
|
||||
// Return cart, checkout or generic error message.
|
||||
const getCouponErrorMessage = ( error: ApiErrorResponse ) => {
|
||||
if ( orderId && orderId > 0 && error?.data?.details?.checkout ) {
|
||||
return error.data.details.checkout;
|
||||
} else if ( error?.data?.details?.cart ) {
|
||||
return error.data.details.cart;
|
||||
}
|
||||
return error.message;
|
||||
};
|
||||
|
||||
const applyCouponWithNotices = ( couponCode: string ) => {
|
||||
return applyCoupon( couponCode )
|
||||
|
@ -72,9 +89,10 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
|
|||
return Promise.resolve( true );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
const errorMessage = getCouponErrorMessage( error );
|
||||
setValidationErrors( {
|
||||
coupon: {
|
||||
message: decodeEntities( error.message ),
|
||||
message: decodeEntities( errorMessage ), // TODO fix the circular loop with ApiErrorResponseData and ApiErrorResponseDataDetails
|
||||
hidden: false,
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Validate coupons with email restrictions upfront and change user's feedback when a coupon is not valid for the user.
|
|
@ -617,8 +617,9 @@ jQuery( function( $ ) {
|
|||
});
|
||||
|
||||
var data = {
|
||||
security: wc_checkout_params.apply_coupon_nonce,
|
||||
coupon_code: $form.find( 'input[name="coupon_code"]' ).val()
|
||||
security: wc_checkout_params.apply_coupon_nonce,
|
||||
coupon_code: $form.find('input[name="coupon_code"]').val(),
|
||||
billing_email: wc_checkout_form.$checkout_form.find('input[name="billing_email"]').val()
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
|
|
|
@ -245,7 +245,13 @@ class WC_AJAX {
|
|||
|
||||
check_ajax_referer( 'apply-coupon', 'security' );
|
||||
|
||||
$coupon_code = ArrayUtil::get_value_or_default( $_POST, 'coupon_code' );
|
||||
$coupon_code = ArrayUtil::get_value_or_default( $_POST, 'coupon_code' );
|
||||
$billing_email = ArrayUtil::get_value_or_default( $_POST, 'billing_email' );
|
||||
|
||||
if ( is_email( $billing_email ) ) {
|
||||
wc()->customer->set_billing_email( $billing_email );
|
||||
}
|
||||
|
||||
if ( ! StringUtil::is_null_or_whitespace( $coupon_code ) ) {
|
||||
WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $coupon_code ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
} else {
|
||||
|
|
|
@ -944,20 +944,27 @@ class WC_Coupon extends WC_Legacy_Coupon {
|
|||
* Converts one of the WC_Coupon message/error codes to a message string and.
|
||||
* displays the message/error.
|
||||
*
|
||||
* @param int $msg_code Message/error code.
|
||||
* @param int $msg_code Message/error code.
|
||||
* @param string $notice_type Notice type.
|
||||
*/
|
||||
public function add_coupon_message( $msg_code ) {
|
||||
$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
|
||||
public function add_coupon_message( $msg_code, $notice_type = 'success' ) {
|
||||
if ( $msg_code < 200 ) {
|
||||
$msg = $this->get_coupon_error( $msg_code );
|
||||
$notice_type = 'error';
|
||||
} else {
|
||||
$msg = $this->get_coupon_message( $msg_code );
|
||||
}
|
||||
|
||||
if ( ! $msg ) {
|
||||
if ( empty( $msg ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $msg_code < 200 ) {
|
||||
wc_add_notice( $msg, 'error' );
|
||||
} else {
|
||||
wc_add_notice( $msg );
|
||||
// Since coupon validation is done multiple times (e.g. to ensure a valid cart), we need to check for dupes.
|
||||
if ( wc_has_notice( $msg, $notice_type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wc_add_notice( $msg, $notice_type );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1001,8 +1008,15 @@ class WC_Coupon extends WC_Legacy_Coupon {
|
|||
$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
|
||||
break;
|
||||
case self::E_WC_COUPON_NOT_YOURS_REMOVED:
|
||||
/* translators: %s: coupon code */
|
||||
$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
|
||||
// We check for supplied billing email. On shortcode, this will be present for checkout requests.
|
||||
$billing_email = \Automattic\WooCommerce\Utilities\ArrayUtil::get_value_or_default( $_POST, 'billing_email' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
if ( ! is_null( $billing_email ) ) {
|
||||
/* translators: %s: coupon code */
|
||||
$err = sprintf( __( 'Please enter a valid email to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) );
|
||||
} else {
|
||||
/* translators: %s: coupon code */
|
||||
$err = sprintf( __( 'Please enter a valid email at checkout to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) );
|
||||
}
|
||||
break;
|
||||
case self::E_WC_COUPON_ALREADY_APPLIED:
|
||||
$err = __( 'Coupon code already applied!', 'woocommerce' );
|
||||
|
@ -1152,4 +1166,26 @@ class WC_Coupon extends WC_Legacy_Coupon {
|
|||
$this->set_amount( $info[3] ?? 0 );
|
||||
$this->set_free_shipping( $info[4] ?? false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns alternate error messages based on context (eg. Cart and Checkout).
|
||||
*
|
||||
* @param int $err_code Message/error code.
|
||||
*
|
||||
* @return array Context based alternate error messages.
|
||||
*/
|
||||
public function get_context_based_coupon_errors( $err_code = null ) {
|
||||
|
||||
switch ( $err_code ) {
|
||||
case self::E_WC_COUPON_NOT_YOURS_REMOVED:
|
||||
return array(
|
||||
/* translators: %s: coupon code */
|
||||
'cart' => sprintf( __( 'Please enter a valid email at checkout to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) ),
|
||||
/* translators: %s: coupon code */
|
||||
'checkout' => sprintf( __( 'Please enter a valid email to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) ),
|
||||
);
|
||||
default:
|
||||
return array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -939,6 +939,50 @@ class WC_Discounts {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure coupon is valid for allowed emails or throw exception.
|
||||
*
|
||||
* @since 8.6.0
|
||||
* @throws Exception Error message.
|
||||
* @param WC_Coupon $coupon Coupon data.
|
||||
* @return bool
|
||||
*/
|
||||
protected function validate_coupon_allowed_emails( $coupon ) {
|
||||
|
||||
$restrictions = $coupon->get_email_restrictions();
|
||||
|
||||
if ( ! is_array( $restrictions ) || empty( $restrictions ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$check_emails = array( $user->get_billing_email(), $user->get_email() );
|
||||
|
||||
if ( $this->object instanceof WC_Cart ) {
|
||||
$check_emails[] = $this->object->get_customer()->get_billing_email();
|
||||
} elseif ( $this->object instanceof WC_Order ) {
|
||||
$check_emails[] = $this->object->get_billing_email();
|
||||
}
|
||||
|
||||
$check_emails = array_unique(
|
||||
array_filter(
|
||||
array_map(
|
||||
'strtolower',
|
||||
array_map(
|
||||
'sanitize_email',
|
||||
$check_emails
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! WC()->cart->is_coupon_emails_allowed( $check_emails, $restrictions ) ) {
|
||||
throw new Exception( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ), WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object subtotal
|
||||
*
|
||||
|
@ -981,10 +1025,11 @@ class WC_Discounts {
|
|||
* - 113: Excluded products.
|
||||
* - 114: Excluded categories.
|
||||
*
|
||||
* @since 3.2.0
|
||||
* @throws Exception Error message.
|
||||
* @param WC_Coupon $coupon Coupon data.
|
||||
* @param WC_Coupon $coupon Coupon data.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
* @throws Exception Error message.
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public function is_coupon_valid( $coupon ) {
|
||||
try {
|
||||
|
@ -998,9 +1043,10 @@ class WC_Discounts {
|
|||
$this->validate_coupon_product_categories( $coupon );
|
||||
$this->validate_coupon_excluded_items( $coupon );
|
||||
$this->validate_coupon_eligible_items( $coupon );
|
||||
$this->validate_coupon_allowed_emails( $coupon );
|
||||
|
||||
if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) {
|
||||
throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 );
|
||||
throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), WC_Coupon::E_WC_COUPON_INVALID_FILTERED );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
/**
|
||||
|
@ -1012,14 +1058,23 @@ class WC_Discounts {
|
|||
*/
|
||||
$message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon );
|
||||
|
||||
$additional_data = array(
|
||||
'status' => 400,
|
||||
);
|
||||
|
||||
$context_coupon_errors = $coupon->get_context_based_coupon_errors( $e->getCode() );
|
||||
|
||||
if ( ! empty( $context_coupon_errors ) ) {
|
||||
$additional_data['details'] = $context_coupon_errors;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'invalid_coupon',
|
||||
$message,
|
||||
array(
|
||||
'status' => 400,
|
||||
)
|
||||
$additional_data,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,24 +332,15 @@ abstract class AbstractCartRoute extends AbstractRoute {
|
|||
* @return \WP_Error WP Error object.
|
||||
*/
|
||||
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
|
||||
switch ( $http_status_code ) {
|
||||
case 409:
|
||||
// If there was a conflict, return the cart so the client can resolve it.
|
||||
$cart = $this->cart_controller->get_cart_instance();
|
||||
|
||||
return new \WP_Error(
|
||||
$error_code,
|
||||
$error_message,
|
||||
array_merge(
|
||||
$additional_data,
|
||||
[
|
||||
'status' => $http_status_code,
|
||||
'cart' => $this->cart_schema->get_item_response( $cart ),
|
||||
]
|
||||
)
|
||||
);
|
||||
$additional_data['status'] = $http_status_code;
|
||||
|
||||
// If there was a conflict, return the cart so the client can resolve it.
|
||||
if ( 409 === $http_status_code ) {
|
||||
$cart = $this->cart_controller->get_cart_instance();
|
||||
$additional_data['cart'] = $this->cart_schema->get_item_response( $cart );
|
||||
}
|
||||
|
||||
return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] );
|
||||
return new \WP_Error( $error_code, $error_message, $additional_data );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ class CartApplyCoupon extends AbstractCartRoute {
|
|||
throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 );
|
||||
}
|
||||
|
||||
$cart = $this->cart_controller->get_cart_instance();
|
||||
$coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) );
|
||||
|
||||
try {
|
||||
|
@ -67,6 +66,7 @@ class CartApplyCoupon extends AbstractCartRoute {
|
|||
throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() );
|
||||
}
|
||||
|
||||
$cart = $this->cart_controller->get_cart_instance();
|
||||
return rest_ensure_response( $this->schema->get_item_response( $cart ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -928,11 +928,15 @@ class CartController {
|
|||
);
|
||||
}
|
||||
|
||||
if ( ! $coupon->is_valid() ) {
|
||||
$discounts = new \WC_Discounts( $this->get_cart_instance() );
|
||||
$valid = $discounts->is_coupon_valid( $coupon );
|
||||
|
||||
if ( is_wp_error( $valid ) ) {
|
||||
throw new RouteException(
|
||||
'woocommerce_rest_cart_coupon_error',
|
||||
wp_strip_all_tags( $coupon->get_error_message() ),
|
||||
400
|
||||
esc_html( wp_strip_all_tags( $valid->get_error_message() ) ),
|
||||
400,
|
||||
$valid->get_error_data() // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1013,9 +1017,9 @@ class CartController {
|
|||
/**
|
||||
* Validates an existing cart coupon and returns any errors.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*
|
||||
* @param \WC_Coupon $coupon Coupon object applied to the cart.
|
||||
*
|
||||
* @throws RouteException Exception if invalid data is detected.
|
||||
*/
|
||||
protected function validate_cart_coupon( \WC_Coupon $coupon ) {
|
||||
if ( ! $coupon->is_valid() ) {
|
||||
|
|
|
@ -624,22 +624,30 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
test( 'coupon cannot be used by any customer (email restricted)', async ( {
|
||||
test( 'coupon cannot be used by any customer on cart (email restricted)', async ( {
|
||||
page,
|
||||
} ) => {
|
||||
await page.goto( `/shop/?add-to-cart=${ firstProductId }` );
|
||||
await page.waitForLoadState( 'networkidle' );
|
||||
|
||||
await page.goto( '/cart/' );
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Please enter a valid email at checkout to use coupon code "email-restricted".'
|
||||
)
|
||||
).toBeVisible();
|
||||
} );
|
||||
|
||||
test( 'coupon cannot be used by any customer on checkout (email restricted)', async ( {
|
||||
page,
|
||||
} ) => {
|
||||
await page.goto( `/shop/?add-to-cart=${ firstProductId }` );
|
||||
await page.waitForLoadState( 'networkidle' );
|
||||
|
||||
await page.goto( '/checkout/' );
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
// succeeded so far because we don't know who the customr is
|
||||
await expect(
|
||||
page.getByText( 'Coupon code applied successfully.' )
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel( 'First name' ).first().fill( 'Marge' );
|
||||
await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
|
||||
|
@ -654,11 +662,16 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
.getByLabel( 'Email address' )
|
||||
.first()
|
||||
.fill( 'marge@example.com' );
|
||||
await page.getByRole( 'button', { name: 'Place order' } ).click();
|
||||
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Sorry, it seems the coupon "email-restricted" is not yours - it has now been removed from your order.'
|
||||
'Please enter a valid email to use coupon code "email-restricted".'
|
||||
)
|
||||
).toBeVisible();
|
||||
} );
|
||||
|
@ -678,14 +691,6 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
await page.waitForLoadState( 'networkidle' );
|
||||
|
||||
await page.goto( '/checkout/' );
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
await expect(
|
||||
page.getByText( 'Coupon code applied successfully.' )
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel( 'First name' ).first().fill( 'Homer' );
|
||||
await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
|
||||
|
@ -700,6 +705,16 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
.getByLabel( 'Email address' )
|
||||
.first()
|
||||
.fill( 'homer@example.com' );
|
||||
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
await expect(
|
||||
page.getByText( 'Coupon code applied successfully.' )
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole( 'button', { name: 'Place order' } ).click();
|
||||
|
||||
await expect(
|
||||
|
@ -714,15 +729,6 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
await page.waitForLoadState( 'networkidle' );
|
||||
|
||||
await page.goto( '/checkout/' );
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
// succeeded so far because we don't know who the customr is
|
||||
await expect(
|
||||
page.getByText( 'Coupon code applied successfully.' )
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel( 'First name' ).first().fill( 'Homer' );
|
||||
await page.getByLabel( 'Last name' ).first().fill( 'Simpson' );
|
||||
|
@ -737,6 +743,16 @@ test.describe( 'Cart & Checkout Restricted Coupons', () => {
|
|||
.getByLabel( 'Email address' )
|
||||
.first()
|
||||
.fill( 'homer@example.com' );
|
||||
|
||||
await page
|
||||
.getByRole( 'link', { name: 'Click here to enter your code' } )
|
||||
.click();
|
||||
await page.getByPlaceholder( 'Coupon code' ).fill( 'email-restricted' );
|
||||
await page.getByRole( 'button', { name: 'Apply coupon' } ).click();
|
||||
await expect(
|
||||
page.getByText( 'Coupon code applied successfully.' )
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole( 'button', { name: 'Place order' } ).click();
|
||||
|
||||
await expect(
|
||||
|
|
Loading…
Reference in New Issue