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() {