diff --git a/assets/css/dashboard-setup.scss b/assets/css/dashboard-setup.scss
new file mode 100644
index 00000000000..cee2344428e
--- /dev/null
+++ b/assets/css/dashboard-setup.scss
@@ -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;
+ }
+ }
+}
diff --git a/assets/images/dashboard-widget-setup.png b/assets/images/dashboard-widget-setup.png
new file mode 100644
index 00000000000..fcba8f5532a
Binary files /dev/null and b/assets/images/dashboard-widget-setup.png differ
diff --git a/includes/abstracts/abstract-wc-payment-token.php b/includes/abstracts/abstract-wc-payment-token.php
index ad958991981..6fc6f634d29 100644
--- a/includes/abstracts/abstract-wc-payment-token.php
+++ b/includes/abstracts/abstract-wc-payment-token.php
@@ -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
diff --git a/includes/admin/class-wc-admin-dashboard-setup.php b/includes/admin/class-wc-admin-dashboard-setup.php
new file mode 100644
index 00000000000..8fdbc1e94de
--- /dev/null
+++ b/includes/admin/class-wc-admin-dashboard-setup.php
@@ -0,0 +1,211 @@
+ 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();
diff --git a/includes/admin/class-wc-admin-dashboard.php b/includes/admin/class-wc-admin-dashboard.php
index 2bc03a07edb..498eeee78ae 100644
--- a/includes/admin/class-wc-admin-dashboard.php
+++ b/includes/admin/class-wc-admin-dashboard.php
@@ -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.
*
diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php
index e136974d539..0c2acf037f0 100644
--- a/includes/admin/class-wc-admin.php
+++ b/includes/admin/class-wc-admin.php
@@ -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':
diff --git a/includes/admin/views/html-admin-dashboard-setup.php b/includes/admin/views/html-admin-dashboard-setup.php
new file mode 100644
index 00000000000..ddc7b6b85f9
--- /dev/null
+++ b/includes/admin/views/html-admin-dashboard-setup.php
@@ -0,0 +1,29 @@
+
+
diff --git a/includes/class-wc-countries.php b/includes/class-wc-countries.php
index ae15bb442ab..064f035cabc 100644
--- a/includes/class-wc-countries.php
+++ b/includes/class-wc-countries.php
@@ -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'] ),
diff --git a/includes/class-wc-order.php b/includes/class-wc-order.php
index f33c5db1b36..516f905b10b 100644
--- a/includes/class-wc-order.php
+++ b/includes/class-wc-order.php
@@ -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.
diff --git a/includes/class-wc-shipping-rate.php b/includes/class-wc-shipping-rate.php
index 1248a70a423..72bb0686d56 100644
--- a/includes/class-wc-shipping-rate.php
+++ b/includes/class-wc-shipping-rate.php
@@ -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
diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php
index e6bcf282656..43597440f1c 100644
--- a/includes/wc-template-functions.php
+++ b/includes/wc-template-functions.php
@@ -2298,12 +2298,14 @@ if ( ! function_exists( 'woocommerce_checkout_coupon_form' ) ) {
* Output the Coupon form for the checkout.
*/
function woocommerce_checkout_coupon_form() {
- wc_get_template(
- 'checkout/form-coupon.php',
- array(
- 'checkout' => WC()->checkout(),
- )
- );
+ if ( is_user_logged_in() || WC()->checkout()->is_registration_enabled() || ! WC()->checkout()->is_registration_required() ) {
+ wc_get_template(
+ 'checkout/form-coupon.php',
+ array(
+ 'checkout' => WC()->checkout(),
+ )
+ );
+ }
}
}
diff --git a/templates/loop/pagination.php b/templates/loop/pagination.php
index 9d61f8799cf..b524d19d14e 100644
--- a/templates/loop/pagination.php
+++ b/templates/loop/pagination.php
@@ -39,8 +39,8 @@ if ( $total <= 1 ) {
'add_args' => false,
'current' => max( 1, $current ),
'total' => $total,
- 'prev_text' => '←',
- 'next_text' => '→',
+ 'prev_text' => is_rtl() ? '→' : '←',
+ 'next_text' => is_rtl() ? '←' : '→',
'type' => 'list',
'end_size' => 3,
'mid_size' => 3,
diff --git a/templates/single-product-reviews.php b/templates/single-product-reviews.php
index 995a11ea348..4d6f12e5938 100644
--- a/templates/single-product-reviews.php
+++ b/templates/single-product-reviews.php
@@ -51,8 +51,8 @@ if ( ! comments_open() ) {
apply_filters(
'woocommerce_comment_pagination_args',
array(
- 'prev_text' => '←',
- 'next_text' => '→',
+ 'prev_text' => is_rtl() ? '→' : '←',
+ 'next_text' => is_rtl() ? '←' : '→',
'type' => 'list',
)
)
diff --git a/tests/e2e/config/default.json b/tests/e2e/config/default.json
index d20c7e8673e..4d6b397aac2 100644
--- a/tests/e2e/config/default.json
+++ b/tests/e2e/config/default.json
@@ -1,6 +1,5 @@
{
"url": "http://localhost:8084/",
- "appName": "woocommerce_e2e",
"users": {
"admin": {
"username": "admin",
diff --git a/tests/e2e/core-tests/CHANGELOG.md b/tests/e2e/core-tests/CHANGELOG.md
index 21eb3b7873e..1204b97fcdb 100644
--- a/tests/e2e/core-tests/CHANGELOG.md
+++ b/tests/e2e/core-tests/CHANGELOG.md
@@ -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
diff --git a/tests/e2e/core-tests/README.md b/tests/e2e/core-tests/README.md
index cb16278e222..c9c5ec1788c 100644
--- a/tests/e2e/core-tests/README.md
+++ b/tests/e2e/core-tests/README.md
@@ -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,7 +69,8 @@ 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)
- - `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
+ - `runProductBrowseSearchSortTest` - Shopper can browse, search & sort products
+ - `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
## Contributing a new test
diff --git a/tests/e2e/core-tests/specs/api/coupon.test.js b/tests/e2e/core-tests/specs/api/coupon.test.js
index ec5200afa2b..6ba22bb4d3e 100644
--- a/tests/e2e/core-tests/specs/api/coupon.test.js
+++ b/tests/e2e/core-tests/specs/api/coupon.test.js
@@ -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();
});
});
};
diff --git a/tests/e2e/core-tests/specs/api/external-product.test.js b/tests/e2e/core-tests/specs/api/external-product.test.js
index 674cbdbae25..463f248b726 100644
--- a/tests/e2e/core-tests/specs/api/external-product.test.js
+++ b/tests/e2e/core-tests/specs/api/external-product.test.js
@@ -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();
+ });
});
};
diff --git a/tests/e2e/core-tests/specs/api/grouped-product.test.js b/tests/e2e/core-tests/specs/api/grouped-product.test.js
index 11dea1ba7a8..d1f12567305 100644
--- a/tests/e2e/core-tests/specs/api/grouped-product.test.js
+++ b/tests/e2e/core-tests/specs/api/grouped-product.test.js
@@ -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();
+ });
});
};
diff --git a/tests/e2e/core-tests/specs/index.js b/tests/e2e/core-tests/specs/index.js
index c35be68e428..2a59fed2696 100644
--- a/tests/e2e/core-tests/specs/index.js
+++ b/tests/e2e/core-tests/specs/index.js
@@ -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,
};
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js
new file mode 100644
index 00000000000..ad18a8de6d1
--- /dev/null
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-emails.test.js
@@ -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;
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js b/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js
index 4b3188a7e13..b2b3f4888a3 100644
--- a/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js
+++ b/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js
@@ -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}
- */
-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}
- */
-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'});
});
});
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js b/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js
index abe006ef6d0..e017c6d1739 100644
--- a/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js
+++ b/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js
@@ -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}
- */
-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}
- */
-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'});
});
});
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js b/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js
new file mode 100644
index 00000000000..c852cf1996e
--- /dev/null
+++ b/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js
@@ -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;
diff --git a/tests/e2e/docker/initialize.sh b/tests/e2e/docker/initialize.sh
index c5cbea47181..0a9592f5379 100755
--- a/tests/e2e/docker/initialize.sh
+++ b/tests/e2e/docker/initialize.sh
@@ -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
diff --git a/tests/e2e/env/CHANGELOG.md b/tests/e2e/env/CHANGELOG.md
index 94be2bfd1f8..8e6272ba44b 100644
--- a/tests/e2e/env/CHANGELOG.md
+++ b/tests/e2e/env/CHANGELOG.md
@@ -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
diff --git a/tests/e2e/specs/front-end/test-product-browse-search-sort.js b/tests/e2e/specs/front-end/test-product-browse-search-sort.js
new file mode 100644
index 00000000000..ed12d1cb341
--- /dev/null
+++ b/tests/e2e/specs/front-end/test-product-browse-search-sort.js
@@ -0,0 +1,6 @@
+/*
+ * Internal dependencies
+ */
+const { runProductBrowseSearchSortTest } = require( '@woocommerce/e2e-core-tests' );
+
+runProductBrowseSearchSortTest();
diff --git a/tests/e2e/specs/wp-admin/order-emails.test.js b/tests/e2e/specs/wp-admin/order-emails.test.js
new file mode 100644
index 00000000000..e0597074bda
--- /dev/null
+++ b/tests/e2e/specs/wp-admin/order-emails.test.js
@@ -0,0 +1,6 @@
+/*
+ * Internal dependencies
+ */
+const { runMerchantOrderEmailsTest } = require( '@woocommerce/e2e-core-tests' );
+
+runMerchantOrderEmailsTest();
diff --git a/tests/e2e/utils/CHANGELOG.md b/tests/e2e/utils/CHANGELOG.md
index 3bb027f92f7..519bc634d5e 100644
--- a/tests/e2e/utils/CHANGELOG.md
+++ b/tests/e2e/utils/CHANGELOG.md
@@ -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
diff --git a/tests/e2e/utils/README.md b/tests/e2e/utils/README.md
index 12f814aa992..ca185bae73b 100644
--- a/tests/e2e/utils/README.md
+++ b/tests/e2e/utils/README.md
@@ -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
diff --git a/tests/e2e/utils/src/components.js b/tests/e2e/utils/src/components.js
index 04a7b4a3e02..1776478cecf 100644
--- a/tests/e2e/utils/src/components.js
+++ b/tests/e2e/utils/src/components.js
@@ -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,
};
diff --git a/tests/e2e/utils/src/flows/merchant.js b/tests/e2e/utils/src/flows/merchant.js
index df4fae99d68..242a42c171d 100644
--- a/tests/e2e/utils/src/flows/merchant.js
+++ b/tests/e2e/utils/src/flows/merchant.js
@@ -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;
diff --git a/tests/e2e/utils/src/flows/shopper.js b/tests/e2e/utils/src/flows/shopper.js
index 516a69e4df6..8e4acad61f2 100644
--- a/tests/e2e/utils/src/flows/shopper.js
+++ b/tests/e2e/utils/src/flows/shopper.js
@@ -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.
*/
diff --git a/tests/e2e/utils/src/page-utils.js b/tests/e2e/utils/src/page-utils.js
index 7679c15f2b7..400a2c8fd29 100644
--- a/tests/e2e/utils/src/page-utils.js
+++ b/tests/e2e/utils/src/page-utils.js
@@ -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}
+ */
+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}
+ */
+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,
};
diff --git a/tests/legacy/unit-tests/checkout/checkout.php b/tests/legacy/unit-tests/checkout/checkout.php
index 73586cab609..953351a9350 100644
--- a/tests/legacy/unit-tests/checkout/checkout.php
+++ b/tests/legacy/unit-tests/checkout/checkout.php
@@ -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.
*/
diff --git a/tests/legacy/unit-tests/widgets/class-dummy-widget.php b/tests/legacy/unit-tests/widgets/class-dummy-widget.php
index f8aa56223d6..67ea55bfccb 100644
--- a/tests/legacy/unit-tests/widgets/class-dummy-widget.php
+++ b/tests/legacy/unit-tests/widgets/class-dummy-widget.php
@@ -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 ) {
diff --git a/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
new file mode 100644
index 00000000000..82ebf87cc05
--- /dev/null
+++ b/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
@@ -0,0 +1,136 @@
+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're almost there! Once you complete store setup you can start receiving orders.',
+ 'Start selling',
+ 'admin.php\?page=wc-admin&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',
+ ),
+ ),
+ );
+ }
+}
diff --git a/tests/php/includes/wc-stock-functions-tests.php b/tests/php/includes/wc-stock-functions-tests.php
index 56d2d0eeb13..d815eeee5d8 100644
--- a/tests/php/includes/wc-stock-functions-tests.php
+++ b/tests/php/includes/wc-stock-functions-tests.php
@@ -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() {