Merge branch 'trunk' into fix/27873

This commit is contained in:
Ron Rennick 2021-03-05 23:47:41 -04:00
commit 7261813fc9
38 changed files with 870 additions and 109 deletions

View File

@ -0,0 +1,52 @@
/**
* dashboard-setup.scss
* Styles for WooCommerce dashboard finish setup widgets
* only loaded on the dashboard itself.
*/
/**
* Styling begins
*/
.dashboard-widget-finish-setup {
.progress-wrapper {
border: 1px solid #757575;
border-radius: 16px;
font-size: 0.9em;
padding: 2px 8px 2px 8px;
display: inline-block;
box-sizing: border-box;
}
.progress-wrapper span {
position: relative;
top: -3px;
color: #757575;
}
.description div {
margin-top: 11px;
float: left;
width: 70%;
}
.description img {
float: right;
width: 30%;
}
.circle-progress {
margin-top: 1px;
margin-left: -3px;
circle {
stroke: #f0f0f0;
stroke-width: 1px;
}
.bar {
stroke: #949494;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -2,7 +2,7 @@
/**
* Abstract payment tokens
*
* Generic payment tokens functionality which can be extended by idividual types of payment tokens.
* Generic payment tokens functionality which can be extended by individual types of payment tokens.
*
* @class WC_Payment_Token
* @package WooCommerce\Abstracts

View File

@ -0,0 +1,211 @@
<?php
/**
* Admin Dashboard - Setup
*
* @package WooCommerce\Admin
* @version 2.1.0
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
/**
* WC_Admin_Dashboard_Setup Class.
*/
class WC_Admin_Dashboard_Setup {
/**
* List of tasks.
*
* @var array
*/
private $tasks = array(
'store_details' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&path=%2Fsetup-wizard',
),
'products' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=products',
),
'woocommerce-payments' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&path=%2Fpayments%2Fconnect',
),
'payments' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=payments',
),
'tax' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=tax',
),
'shipping' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=shipping',
),
'appearance' => array(
'completed' => false,
'button_link' => 'admin.php?page=wc-admin&task=appearance',
),
);
/**
* # of completed tasks.
*
* @var int
*/
private $completed_tasks_count = 0;
/**
* WC_Admin_Dashboard_Setup constructor.
*/
public function __construct() {
if ( $this->should_display_widget() ) {
$this->populate_general_tasks();
$this->populate_payment_tasks();
$this->completed_tasks_count = $this->get_completed_tasks_count();
add_meta_box(
'wc_admin_dashboard_setup',
__( 'WooCommerce Setup', 'woocommerce' ),
array( $this, 'render' ),
'dashboard',
'normal',
'high'
);
}
}
/**
* Render meta box output.
*/
public function render() {
$version = Constants::get_constant( 'WC_VERSION' );
wp_enqueue_style( 'wc-dashboard-setup', WC()->plugin_url() . '/assets/css/dashboard-setup.css', array(), $version );
$task = $this->get_next_task();
if ( ! $task ) {
return;
}
$button_link = $task['button_link'];
$completed_tasks_count = $this->completed_tasks_count;
$tasks_count = count( $this->tasks );
// Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2).
$progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100;
$circle_r = 6.5;
$circle_dashoffset = ( ( 100 - $progress_percentage ) / 100 ) * ( pi() * ( $circle_r * 2 ) );
include __DIR__ . '/views/html-admin-dashboard-setup.php';
}
/**
* Populate tasks from the database.
*/
private function populate_general_tasks() {
$tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() );
foreach ( $tasks as $task ) {
if ( isset( $this->tasks[ $task ] ) ) {
$this->tasks[ $task ]['completed'] = true;
$this->tasks[ $task ]['button_link'] = wc_admin_url( $this->tasks[ $task ]['button_link'] );
}
}
}
/**
* Getter for $tasks
*
* @return array
*/
public function get_tasks() {
return $this->tasks;
}
/**
* Return # of completed tasks
*/
public function get_completed_tasks_count() {
$completed_tasks = array_filter(
$this->tasks,
function( $task ) {
return $task['completed'];
}
);
return count( $completed_tasks );
}
/**
* Get the next task.
*
* @return array|null
*/
private function get_next_task() {
foreach ( $this->get_tasks() as $task ) {
if ( false === $task['completed'] ) {
return $task;
}
}
return null;
}
/**
* Check to see if we should display the widget
*
* @return bool
*/
private function should_display_widget() {
return 'yes' !== get_option( 'woocommerce_task_list_complete' ) && 'yes' !== get_option( 'woocommerce_task_list_hidden' );
}
/**
* Populate payment tasks's visibility and completion
*/
private function populate_payment_tasks() {
$is_woo_payment_installed = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
$country = explode( ':', get_option( 'woocommerce_default_country', '' ) )[0];
// woocommerce-payments requires its plugin activated and country must be US.
if ( ! $is_woo_payment_installed || 'US' !== $country ) {
unset( $this->tasks['woocommerce-payments'] );
}
// payments can't be used when woocommerce-payments exists and country is US.
if ( $is_woo_payment_installed || 'US' === $country ) {
unset( $this->tasks['payments'] );
}
if ( isset( $this->tasks['payments'] ) ) {
$gateways = WC()->payment_gateways->get_available_payment_gateways();
$enabled_gateways = array_filter(
$gateways,
function ( $gateway ) {
return 'yes' === $gateway->enabled;
}
);
$this->tasks['payments']['completed'] = ! empty( $enabled_gateways );
}
if ( isset( $this->tasks['woocommerce-payments'] ) ) {
$wc_pay_is_connected = false;
if ( class_exists( '\WC_Payments' ) ) {
$wc_payments_gateway = \WC_Payments::get_gateway();
$wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' )
? $wc_payments_gateway->is_connected()
: false;
}
$this->tasks['woocommerce-payments']['completed'] = $wc_pay_is_connected;
}
}
}
endif;
return new WC_Admin_Dashboard_Setup();

View File

@ -24,7 +24,7 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
*/
public function __construct() {
// Only hook in admin parts if the user has admin access.
if ( current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' ) ) {
if ( $this->should_display_widget() ) {
// If on network admin, only load the widget that works in that context and skip the rest.
if ( is_multisite() && is_network_admin() ) {
add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) );
@ -57,6 +57,17 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) );
}
/**
* Check to see if we should display the widget.
*
* @return bool
*/
private function should_display_widget() {
$has_permission = current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' );
$task_completed_or_hidden = 'yes' === get_option( 'woocommerce_task_list_complete' ) || 'yes' === get_option( 'woocommerce_task_list_hidden' );
return $task_completed_or_hidden && $has_permission;
}
/**
* Get top seller from DB.
*

View File

@ -94,6 +94,7 @@ class WC_Admin {
switch ( $screen->id ) {
case 'dashboard':
case 'dashboard-network':
include __DIR__ . '/class-wc-admin-dashboard-setup.php';
include __DIR__ . '/class-wc-admin-dashboard.php';
break;
case 'options-permalink':

View File

@ -0,0 +1,29 @@
<?php
/**
* Admin View: Dashboard - Finish Setup
*
* @package WooCommerce\Admin
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="dashboard-widget-finish-setup">
<span class='progress-wrapper'>
<svg class="circle-progress" width="17" height="17" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle r="6.5" cx="10" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="0"></circle>
<circle class="bar" r="6.5" cx="190" cy="10" fill="transparent" stroke-dasharray="40.859" stroke-dashoffset="<?php echo esc_attr( $circle_dashoffset ); ?>" transform='rotate(-90 100 100)'></circle>
</svg>
<span><?php echo esc_html_e( 'Step', 'woocommerce' ); ?> <?php echo esc_html( $completed_tasks_count ); ?> <?php echo esc_html_e( 'of', 'woocommerce' ); ?> <?php echo esc_html( $tasks_count ); ?></span>
</span>
<div class="description">
<div>
<?php echo esc_html_e( 'You\'re almost there! Once you complete store setup you can start receiving orders.', 'woocommerce' ); ?>
<div><a href='<?php echo esc_attr( $button_link ); ?>' class='button button-primary'><?php echo esc_html_e( 'Start selling', 'woocommerce' ); ?></a></div>
</div>
<img src="<?php echo esc_url( WC()->plugin_url() ); ?>/assets/images/dashboard-widget-setup.png" />
</div>
<div class="clear"></div>
</div>

View File

@ -602,7 +602,12 @@ class WC_Countries {
array(
'{first_name}' => $args['first_name'],
'{last_name}' => $args['last_name'],
'{name}' => $args['first_name'] . ' ' . $args['last_name'],
'{name}' => sprintf(
/* translators: 1: first name 2: last name */
_x( '%1$s %2$s', 'full name', 'woocommerce' ),
$args['first_name'],
$args['last_name']
),
'{company}' => $args['company'],
'{address_1}' => $args['address_1'],
'{address_2}' => $args['address_2'],
@ -612,7 +617,14 @@ class WC_Countries {
'{country}' => $full_country,
'{first_name_upper}' => wc_strtoupper( $args['first_name'] ),
'{last_name_upper}' => wc_strtoupper( $args['last_name'] ),
'{name_upper}' => wc_strtoupper( $args['first_name'] . ' ' . $args['last_name'] ),
'{name_upper}' => wc_strtoupper(
sprintf(
/* translators: 1: first name 2: last name */
_x( '%1$s %2$s', 'full name', 'woocommerce' ),
$args['first_name'],
$args['last_name']
)
),
'{company_upper}' => wc_strtoupper( $args['company'] ),
'{address_1_upper}' => wc_strtoupper( $args['address_1'] ),
'{address_2_upper}' => wc_strtoupper( $args['address_2'] ),

View File

@ -907,7 +907,7 @@ class WC_Order extends WC_Abstract_Order {
$address = WC()->countries->get_formatted_address( $raw_address );
/**
* Filter orders formatterd billing address.
* Filter orders formatted billing address.
*
* @since 3.8.0
* @param string $address Formatted billing address string.
@ -933,7 +933,7 @@ class WC_Order extends WC_Abstract_Order {
}
/**
* Filter orders formatterd shipping address.
* Filter orders formatted shipping address.
*
* @since 3.8.0
* @param string $address Formatted billing address string.

View File

@ -162,7 +162,7 @@ class WC_Shipping_Rate {
}
/**
* Set ID for the rate. This is usually a combination of the method and instance IDs.
* Get ID for the rate. This is usually a combination of the method and instance IDs.
*
* @since 3.2.0
* @return string
@ -172,7 +172,7 @@ class WC_Shipping_Rate {
}
/**
* Set shipping method ID the rate belongs to.
* Get shipping method ID the rate belongs to.
*
* @since 3.2.0
* @return string
@ -182,7 +182,7 @@ class WC_Shipping_Rate {
}
/**
* Set instance ID the rate belongs to.
* Get instance ID the rate belongs to.
*
* @since 3.2.0
* @return int
@ -192,7 +192,7 @@ class WC_Shipping_Rate {
}
/**
* Set rate label.
* Get rate label.
*
* @return string
*/
@ -201,7 +201,7 @@ class WC_Shipping_Rate {
}
/**
* Set rate cost.
* Get rate cost.
*
* @since 3.2.0
* @return string
@ -211,7 +211,7 @@ class WC_Shipping_Rate {
}
/**
* Set rate taxes.
* Get rate taxes.
*
* @since 3.2.0
* @return array

View File

@ -2298,6 +2298,7 @@ if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) {
* Output the Coupon form for the checkout.
*/
function woocommerce_checkout_coupon_form() {
if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) {
wc_get_template(
'checkout/form-coupon.php',
array(
@ -2306,6 +2307,7 @@ if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) {
);
}
}
}
if ( ! function_exists( 'woocommerce_products_will_display' ) ) {

View File

@ -39,8 +39,8 @@ if ( $total <= 1 ) {
'add_args' => false,
'current' => max( 1, $current ),
'total' => $total,
'prev_text' => '&larr;',
'next_text' => '&rarr;',
'prev_text' => is_rtl() ? '&rarr;' : '&larr;',
'next_text' => is_rtl() ? '&larr;' : '&rarr;',
'type' => 'list',
'end_size' => 3,
'mid_size' => 3,

View File

@ -51,8 +51,8 @@ if ( ! comments_open() ) {
apply_filters(
'woocommerce_comment_pagination_args',
array(
'prev_text' => '&larr;',
'next_text' => '&rarr;',
'prev_text' => is_rtl() ? '&rarr;' : '&larr;',
'next_text' => is_rtl() ? '&larr;' : '&rarr;',
'type' => 'list',
)
)

View File

@ -1,6 +1,5 @@
{
"url": "http://localhost:8084/",
"appName": "woocommerce_e2e",
"users": {
"admin": {
"username": "admin",

View File

@ -20,9 +20,11 @@
- Merchant Product Search tests
- Shopper Single Product tests
- Shopper Checkout Apply Coupon
- Shopper Shop Browse Search Sort
- Merchant Orders Customer Checkout Page
- Shopper Cart Apply Coupon
- Shopper Variable product info updates on different variations
- Merchant order emails flow
## Fixed

View File

@ -58,6 +58,7 @@ The functions to access the core tests are:
- `runProductEditDetailsTest` - Merchant can edit an existing product
- `runProductSearchTest` - Merchant can search for a product and view it
- `runMerchantOrdersCustomerPaymentPage` - Merchant can visit the customer payment page
- `runMerchantOrderEmailsTest` - Merchant can receive order emails and resend emails by Order Actions
### Shopper
@ -68,6 +69,7 @@ The functions to access the core tests are:
- `runCheckoutPageTest` - Shopper can complete checkout
- `runMyAccountPageTest` - Shopper can access my account page
- `runSingleProductPageTest` - Shopper can view single product page in many variations (simple, variable, grouped)
- `runProductBrowseSearchSortTest` - Shopper can browse, search & sort products
- `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
## Contributing a new test

View File

@ -74,10 +74,10 @@ const runCouponApiTest = () => {
it('can delete a coupon', async () => {
// Delete the coupon
const deletedCoupon = await repository.delete( coupon.id );
const status = await repository.delete( coupon.id );
// If the delete is successful, the response comes back truthy
expect( deletedCoupon ).toBeTruthy();
expect( status ).toBeTruthy();
});
});
};

View File

@ -69,6 +69,11 @@ const runExternalProductAPITest = () => {
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( transformedProperties ) );
});
it('can delete an external product', async () => {
const status = repository.delete( product.id );
expect( status ).toBeTruthy();
});
});
};

View File

@ -71,11 +71,16 @@ const runGroupedProductAPITest = () => {
expect( response.data ).toEqual( expect.objectContaining( rawProperties ) );
});
it('can retrieve a transformed external product', async () => {
it('can retrieve a transformed grouped product', async () => {
// Read product via the repository.
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
it('can delete a grouped product', async () => {
const status = repository.delete( product.id );
expect( status ).toBeTruthy();
});
});
};

View File

@ -9,6 +9,7 @@ const { runOnboardingFlowTest, runTaskListTest } = require( './activate-and-setu
const runInitialStoreSettingsTest = require( './activate-and-setup/setup.test' );
// Shopper tests
const runProductBrowseSearchSortTest = require( './shopper/front-end-product-browse-search-sort.test' );
const runCartApplyCouponsTest = require( './shopper/front-end-cart-coupons.test');
const runCartPageTest = require( './shopper/front-end-cart.test' );
const runCheckoutApplyCouponsTest = require( './shopper/front-end-checkout-coupons.test');
@ -31,6 +32,7 @@ const runOrderApplyCouponTest = require( './merchant/wp-admin-order-apply-coupon
const runProductEditDetailsTest = require( './merchant/wp-admin-product-edit-details.test' );
const runProductSearchTest = require( './merchant/wp-admin-product-search.test' );
const runMerchantOrdersCustomerPaymentPage = require( './merchant/wp-admin-order-customer-payment-page.test' );
const runMerchantOrderEmailsTest = require( './merchant/wp-admin-order-emails.test' );
// REST API tests
const runExternalProductAPITest = require( './api/external-product.test' );
@ -46,6 +48,7 @@ const runSetupOnboardingTests = () => {
};
const runShopperTests = () => {
runProductBrowseSearchSortTest();
runCartApplyCouponsTest();
runCartPageTest();
runCheckoutApplyCouponsTest();
@ -110,6 +113,8 @@ module.exports = {
runProductEditDetailsTest,
runProductSearchTest,
runMerchantOrdersCustomerPaymentPage,
runMerchantOrderEmailsTest,
runMerchantTests,
runProductBrowseSearchSortTest,
runApiTests,
};

View File

@ -0,0 +1,73 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests, jest/no-standalone-expect */
/**
* Internal dependencies
*/
const {
merchant,
clickUpdateOrder,
createSimpleOrder,
selectOrderAction,
deleteAllEmailLogs,
} = require( '@woocommerce/e2e-utils' );
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
const customerEmail = config.get( 'addresses.customer.billing.email' );
const adminEmail = 'admin@woocommercecoree2etestsuite.com';
const storeName = 'WooCommerce Core E2E Test Suite';
let orderId;
const runMerchantOrderEmailsTest = () => {
describe('Merchant > Order Action emails received', () => {
beforeAll( async () => {
await merchant.login();
// Clear out the existing email logs if any
await deleteAllEmailLogs();
orderId = await createSimpleOrder( 'Processing' );
await Promise.all( [
// Select the billing email address field and add the customer billing email from the config
await page.click( 'div.order_data_column:nth-child(2) > h3:nth-child(1) > a:nth-child(1)' ),
await expect( page ).toFill( '#_billing_email', customerEmail ),
await clickUpdateOrder( 'Order updated.' ),
] );
} );
afterEach( async () => {
// Clear out any emails after each test
await deleteAllEmailLogs();
} );
// New order emails are sent automatically when we create the simple order above, so let's verify we get these emails
it('can receive new order email', async () => {
await merchant.openEmailLog();
await expect( page ).toMatchElement( '.column-receiver', { text: adminEmail } );
await expect( page ).toMatchElement( '.column-subject', { text: `[${storeName}]: New order #${orderId}` } );
} );
it('can resend new order notification', async () => {
await merchant.goToOrder( orderId );
await selectOrderAction( 'send_order_details_admin' );
await merchant.openEmailLog();
await expect( page ).toMatchElement( '.column-receiver', { text: adminEmail } );
await expect( page ).toMatchElement( '.column-subject', { text: `[${storeName}]: New order #${orderId}` } );
} );
it('can email invoice/order details to customer', async () => {
await merchant.goToOrder( orderId );
await selectOrderAction( 'send_order_details' );
await merchant.openEmailLog();
await expect( page ).toMatchElement( '.column-receiver', { text: customerEmail } );
await expect( page ).toMatchElement( '.column-subject', { text: `Invoice for order #${orderId} on ${storeName}` } );
} );
} );
}
module.exports = runMerchantOrderEmailsTest;

View File

@ -8,7 +8,8 @@ const {
createCoupon,
createSimpleProduct,
uiUnblocked,
clearAndFillInput,
applyCoupon,
removeCoupon,
} = require( '@woocommerce/e2e-utils' );
/**
@ -20,28 +21,6 @@ const {
beforeAll,
} = require( '@jest/globals' );
/**
* Apply a coupon code to the cart.
*
* @param couponCode string
* @returns {Promise<void>}
*/
const applyCouponToCart = async ( couponCode ) => {
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
};
/**
* Remove one coupon from the cart.
*
* @returns {Promise<void>}
*/
const removeCouponFromCart = async () => {
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
}
const runCartApplyCouponsTest = () => {
describe('Cart applying coupons', () => {
let couponFixedCart;
@ -62,42 +41,42 @@ const runCartApplyCouponsTest = () => {
});
it('allows customer to apply fixed cart coupon', async () => {
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
await page.waitForSelector('.order-total');
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
await removeCoupon(couponFixedCart);
});
it('allows customer to apply percentage coupon', async () => {
await applyCouponToCart( couponPercentage );
await applyCoupon(couponPercentage);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
await page.waitForSelector('.order-total');
await expect(page).toMatchElement('.cart-discount .amount', {text: '$4.99'});
await expect(page).toMatchElement('.order-total .amount', {text: '$5.00'});
await removeCouponFromCart();
await removeCoupon(couponPercentage);
});
it('allows customer to apply fixed product coupon', async () => {
await applyCouponToCart( couponFixedProduct );
await applyCoupon(couponFixedProduct);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
await page.waitForSelector('.order-total');
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
await removeCoupon(couponFixedProduct);
});
it('prevents customer applying same coupon twice', async () => {
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
// Verify only one discount applied
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
@ -105,7 +84,7 @@ const runCartApplyCouponsTest = () => {
});
it('allows customer to apply multiple coupons', async () => {
await applyCouponToCart( couponFixedProduct );
await applyCoupon(couponFixedProduct);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
@ -114,8 +93,8 @@ const runCartApplyCouponsTest = () => {
});
it('restores cart total when coupons are removed', async () => {
await removeCouponFromCart();
await removeCouponFromCart();
await removeCoupon(couponFixedCart);
await removeCoupon(couponFixedProduct);
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
});
});

View File

@ -8,7 +8,8 @@ const {
createCoupon,
createSimpleProduct,
uiUnblocked,
clearAndFillInput,
applyCoupon,
removeCoupon,
} = require( '@woocommerce/e2e-utils' );
/**
@ -20,30 +21,6 @@ const {
beforeAll,
} = require( '@jest/globals' );
/**
* Apply a coupon code to the cart.
*
* @param couponCode string
* @returns {Promise<void>}
*/
const applyCouponToCart = async ( couponCode ) => {
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
};
/**
* Remove one coupon from the cart.
*
* @returns {Promise<void>}
*/
const removeCouponFromCart = async () => {
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
}
const runCheckoutApplyCouponsTest = () => {
describe('Checkout coupons', () => {
let couponFixedCart;
@ -64,7 +41,7 @@ const runCheckoutApplyCouponsTest = () => {
});
it('allows customer to apply fixed cart coupon', async () => {
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Wait for page to expand total calculations to avoid flakyness
@ -73,31 +50,31 @@ const runCheckoutApplyCouponsTest = () => {
// Verify discount applied and order total
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
await removeCoupon(couponFixedCart);
});
it('allows customer to apply percentage coupon', async () => {
await applyCouponToCart( couponPercentage );
await applyCoupon(couponPercentage);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
await expect(page).toMatchElement('.cart-discount .amount', {text: '$4.99'});
await expect(page).toMatchElement('.order-total .amount', {text: '$5.00'});
await removeCouponFromCart();
await removeCoupon(couponPercentage);
});
it('allows customer to apply fixed product coupon', async () => {
await applyCouponToCart( couponFixedProduct );
await applyCoupon(couponFixedProduct);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
await removeCoupon(couponFixedProduct);
});
it('prevents customer applying same coupon twice', async () => {
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await applyCouponToCart( couponFixedCart );
await applyCoupon(couponFixedCart);
// Verify only one discount applied
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
@ -105,14 +82,14 @@ const runCheckoutApplyCouponsTest = () => {
});
it('allows customer to apply multiple coupons', async () => {
await applyCouponToCart( couponFixedProduct );
await applyCoupon(couponFixedProduct);
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await expect(page).toMatchElement('.order-total .amount', {text: '$0.00'});
});
it('restores cart total when coupons are removed', async () => {
await removeCouponFromCart();
await removeCouponFromCart();
await removeCoupon(couponFixedCart);
await removeCoupon(couponFixedProduct);
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
});
});

View File

@ -0,0 +1,87 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const {
shopper,
merchant,
createSimpleProductWithCategory,
uiUnblocked,
} = require( '@woocommerce/e2e-utils' );
/**
* External dependencies
*/
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
const singleProductPrice = config.has('products.simple.price') ? config.get('products.simple.price') : '9.99';
const singleProductPrice2 = config.has('products.simple.price') ? config.get('products.simple.price') : '19.99';
const singleProductPrice3 = config.has('products.simple.price') ? config.get('products.simple.price') : '29.99';
const clothing = 'Clothing';
const audio = 'Audio';
const hardware = 'Hardware';
const productTitle = 'li.first > a > h2.woocommerce-loop-product__title';
const runProductBrowseSearchSortTest = () => {
describe('Search, browse by categories and sort items in the shop', () => {
beforeAll(async () => {
await merchant.login();
// Create 1st product with Clothing category
await createSimpleProductWithCategory(simpleProductName + ' 1', singleProductPrice, clothing);
// Create 2nd product with Audio category
await createSimpleProductWithCategory(simpleProductName + ' 2', singleProductPrice2, audio);
// Create 3rd product with Hardware category
await createSimpleProductWithCategory(simpleProductName + ' 3', singleProductPrice3, hardware);
await merchant.logout();
});
it('should let user search the store', async () => {
await shopper.goToShop();
await shopper.searchForProduct(simpleProductName + ' 1');
});
it('should let user browse products by categories', async () => {
// Browse through Clothing category link
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle0'}),
page.click('span.posted_in > a', {text: clothing}),
]);
await uiUnblocked();
// Verify Clothing category page
await page.waitForSelector(productTitle);
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 1'});
await expect(page).toClick(productTitle, {text: simpleProductName + ' 1'});
await uiUnblocked();
await page.waitForSelector('h1.entry-title');
await expect(page).toMatchElement('h1.entry-title', simpleProductName + ' 1');
});
it('should let user sort the products in the shop', async () => {
await shopper.goToShop();
// Sort by price high to low
await page.select('.orderby', 'price-desc');
// Verify the first product in sort order
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 3'});
// Sort by price low to high
await page.select('.orderby', 'price');
// Verify the first product in sort order
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 1'});
// Sort by date of creation, latest to oldest
await page.select('.orderby', 'date');
// Verify the first product in sort order
await expect(page).toMatchElement(productTitle, {text: simpleProductName + ' 3'});
});
});
};
module.exports = runProductBrowseSearchSortTest;

View File

@ -8,3 +8,6 @@ wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=pas
# we cannot create API keys for the API, so we using basic auth, this plugin allows that.
wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate
# install the WP Mail Logging plugin to test emails
wp plugin install wp-mail-logging --activate

View File

@ -11,6 +11,7 @@
- support for custom container name
- Insert a 12 hour delay in using new docker image tags
- Package `bin` script `wc-e2e`
- WP Mail Log plugin as part of container initialization
## Fixed

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runProductBrowseSearchSortTest } = require( '@woocommerce/e2e-core-tests' );
runProductBrowseSearchSortTest();

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runMerchantOrderEmailsTest } = require( '@woocommerce/e2e-core-tests' );
runMerchantOrderEmailsTest();

View File

@ -17,6 +17,13 @@
- `createCoupon( couponAmount )` component which accepts a coupon amount string (it defaults to 5) and creates a basic coupon. Returns the generated coupon code.
- `evalAndClick( selector )` use Puppeteer page.$eval to select and click and element.
- `selectOptionInSelect2( selector, value )` util helper method that search and select in any select2 type field
- `createSimpleProductWithCategory` component which creates a simple product with categories, containing three parameters for title, price and category name.
- `applyCoupon( couponName )` util helper method which applies previously created coupon to cart or checkout
- `removeCoupon()` util helper method that removes a single coupon within cart or checkout
- `selectOrderAction( action )` util helper method to select and initiate an order action in the Order Action postbox
- `merchant.openEmailLog()` go to the WP Mail Log page
- `deleteAllEmailLogs` delete all email logs in the WP Mail Log plugin
- `clickUpdateOrder( noticeText, waitForSave )` util helper that clicks the `Update` button on an order
## Changes

View File

@ -54,6 +54,7 @@ describe( 'Cart page', () => {
| `openSettings` | | Go to WooCommerce -> Settings |
| `runSetupWizard` | | Open the onboarding profiler |
| `updateOrderStatus` | `orderId, status` | Update the status of an order |
| `openEmailLog` | | Open the WP Mail Log page |
### Shopper `shopper`
@ -77,6 +78,7 @@ describe( 'Cart page', () => {
| `productIsInCheckout` | `productTitle, quantity, total, cartSubtotal` | Verify product is in cart on checkout page |
| `removeFromCart` | `productTitle` | Remove a product from the cart on the cart page |
| `setCartQuantity` | `productTitle, quantityValue` | Change the quantity of a product on the cart page |
| `searchForProduct` | Searching for a product name and landing on its detail page |
### Page Utilities
@ -101,7 +103,11 @@ describe( 'Cart page', () => {
| `clickFilter` | `selector` | Click on a list page filter |
| `moveAllItemsToTrash` | | Moves all items in a list view to the Trash |
| `verifyAndPublish` | `noticeText` | Verify that an item can be published |
| `selectOptionInSelect2` | `selector, value` | helper method that searchs for select2 type fields and select plus insert value inside
| `selectOptionInSelect2` | `selector, value` | helper method that searchs for select2 type fields and select plus insert value inside |
| `applyCoupon` | `couponName` | helper method which applies a coupon in cart or checkout |
| `removeCoupon` | | helper method that removes a single coupon within cart or checkout |
| `selectOrderAction` | `action` | Helper method to select an order action in the `Order Actions` postbox |
| `clickUpdateOrder` | `noticeText`, `waitForSave` | Helper method to click the Update button on the order details page |
### Test Utilities

View File

@ -6,7 +6,7 @@
* Internal dependencies
*/
import { merchant } from './flows';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick, selectOptionInSelect2 } from './page-utils';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick, selectOptionInSelect2, setCheckbox } from './page-utils';
import factories from './factories';
const config = require( 'config' );
@ -184,6 +184,44 @@ const createSimpleProduct = async () => {
return product.id;
} ;
/**
* Create simple product with categories
*
* @param productName Product's name which can be changed when writing a test
* @param productPrice Product's price which can be changed when writing a test
* @param categoryName Product's category which can be changed when writing a test
*/
const createSimpleProductWithCategory = async ( productName, productPrice, categoryName ) => {
// Go to "add product" page
await merchant.openNewProduct();
// Add title and regular price
await expect(page).toFill('#title', productName);
await expect(page).toClick('#_virtual');
await clickTab('General');
await expect(page).toFill('#_regular_price', productPrice);
// Try to select the existing category if present already, otherwise add a new and select it
try {
const [checkbox] = await page.$x('//label[contains(text(), "'+categoryName+'")]');
await checkbox.click();
} catch (error) {
await expect(page).toClick('#product_cat-add-toggle');
await expect(page).toFill('#newproduct_cat', categoryName);
await expect(page).toClick('#product_cat-add-submit');
}
// Publish the product
await expect(page).toClick('#publish');
await uiUnblocked();
await page.waitForSelector('.updated.notice', {text:'Product published.'});
// Get the product ID
const variablePostId = await page.$('#post_ID');
let variablePostIdValue = (await(await variablePostId.getProperty('value')).jsonValue());
return variablePostIdValue;
};
/**
* Create variable product.
*/
@ -435,6 +473,42 @@ const createCoupon = async ( couponAmount = '5', discountType = 'Fixed cart disc
return couponCode;
};
/**
* Click the Update button on the order details page.
*
* @param noticeText The text that appears in the notice after updating the order.
* @param waitForSave Optionally wait for auto save.
*/
const clickUpdateOrder = async ( noticeText, waitForSave = false ) => {
if ( waitForSave ) {
await page.waitFor( 2000 );
}
// PUpdate order
await expect( page ).toClick( 'button.save_order' );
await page.waitForSelector( '.updated.notice' );
// Verify
await expect( page ).toMatchElement( '.updated.notice', { text: noticeText } );
};
/**
* Delete all email logs in the WP Mail Logging plugin page.
*/
const deleteAllEmailLogs = async () => {
await merchant.openEmailLog();
// Make sure we have emails to delete. If we don't, this selector will return null.
if ( await page.$( '#bulk-action-selector-top' ) !== null ) {
await setCheckbox( '#cb-select-all-1' );
await expect( page ).toSelect( '#bulk-action-selector-top', 'Delete' );
await Promise.all( [
page.click( '#doaction' ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
}
};
export {
completeOnboardingWizard,
createSimpleProduct,
@ -444,4 +518,7 @@ export {
verifyAndPublish,
addProductToOrder,
createCoupon,
createSimpleProductWithCategory,
clickUpdateOrder,
deleteAllEmailLogs,
};

View File

@ -169,6 +169,12 @@ const merchant = {
await expect( page ).toMatchElement( 'label[for="customer_user"] a[href*=user-edit]', { text: 'Profile' } );
}
},
openEmailLog: async () => {
await page.goto( `${baseUrl}wp-admin/tools.php?page=wpml_plugin_log`, {
waitUntil: 'networkidle0',
} );
}
};
module.exports = merchant;

View File

@ -137,6 +137,17 @@ const shopper = {
await quantityInput.type( quantityValue.toString() );
},
searchForProduct: async ( prouductName ) => {
await expect(page).toFill('.search-field', prouductName);
await expect(page).toClick('.search-submit');
await page.waitForSelector('h2.entry-title');
await expect(page).toMatchElement('h2.entry-title', {text: prouductName});
await expect(page).toClick('h2.entry-title', {text: prouductName});
await page.waitForSelector('h1.entry-title');
await expect(page.title()).resolves.toMatch(prouductName);
await expect(page).toMatchElement('h1.entry-title', prouductName);
},
/*
* My Accounts flows.
*/

View File

@ -209,6 +209,53 @@ const selectOptionInSelect2 = async ( value, selector = 'input.select2-search__f
await page.keyboard.press('Enter');
};
/**
* Apply a coupon code within cart or checkout.
* Method will try to apply a coupon in the checkout, otherwise will try to apply in the cart.
*
* @param couponCode string
* @returns {Promise<void>}
*/
const applyCoupon = async ( couponCode ) => {
try {
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
} catch (error) {
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
};
};
/**
* Remove one coupon within cart or checkout.
*
* @param couponCode Coupon name.
* @returns {Promise<void>}
*/
const removeCoupon = async ( couponCode ) => {
await expect(page).toClick('[data-coupon="'+couponCode.toLowerCase()+'"]', {text: '[Remove]'});
await uiUnblocked();
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
};
/**
*
* Select and perform an order action in the `Order actions` postbox.
*
* @param {string} action The action to take on the order.
*/
const selectOrderAction = async ( action ) => {
await page.select( 'select[name=wc_order_action]', action );
await Promise.all( [
page.click( '.wc-reload' ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
}
export {
clearAndFillInput,
clickTab,
@ -225,4 +272,7 @@ export {
moveAllItemsToTrash,
evalAndClick,
selectOptionInSelect2,
applyCoupon,
removeCoupon,
selectOrderAction,
};

View File

@ -141,7 +141,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
}
/**
* Test usage limit for guest users uasge limit per user is set.
* Test usage limit for guest users usage limit per user is set.
*
* @throws Exception When unable to create order.
*/

View File

@ -18,7 +18,7 @@ class Dummy_Widget extends WC_Widget {
* Output widget.
*
* @param mixed $args Arguments.
* @param WP_Widget $instance Intance of WP_Widget.
* @param WP_Widget $instance Instance of WP_Widget.
* @return void
*/
public function widget( $args, $instance ) {

View File

@ -0,0 +1,136 @@
<?php
/**
* Tests for the WC_Admin_Dashboard_Setup class.
*
* @package WooCommerce\Tests\Admin
*/
/**
* Class WC_Admin_Dashboard_Setup_Test
*/
class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
/**
* Set up
*/
public function setUp() {
// set default country to US so that 'payments' task does not get added.
// we want to remove payment tasks as they depend on installation & activation.
update_option( 'woocommerce_default_country', 'US' );
parent::setUp();
}
/**
* Includes widget class and return the class.
*
* @return WC_Admin_Dashboard_Setup
*/
public function get_widget() {
return include __DIR__ . '/../../../../includes/admin/class-wc-admin-dashboard-setup.php';
}
/**
* Return widget output (HTML).
*
* @return string Render widget HTML
*/
public function get_widget_output() {
update_option( 'woocommerce_task_list_hidden', 'no' );
ob_start();
$this->get_widget()->render();
return ob_get_clean();
}
/**
* Tests widget does not get rendered when woocommerce_task_list_hidden or woocommerce_task_list_hidden
* is true.
*
* @dataProvider should_display_widget_data_provider
*
* @param array $options a set of options.
*/
public function test_widget_does_not_get_rendered( array $options ) {
global $wp_meta_boxes;
foreach ( $options as $name => $value ) {
update_option( $name, $value );
}
$this->get_widget();
$this->assertNull( $wp_meta_boxes );
}
/**
* Given both woocommerce_task_list_hidden and woocommerce_task_list_complete are false
* Then the widget should be added to the $wp_meta_boxes
*/
public function test_widget_gets_rendered_when_both_options_are_false() {
global $wp_meta_boxes;
update_option( 'woocommerce_task_list_complete', false );
update_option( 'woocommerce_task_list_hidden', false );
$this->get_widget();
$this->assertArrayHasKey( 'wc_admin_dashboard_setup', $wp_meta_boxes['dashboard']['normal']['high'] );
}
/**
* Tests the widget output when 0 task has been completed.
*/
public function test_initial_widget_output() {
$html = $this->get_widget_output();
$required_strings = array(
'Step 0 of 5',
'You&#039;re almost there! Once you complete store setup you can start receiving orders.',
'Start selling',
'admin.php\?page=wc-admin&amp;path=%2Fsetup-wizard',
);
foreach ( $required_strings as $required_string ) {
$this->assertRegexp( "/${required_string}/", $html );
}
}
/**
* Tests completed task count as it completes one by one
*/
public function test_widget_renders_completed_task_count() {
$completed_tasks = array();
$tasks = $this->get_widget()->get_tasks();
$tasks_count = count( $tasks );
foreach ( $tasks as $key => $task ) {
array_push( $completed_tasks, $key );
update_option( 'woocommerce_task_list_tracked_completed_tasks', $completed_tasks );
$completed_tasks_count = count( $completed_tasks );
// When all tasks are completed, assert that the widget output is empty.
// As widget won't be rendered when tasks are completed.
if ( $completed_tasks_count === $tasks_count ) {
$this->assertEmpty( $this->get_widget_output() );
} else {
$this->assertRegexp( "/Step ${completed_tasks_count} of 5/", $this->get_widget_output() );
}
}
}
/**
* Provides dataset that controls output of `should_display_widget`
*/
public function should_display_widget_data_provider() {
return array(
array(
array(
'woocommerce_task_list_complete' => 'yes',
'woocommerce_task_list_hidden' => 'no',
),
),
array(
array(
'woocommerce_task_list_complete' => 'no',
'woocommerce_task_list_hidden' => 'yes',
),
),
);
}
}

View File

@ -85,7 +85,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which reduces stock to another status which also reduces stock.
* Test inventory count after order status transitions which reduces stock to another status which also reduces stock.
* Stock should have reduced once already, and should not reduce again.
*/
public function test_status_transition_stock_reduce_to_stock_reduce() {
@ -97,7 +97,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which reduces stock to another status which restores stock.
* Test inventory count after order status transitions which reduces stock to another status which restores stock.
* Should should have already reduced once, and will increase again after transitioning.
*/
public function test_status_transition_stock_reduce_to_stock_restore() {
@ -109,7 +109,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which reduces stock to another status which don't affect inventory.
* Test inventory count after order status transitions which reduces stock to another status which don't affect inventory.
* Stock should have already reduced, and will not change on transitioning.
*/
public function test_status_transition_stock_reduce_to_stock_no_effect() {
@ -133,7 +133,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which restores stock to another status which also restores stock.
* Test inventory count after order status transitions which restores stock to another status which also restores stock.
* Stock should not have reduced, and will remain the same even after transition (i.e. should not be restocked again).
*/
public function test_status_transition_stock_restore_to_stock_restore() {
@ -145,7 +145,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which restores stock to another status which don't affect inventory.
* Test inventory count after order status transitions which restores stock to another status which don't affect inventory.
* Stock should not have reduced, and will remain the same even after transition.
*/
public function test_status_transition_stock_restore_to_stock_no_effect() {
@ -157,7 +157,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which don't affect inventory stock to another status which reduces stock.
* Test inventory count after order status transitions which don't affect inventory stock to another status which reduces stock.
* Stock would not have been affected, but will reduce after transition.
*/
public function test_status_transition_stock_no_effect_to_stock_reduce() {
@ -169,7 +169,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which don't affect inventory stock to another status which restores stock.
* Test inventory count after order status transitions which don't affect inventory stock to another status which restores stock.
* Stock would not have been affected, and will not be restored after transition (since it was not reduced to begin with).
*/
public function test_status_transition_stock_no_effect_to_stock_restore() {
@ -181,7 +181,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
/**
* Test inventory count after order status transtions which don't affect inventory stock to another status which also don't affect inventory.
* Test inventory count after order status transitions which don't affect inventory stock to another status which also don't affect inventory.
* Stock levels will not change before or after the transition.
*/
public function test_status_transition_stock_no_effect_to_stock_no_effect() {