@@ -174,6 +178,31 @@ public function add_order_attribution_inputs() {
echo '
';
}
+
+ /**
+ * Add HTML containers to be used by the Express Checkout buttons that check if the payment method is available.
+ *
+ * @return void
+ */
+ private function add_html_container_for_test_express_checkout_buttons() {
+ add_filter(
+ 'the_content',
+ function ( $content ) {
+ $supported_payment_methods = [ 'applePay' , 'googlePay' ];
+ // Restrict adding these HTML containers to only the necessary pages.
+ if ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) {
+ foreach ( $supported_payment_methods as $value ) {
+ // The inline styles ensure that the HTML elements don't occupy space on the page.
+ $content = '
' . $content;
+ }
+ }
+ return $content;
+ },
+ 10,
+ 1
+ );
+ }
+
/**
* Check if the pay-for-order flow is supported.
*
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
index 1b6f68f6275..682b4eac903 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
@@ -288,7 +288,7 @@ public function display_express_checkout_button_html() {
return;
}
?>
-
+
cart->needs_shipping() ) {
$shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0;
$items[] = [
+ 'key' => 'total_shipping',
'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ),
'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ),
];
diff --git a/includes/multi-currency/CurrencySwitcherBlock.php b/includes/multi-currency/CurrencySwitcherBlock.php
index 840530e32a2..95d4762365c 100644
--- a/includes/multi-currency/CurrencySwitcherBlock.php
+++ b/includes/multi-currency/CurrencySwitcherBlock.php
@@ -65,7 +65,7 @@ public function init_block_widget() {
register_block_type(
'woocommerce-payments/multi-currency-switcher',
[
- 'api_version' => 2,
+ 'api_version' => '2',
'editor_script' => 'woocommerce-payments/multi-currency-switcher',
'render_callback' => [ $this, 'render_block_widget' ],
'attributes' => [
diff --git a/includes/multi-currency/wc-payments-multi-currency.php b/includes/multi-currency/wc-payments-multi-currency.php
index 31017f9ff35..fef6e10db81 100644
--- a/includes/multi-currency/wc-payments-multi-currency.php
+++ b/includes/multi-currency/wc-payments-multi-currency.php
@@ -15,8 +15,10 @@ function wcpay_multi_currency_onboarding_check() {
// Skip checking the HTTP referer if it is a cron job.
if ( ! defined( 'DOING_CRON' ) ) {
- $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
- $is_setup_page = 0 < strpos( $http_referer, 'multi-currency-setup' );
+ $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
+ if ( ! empty( $http_referer ) ) {
+ $is_setup_page = strpos( $http_referer, 'multi-currency-setup' ) !== false;
+ }
}
return $is_setup_page;
diff --git a/includes/payment-methods/class-becs-payment-method.php b/includes/payment-methods/class-becs-payment-method.php
index 88cfd9d8199..1d2c20b5b70 100644
--- a/includes/payment-methods/class-becs-payment-method.php
+++ b/includes/payment-methods/class-becs-payment-method.php
@@ -39,6 +39,6 @@ public function __construct( $token_service ) {
* @return string
*/
public function get_testing_instructions() {
- return __( '
Test mode: use the test account number 000123456. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed
here.', 'woocommerce-payments' );
+ return __( '
Test mode: use the test account number
000123456. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed
here.', 'woocommerce-payments' );
}
}
diff --git a/includes/payment-methods/class-cc-payment-method.php b/includes/payment-methods/class-cc-payment-method.php
index dc85cf662af..e0a3677d53e 100644
--- a/includes/payment-methods/class-cc-payment-method.php
+++ b/includes/payment-methods/class-cc-payment-method.php
@@ -70,6 +70,6 @@ public function get_title( string $account_country = null, $payment_details = fa
* @return string
*/
public function get_testing_instructions() {
- return __( '
Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed
here.', 'woocommerce-payments' );
+ return __( '
Test mode: use test card
4242 4242 4242 4242 or refer to our
testing guide.', 'woocommerce-payments' );
}
}
diff --git a/includes/payment-methods/class-sepa-payment-method.php b/includes/payment-methods/class-sepa-payment-method.php
index b11801ee738..8ea671f7706 100644
--- a/includes/payment-methods/class-sepa-payment-method.php
+++ b/includes/payment-methods/class-sepa-payment-method.php
@@ -43,6 +43,6 @@ public function __construct( $token_service ) {
* @return string
*/
public function get_testing_instructions() {
- return __( '
Test mode: use the test account number AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed
here.', 'woocommerce-payments' );
+ return __( '
Test mode: use the test account number
AT611904300234573201. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed
here.', 'woocommerce-payments' );
}
}
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index d360b76f07f..3ec8bc40e4b 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -970,6 +970,64 @@ public function get_onboarding_data( bool $live_account, string $return_url, arr
return $this->request( $request_args, self::ONBOARDING_API . '/init', self::POST, true, true );
}
+ /**
+ * Initialize the onboarding embedded KYC flow, returning a session object which is used by the frontend.
+ *
+ * @param bool $live_account Whether to create live account.
+ * @param array $site_data Site data.
+ * @param array $user_data User data.
+ * @param array $account_data Account data to be prefilled.
+ * @param array $actioned_notes Actioned notes to be sent.
+ * @param bool $progressive Whether progressive onboarding should be enabled for this onboarding.
+ * @param bool $collect_payout_requirements Whether we need to collect payout requirements.
+ *
+ * @return array
+ *
+ * @throws API_Exception
+ */
+ public function initialize_onboarding_embedded_kyc( bool $live_account, array $site_data = [], array $user_data = [], array $account_data = [], array $actioned_notes = [], bool $progressive = false, bool $collect_payout_requirements = false ): array {
+ $request_args = apply_filters(
+ 'wc_payments_get_onboarding_data_args',
+ [
+ 'site_data' => $site_data,
+ 'user_data' => $user_data,
+ 'account_data' => $account_data,
+ 'actioned_notes' => $actioned_notes,
+ 'create_live_account' => $live_account,
+ 'progressive' => $progressive,
+ 'collect_payout_requirements' => $collect_payout_requirements,
+ ]
+ );
+
+ $session = $this->request( $request_args, self::ONBOARDING_API . '/embedded', self::POST, true, true );
+
+ if ( ! is_array( $session ) ) {
+ return [];
+ }
+
+ return $session;
+ }
+
+ /**
+ * Finalize the onboarding embedded KYC flow.
+ *
+ * @param string $locale The locale to use to i18n the data.
+ * @param string $source The source of the onboarding flow.
+ * @param array $actioned_notes The actioned notes on the account related to this onboarding.
+ * @return array
+ *
+ * @throws API_Exception
+ */
+ public function finalize_onboarding_embedded_kyc( string $locale, string $source, array $actioned_notes ): array {
+ $request_args = [
+ 'locale' => $locale,
+ 'source' => $source,
+ 'actioned_notes' => $actioned_notes,
+ ];
+
+ return $this->request( $request_args, self::ONBOARDING_API . '/embedded/finalize', self::POST, true, true );
+ }
+
/**
* Get the fields data to be used by the onboarding flow.
*
diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php
index 8488354ad4f..cde7ffc0e5f 100644
--- a/includes/woopay/class-woopay-session.php
+++ b/includes/woopay/class-woopay-session.php
@@ -385,7 +385,7 @@ private static function get_checkout_data( $woopay_request ) {
* @param \WP_User $user The user object.
* @return string The user email.
*/
- private static function get_user_email( $user ) {
+ public static function get_user_email( $user ) {
if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
@@ -403,6 +403,21 @@ private static function get_user_email( $user ) {
}
}
+ // Get the email from the customer object if it's available.
+ if ( ! empty( WC()->customer ) ) {
+ $billing_email = WC()->customer->get_billing_email();
+
+ if ( ! empty( $billing_email ) ) {
+ return $billing_email;
+ }
+
+ $customer_email = WC()->customer->get_email();
+
+ if ( ! empty( $customer_email ) ) {
+ return $customer_email;
+ }
+ }
+
// As a last resort, we try to get the email from the customer logged in the store.
if ( $user->exists() ) {
return $user->user_email;
@@ -480,6 +495,7 @@ public static function get_init_session_request( $order_id = null, $key = null,
'blog_url' => get_site_url(),
'blog_checkout_url' => ! $is_pay_for_order ? wc_get_checkout_url() : $order->get_checkout_payment_url(),
'blog_shop_url' => get_permalink( wc_get_page_id( 'shop' ) ),
+ 'blog_timezone' => wp_timezone_string(),
'store_api_url' => self::get_store_api_url(),
'account_id' => $account_id,
'test_mode' => WC_Payments::mode()->is_test(),
diff --git a/includes/woopay/class-woopay-store-api-session-handler.php b/includes/woopay/class-woopay-store-api-session-handler.php
index 444a20adf07..30fd10219a2 100644
--- a/includes/woopay/class-woopay-store-api-session-handler.php
+++ b/includes/woopay/class-woopay-store-api-session-handler.php
@@ -113,6 +113,15 @@ public function get_session( $customer_id, $default = false ) {
return maybe_unserialize( $value );
}
+ /**
+ * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call.
+ *
+ * @return string
+ */
+ private function get_cache_prefix() {
+ return \WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP );
+ }
+
/**
* Save data and delete user session.
*/
@@ -129,7 +138,7 @@ public function save_data() {
$this->session_expiration
)
);
-
+ wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->session_expiration - time() );
$this->_dirty = false;
}
}
diff --git a/package-lock.json b/package-lock.json
index 096544d3a4a..23ef7829ee6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,19 @@
{
"name": "woocommerce-payments",
- "version": "8.1.1",
+ "version": "8.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "8.1.1",
+ "version": "8.2.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
+ "@stripe/connect-js": "3.3.12",
+ "@stripe/react-connect-js": "3.3.13",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
@@ -9155,6 +9157,21 @@
}
}
},
+ "node_modules/@stripe/connect-js": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/@stripe/connect-js/-/connect-js-3.3.12.tgz",
+ "integrity": "sha512-hXbgvGq9Lb6BYgsb8lcbjL76Yqsxr0yAj6T9ZFTfUK0O4otI5GSEWum9do9rf/E5OfYy6fR1FG/77Jve2w1o6Q=="
+ },
+ "node_modules/@stripe/react-connect-js": {
+ "version": "3.3.13",
+ "resolved": "https://registry.npmjs.org/@stripe/react-connect-js/-/react-connect-js-3.3.13.tgz",
+ "integrity": "sha512-kMxYjeQUcl/ixu/mSeX5QGIr/MuP+YxFSEBdb8j6w+tbK82tmcjyFDgoQTQwVXNqUV6jI66Kks3XcfpPRfeiJA==",
+ "peerDependencies": {
+ "@stripe/connect-js": ">=3.3.11",
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@stripe/react-stripe-js": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.5.1.tgz",
diff --git a/package.json b/package.json
index e77b6facd24..00dea7ef57a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "8.1.1",
+ "version": "8.2.0",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
@@ -77,6 +77,8 @@
"dependencies": {
"@automattic/interpolate-components": "1.2.1",
"@fingerprintjs/fingerprintjs": "3.4.1",
+ "@stripe/connect-js": "3.3.12",
+ "@stripe/react-connect-js": "3.3.13",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/stripe-js": "1.15.1",
"@woocommerce/explat": "2.3.0",
diff --git a/readme.txt b/readme.txt
index 389552701e1..f89a0a662c1 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment
Requires at least: 6.0
Tested up to: 6.6
Requires PHP: 7.3
-Stable tag: 8.1.1
+Stable tag: 8.2.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -94,6 +94,37 @@ Please note that our support for the checkout block is still experimental and th
== Changelog ==
+= 8.2.0 - 2024-09-11 =
+* Add - add: test instructions icon animation
+* Add - Added Embdedded KYC, currently behind feature flag.
+* Fix - Avoid unnecessary account data cache refresh on WooPayments pages refresh.
+* Fix - Check payment method is available before rendering it.
+* Fix - Disables custom checkout field detection due to compatibility issues and false positives.
+* Fix - Disables testing instructions clipboard button on HTTP sites when navigator.clipboard is undefined.
+* Fix - fix: missing translations on testing instructions.
+* Fix - fix: platform_global_theme_support_enabled undefined index
+* Fix - fix: testing instructions dark theme support
+* Fix - Fix caching with tracking cookie.
+* Fix - Fixed an issue where the Connect page would scroll to the top upon clicking the Enable Sandbox Mode button.
+* Fix - Fixed default borderRadius value for the express checkout buttons
+* Fix - Fix shipping rates retrieval method for shortcode cart/checkout.
+* Fix - Fix support for merchant site styling when initializing WooPay via classic checkout
+* Fix - Fix WooPay direct checkout.
+* Fix - Handle loadError in ECE for Block Context Initialization.
+* Fix - Move woopay theme support checkbox to the appearance section.
+* Fix - Pass appearance data when initiating WooPay via the email input flow
+* Fix - Prevent preload of BNPL messaging if minimum order amount isn't hit.
+* Fix - Redirect user to WooPay OTP when the email is saved.
+* Fix - Remove obsolete ApplePay warning on wp-admin for test sites.
+* Fix - Update cache after persisting the User session via WooPay
+* Fix - Updates test mode instructions copy for cards at checkout.
+* Update - update: payment method fees in one line
+* Update - Update Jetpack packages to the latest versions
+* Dev - Fix failing e2e tests for saved cards.
+* Dev - Fix Klarna product page message E2E test after the contents inside the iframe were updated.
+* Dev - Migrate Klarna E2E tests to playwright. Reduce noise in E2E tests console output.
+* Dev - Migrate multi-currency e2e tests to Playwright.
+
= 8.1.1 - 2024-08-29 =
* Fix - Ensure 55px is the maximum height for Apple Pay button.
* Fix - Fixed sandbox mode accounts being able to disable test mode for the payment gateway settings.
@@ -119,7 +150,6 @@ Please note that our support for the checkout block is still experimental and th
* Fix - Fix uncaught error on the block based Cart page when WooPayments is disabled.
* Fix - Fix WooPay checkboxes while signed in.
* Fix - If a payment method fails to be created in the frontend during checkout, forward the errors to the server so it can be recorded in an order.
-* Fix - Migrate to Docker Compose V2 for test runner environment setup scripts
* Fix - Reverts changes related to Direct Checkout that broke the PayPal extension.
* Fix - Translate hardcoded strings on the Connect page
* Update - refactor: separate BNPL methods from settings list
@@ -131,6 +161,7 @@ Please note that our support for the checkout block is still experimental and th
* Dev - Match the Node version in nvm with the minimum version in package.json.
* Dev - Remove unnecessary console.warn statements added in #9121.
* Dev - Update bundle size checker workflow to support node v20
+* Dev - Migrate to Docker Compose V2 for test runner environment setup scripts
= 8.0.2 - 2024-08-07 =
* Fix - Add opt-in checks to prevent blocking customers using other payment methods.
diff --git a/tests/e2e-pw/README.md b/tests/e2e-pw/README.md
index 84bf18a95ff..f18388e3f31 100644
--- a/tests/e2e-pw/README.md
+++ b/tests/e2e-pw/README.md
@@ -9,6 +9,9 @@ See [tests/e2e/README.md](/tests/e2e/README.md) for detailed e2e environment set
1. `npm run test:e2e-setup`
1. `npm run test:e2e-up`
+> [!TIP]
+> In case some tests fail due to the lack of `data-test-id` attributes, you'll need to run `npm start` or `NODE_ENV=test npm run build:client` to re-build the assets.
+
## Running Playwright e2e tests
- `npm run test:e2e-pw` headless run from within a linux docker container.
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png
new file mode 100644
index 00000000000..fddb7c0a37e
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Currency-selection--7b0d9-submit-button-when-no-currencies-are-selected-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-83665-tch-by-geolocation-correctly-with-USD-and-GBP-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-83665-tch-by-geolocation-correctly-with-USD-and-GBP-1.png
new file mode 100644
index 00000000000..302a722e0a6
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-83665-tch-by-geolocation-correctly-with-USD-and-GBP-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-d8568-tch-by-geolocation-correctly-with-USD-and-GBP-2.png b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-d8568-tch-by-geolocation-correctly-with-USD-and-GBP-2.png
new file mode 100644
index 00000000000..4956b3153cd
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency-on-boarding.spec.ts/Multi-currency-on-boarding-Geolocation-feature-d8568-tch-by-geolocation-correctly-with-USD-and-GBP-2.png differ
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency.spec.ts/Multi-currency-page-load-without-any-errors-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency.spec.ts/Multi-currency-page-load-without-any-errors-1.png
new file mode 100644
index 00000000000..17d9ff94c0e
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/multi-currency.spec.ts/Multi-currency-page-load-without-any-errors-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts b/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
index cd53d8dce72..1e55cafba1e 100644
--- a/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
+++ b/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
@@ -9,7 +9,7 @@ import { test, expect } from '@playwright/test';
import * as shopper from '../../utils/shopper';
import { config } from '../../config/default';
import { getMerchant, getShopper } from '../../utils/helpers';
-import * as merchant from '../../utils/merchant';
+import { goToOrder } from '../../utils/merchant-navigation';
test.describe(
'Disputes > View dispute details via disputed order notice',
@@ -44,7 +44,7 @@ test.describe(
browser,
} ) => {
const { merchantPage } = await getMerchant( browser );
- await merchant.goToOrder( merchantPage, orderId );
+ await goToOrder( merchantPage, orderId );
// If WC < 7.9, return early since the order dispute notice is not present.
const orderPaymentDetailsContainerVisible = await merchantPage
diff --git a/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts
new file mode 100644
index 00000000000..f987ae71d8e
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/multi-currency-on-boarding.spec.ts
@@ -0,0 +1,223 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+/**
+ * Internal dependencies
+ */
+import { useMerchant } from '../../utils/helpers';
+import {
+ activateMulticurrency,
+ activateTheme,
+ addCurrency,
+ deactivateMulticurrency,
+ disableAllEnabledCurrencies,
+ getActiveThemeSlug,
+ removeCurrency,
+} from '../../utils/merchant';
+import * as navigation from '../../utils/merchant-navigation';
+
+test.describe( 'Multi-currency on-boarding', () => {
+ let page: Page;
+ let wasMulticurrencyEnabled: boolean;
+ let activeThemeSlug: string;
+ const goToNextOnboardingStep = async ( currentPage: Page ) => {
+ await currentPage
+ .locator( '.wcpay-wizard-task.is-active button.is-primary' )
+ .click();
+ };
+
+ useMerchant();
+
+ test.beforeAll( async ( { browser } ) => {
+ page = await browser.newPage();
+ wasMulticurrencyEnabled = await activateMulticurrency( page );
+ activeThemeSlug = await getActiveThemeSlug( page );
+ } );
+
+ test.afterAll( async () => {
+ // Restore original theme.
+ await activateTheme( page, activeThemeSlug );
+
+ if ( ! wasMulticurrencyEnabled ) {
+ await deactivateMulticurrency( page );
+ }
+
+ await page.close();
+ } );
+
+ test.describe( 'Currency selection and management', () => {
+ test.beforeAll( async () => {
+ await disableAllEnabledCurrencies( page );
+ } );
+
+ test.beforeEach( async () => {
+ await navigation.goToMultiCurrencyOnboarding( page );
+ } );
+
+ test( 'should disable the submit button when no currencies are selected', async () => {
+ // To take a better screenshot of the component.
+ await page.setViewportSize( { width: 1280, height: 2000 } );
+ await expect(
+ page.locator(
+ '.multi-currency-setup-wizard > div > .components-card-body'
+ )
+ ).toHaveScreenshot();
+ // Set the viewport back to the default size.
+ await page.setViewportSize( { width: 1280, height: 720 } );
+
+ const checkboxes = await page
+ .locator(
+ 'li.enabled-currency-checkbox .components-checkbox-control__input'
+ )
+ .all();
+
+ for ( const checkbox of checkboxes ) {
+ await checkbox.uncheck();
+ }
+
+ await expect(
+ page.getByRole( 'button', { name: 'Add currencies' } )
+ ).toBeDisabled();
+ } );
+
+ test( 'should allow multiple currencies to be selected', async () => {
+ const currenciesNotInRecommendedList = await page
+ .locator(
+ 'li.enabled-currency-checkbox:not([data-testid="recommended-currency"]) input[type="checkbox"]'
+ )
+ .all();
+
+ // We don't need to check them all.
+ const maximumCurrencies =
+ currenciesNotInRecommendedList.length > 3
+ ? 3
+ : currenciesNotInRecommendedList.length;
+
+ for ( let i = 0; i < maximumCurrencies; i++ ) {
+ await expect(
+ currenciesNotInRecommendedList[ i ]
+ ).toBeEnabled();
+ await currenciesNotInRecommendedList[ i ].check();
+ await expect(
+ currenciesNotInRecommendedList[ i ]
+ ).toBeChecked();
+ }
+ } );
+
+ test( 'should exclude already enabled currencies from the onboarding', async () => {
+ await addCurrency( page, 'GBP' );
+ await navigation.goToMultiCurrencyOnboarding( page );
+
+ const recommendedCurrencies = await page
+ .getByTestId( 'recommended-currency' )
+ .allTextContents();
+
+ for ( const currency of recommendedCurrencies ) {
+ expect( currency ).not.toMatch( /GBP/ );
+ }
+
+ await removeCurrency( page, 'GBP' );
+ } );
+
+ test( 'should display suggested currencies at the beginning of the list', async () => {
+ await expect(
+ ( await page.getByTestId( 'recommended-currency' ).all() )
+ .length
+ ).toBeGreaterThan( 0 );
+ } );
+
+ test( 'selected currencies are enabled after onboarding', async () => {
+ const currencyCodes = [ 'GBP', 'EUR', 'CAD', 'AUD' ];
+
+ for ( const currencyCode of currencyCodes ) {
+ await page
+ .locator(
+ `input[type="checkbox"][code="${ currencyCode }"]`
+ )
+ .check();
+ }
+
+ await goToNextOnboardingStep( page );
+ await navigation.goToMultiCurrencySettings( page );
+
+ // Ensure the currencies are enabled.
+ for ( const currencyCode of currencyCodes ) {
+ await expect(
+ page.locator(
+ `li.enabled-currency.${ currencyCode.toLowerCase() }`
+ )
+ ).toBeVisible();
+ }
+ } );
+ } );
+
+ test.describe( 'Geolocation features', () => {
+ test( 'should offer currency switch by geolocation', async () => {
+ await navigation.goToMultiCurrencyOnboarding( page );
+ await goToNextOnboardingStep( page );
+ await page.getByTestId( 'enable_auto_currency' ).check();
+ await expect(
+ page.getByTestId( 'enable_auto_currency' )
+ ).toBeChecked();
+ } );
+
+ test( 'should preview currency switch by geolocation correctly with USD and GBP', async () => {
+ await addCurrency( page, 'GBP' );
+ await navigation.goToMultiCurrencyOnboarding( page );
+ // To take a better screenshot of the iframe preview.
+ await page.setViewportSize( { width: 1280, height: 1280 } );
+ await goToNextOnboardingStep( page );
+ await expect(
+ page.locator( '.wcpay-wizard-task.is-active' )
+ ).toHaveScreenshot();
+ await page.getByTestId( 'enable_auto_currency' ).check();
+ await page.getByRole( 'button', { name: 'Preview' } ).click();
+
+ const previewIframe = await page.locator(
+ '.multi-currency-store-settings-preview-iframe'
+ );
+
+ await expect( previewIframe ).toBeVisible();
+
+ const previewPage = previewIframe.contentFrame();
+
+ await expect(
+ await previewPage.locator( '.woocommerce-store-notice' )
+ ).toBeVisible();
+ await expect(
+ page.locator( '.multi-currency-store-settings-preview-iframe' )
+ ).toHaveScreenshot();
+
+ const noticeText = await previewPage
+ .locator( '.woocommerce-store-notice' )
+ .innerText();
+
+ expect( noticeText ).toContain(
+ "We noticed you're visiting from United Kingdom (UK). We've updated our prices to Pound sterling for your shopping convenience."
+ );
+ } );
+ } );
+
+ test.describe( 'Currency Switcher widget', () => {
+ test( 'should offer the currency switcher widget while Storefront theme is active', async () => {
+ await activateTheme( page, 'storefront' );
+ await navigation.goToMultiCurrencyOnboarding( page );
+ await goToNextOnboardingStep( page );
+ await page.getByTestId( 'enable_storefront_switcher' ).check();
+ await expect(
+ page.getByTestId( 'enable_storefront_switcher' )
+ ).toBeChecked();
+ } );
+
+ test( 'should not offer the currency switcher widget when an unsupported theme is active', async () => {
+ await activateTheme( page, 'twentytwentyfour' );
+ await navigation.goToMultiCurrencyOnboarding( page );
+ await goToNextOnboardingStep( page );
+ await expect(
+ page.getByTestId( 'enable_storefront_switcher' )
+ ).toBeHidden();
+ await activateTheme( page, 'storefront' );
+ } );
+ } );
+} );
diff --git a/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts
new file mode 100644
index 00000000000..515100ab2dc
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/multi-currency-setup.spec.ts
@@ -0,0 +1,222 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+/**
+ * Internal dependencies
+ */
+import { getMerchant, getShopper } from '../../utils/helpers';
+import {
+ activateMulticurrency,
+ addCurrency,
+ deactivateMulticurrency,
+ disableAllEnabledCurrencies,
+ removeCurrency,
+ setCurrencyCharmPricing,
+ setCurrencyPriceRounding,
+ setCurrencyRate,
+} from '../../utils/merchant';
+import * as navigation from '../../utils/shopper-navigation';
+import { getPriceFromProduct } from '../../utils/shopper';
+
+test.describe( 'Multi-currency setup', () => {
+ let merchantPage: Page;
+ let shopperPage: Page;
+ let wasMulticurrencyEnabled: boolean;
+
+ test.beforeAll( async ( { browser } ) => {
+ shopperPage = ( await getShopper( browser ) ).shopperPage;
+ merchantPage = ( await getMerchant( browser ) ).merchantPage;
+ wasMulticurrencyEnabled = await activateMulticurrency( merchantPage );
+ } );
+
+ test.afterAll( async () => {
+ if ( ! wasMulticurrencyEnabled ) {
+ await deactivateMulticurrency( merchantPage );
+ }
+ } );
+
+ test( 'can disable the multi-currency feature', async () => {
+ await deactivateMulticurrency( merchantPage );
+ } );
+
+ test( 'can enable the multi-currency feature', async () => {
+ await activateMulticurrency( merchantPage );
+ } );
+
+ test.describe( 'Currency management', () => {
+ const testCurrency = 'CHF';
+
+ test( 'can add a new currency', async () => {
+ await addCurrency( merchantPage, testCurrency );
+ } );
+
+ test( 'can remove a currency', async () => {
+ await removeCurrency( merchantPage, testCurrency );
+ } );
+ } );
+
+ test.describe( 'Currency settings', () => {
+ let beanieRegularPrice: string;
+ const testData = {
+ currencyCode: 'CHF',
+ rate: '1.25',
+ charmPricing: '-0.01',
+ rounding: '0.5',
+ currencyPrecision: 2,
+ };
+
+ test.beforeAll( async () => {
+ await disableAllEnabledCurrencies( merchantPage );
+ await navigation.goToShopWithCurrency( shopperPage, 'USD' );
+
+ beanieRegularPrice = await getPriceFromProduct(
+ shopperPage,
+ 'beanie'
+ );
+ } );
+
+ test.beforeEach( async () => {
+ await addCurrency( merchantPage, testData.currencyCode );
+ } );
+
+ test.afterEach( async () => {
+ await removeCurrency( merchantPage, testData.currencyCode );
+ } );
+
+ test( 'can change the currency rate manually', async () => {
+ await setCurrencyRate(
+ merchantPage,
+ testData.currencyCode,
+ testData.rate
+ );
+ await setCurrencyPriceRounding(
+ merchantPage,
+ testData.currencyCode,
+ '0'
+ );
+ await navigation.goToShopWithCurrency(
+ shopperPage,
+ testData.currencyCode
+ );
+
+ const beaniePriceOnCurrency = await getPriceFromProduct(
+ shopperPage,
+ 'beanie'
+ );
+
+ expect(
+ parseFloat( beaniePriceOnCurrency ).toFixed(
+ testData.currencyPrecision
+ )
+ ).toEqual(
+ (
+ parseFloat( beanieRegularPrice ) *
+ parseFloat( testData.rate )
+ ).toFixed( testData.currencyPrecision )
+ );
+ } );
+
+ test( 'can change the charm price manually', async () => {
+ await setCurrencyRate(
+ merchantPage,
+ testData.currencyCode,
+ '1.00'
+ );
+ await setCurrencyPriceRounding(
+ merchantPage,
+ testData.currencyCode,
+ '0'
+ );
+ await setCurrencyCharmPricing(
+ merchantPage,
+ testData.currencyCode,
+ testData.charmPricing
+ );
+ await navigation.goToShopWithCurrency(
+ shopperPage,
+ testData.currencyCode
+ );
+
+ const beaniePriceOnCurrency = await getPriceFromProduct(
+ shopperPage,
+ 'beanie'
+ );
+
+ expect(
+ parseFloat( beaniePriceOnCurrency ).toFixed(
+ testData.currencyPrecision
+ )
+ ).toEqual(
+ (
+ parseFloat( beanieRegularPrice ) +
+ parseFloat( testData.charmPricing )
+ ).toFixed( testData.currencyPrecision )
+ );
+ } );
+
+ test( 'can change the rounding precision manually', async () => {
+ const rateForTest = '1.20';
+
+ await setCurrencyRate(
+ merchantPage,
+ testData.currencyCode,
+ rateForTest
+ );
+ await setCurrencyPriceRounding(
+ merchantPage,
+ testData.currencyCode,
+ testData.rounding
+ );
+
+ const beaniePriceOnCurrency = await getPriceFromProduct(
+ shopperPage,
+ 'beanie'
+ );
+
+ expect(
+ parseFloat( beaniePriceOnCurrency ).toFixed(
+ testData.currencyPrecision
+ )
+ ).toEqual(
+ (
+ Math.ceil(
+ parseFloat( beanieRegularPrice ) *
+ parseFloat( rateForTest ) *
+ ( 1 / parseFloat( testData.rounding ) )
+ ) * parseFloat( testData.rounding )
+ ).toFixed( testData.currencyPrecision )
+ );
+ } );
+ } );
+
+ test.describe( 'Currency decimal points', () => {
+ const currencyDecimalMap = {
+ JPY: 0,
+ GBP: 2,
+ };
+
+ test.beforeAll( async () => {
+ for ( const currency of Object.keys( currencyDecimalMap ) ) {
+ await addCurrency( merchantPage, currency );
+ }
+ } );
+
+ Object.keys( currencyDecimalMap ).forEach( ( currency: string ) => {
+ test( `the decimal points for ${ currency } are displayed correctly`, async () => {
+ await navigation.goToShopWithCurrency( shopperPage, currency );
+
+ const beaniePriceOnCurrency = await getPriceFromProduct(
+ shopperPage,
+ 'beanie'
+ );
+ const decimalPart =
+ beaniePriceOnCurrency.split( '.' )[ 1 ] || '';
+
+ expect( decimalPart.length ).toEqual(
+ currencyDecimalMap[ currency ]
+ );
+ } );
+ } );
+ } );
+} );
diff --git a/tests/e2e-pw/specs/merchant/multi-currency.spec.ts b/tests/e2e-pw/specs/merchant/multi-currency.spec.ts
new file mode 100644
index 00000000000..45b6bf0b89b
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/multi-currency.spec.ts
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+/**
+ * Internal dependencies
+ */
+import { useMerchant } from '../../utils/helpers';
+import {
+ activateMulticurrency,
+ addMulticurrencyWidget,
+ deactivateMulticurrency,
+ disableAllEnabledCurrencies,
+} from '../../utils/merchant';
+import * as navigation from '../../utils/merchant-navigation';
+
+test.describe( 'Multi-currency', () => {
+ let wasMulticurrencyEnabled: boolean;
+ let page: Page;
+
+ // Use the merchant user for this test suite.
+ useMerchant();
+
+ test.beforeAll( async ( { browser } ) => {
+ page = await browser.newPage();
+ wasMulticurrencyEnabled = await activateMulticurrency( page );
+
+ await disableAllEnabledCurrencies( page );
+ } );
+
+ test.afterAll( async () => {
+ if ( ! wasMulticurrencyEnabled ) {
+ await deactivateMulticurrency( page );
+ }
+ await page.close();
+ } );
+
+ test( 'page load without any errors', async () => {
+ await navigation.goToMultiCurrencySettings( page );
+ await expect(
+ page.getByRole( 'heading', { name: 'Enabled currencies' } )
+ ).toBeVisible();
+ await expect( page.getByText( 'Default currency' ) ).toBeVisible();
+ await expect(
+ page.locator( '.multi-currency-settings' ).last()
+ ).toHaveScreenshot();
+ } );
+
+ test( 'add the currency switcher to the sidebar', async () => {
+ await addMulticurrencyWidget( page );
+ } );
+
+ test( 'can add the currency switcher to a post/page', async () => {
+ await navigation.goToNewPost( page );
+
+ if ( await page.getByRole( 'button', { name: 'Close' } ).isVisible() ) {
+ await page.getByRole( 'button', { name: 'Close' } ).click();
+ }
+
+ await page.getByRole( 'button', { name: 'Add block' } ).click();
+ await page
+ .locator( 'input[placeholder="Search"]' )
+ .pressSequentially( 'switcher', { delay: 20 } );
+ await expect(
+ page.getByRole( 'option', { name: 'Currency Switcher Block' } )
+ ).toBeVisible();
+ } );
+} );
diff --git a/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts b/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts
new file mode 100644
index 00000000000..b810f32ecb0
--- /dev/null
+++ b/tests/e2e-pw/specs/shopper/klarna-checkout-purchase.spec.ts
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import * as shopper from '../../utils/shopper';
+import { getMerchant, getShopper } from '../../utils/helpers';
+import * as merchant from '../../utils/merchant';
+import { config } from '../../config/default';
+import {
+ goToProductPageBySlug,
+ goToShop,
+} from '../../utils/shopper-navigation';
+
+test.describe( 'Klarna Checkout', () => {
+ let merchantPage: Page;
+ let shopperPage: Page;
+ let wasMulticurrencyEnabled: boolean;
+
+ test.beforeAll( async ( { browser } ) => {
+ shopperPage = ( await getShopper( browser ) ).shopperPage;
+ merchantPage = ( await getMerchant( browser ) ).merchantPage;
+ wasMulticurrencyEnabled = await merchant.isMulticurrencyEnabled(
+ merchantPage
+ );
+ if ( wasMulticurrencyEnabled ) {
+ await merchant.deactivateMulticurrency( merchantPage );
+ }
+ await merchant.enablePaymentMethods( merchantPage, [ 'klarna' ] );
+ } );
+
+ test.afterAll( async () => {
+ await shopper.emptyCart( shopperPage );
+
+ await merchant.disablePaymentMethods( merchantPage, [ 'klarna' ] );
+
+ if ( wasMulticurrencyEnabled ) {
+ await merchant.activateMulticurrency( merchantPage );
+ }
+ } );
+
+ test( 'shows the message in the product page', async () => {
+ await goToProductPageBySlug( shopperPage, 'belt' );
+
+ // Since we cant' control the exact contents of the iframe, we just make sure it's there.
+ await expect(
+ shopperPage
+ .frameLocator( '#payment-method-message iframe' )
+ .locator( 'body' )
+ ).not.toBeEmpty();
+ } );
+
+ test( 'allows to use Klarna as a payment method', async () => {
+ await goToShop( shopperPage );
+ await shopper.setupProductCheckout( shopperPage, [ [ 'Belt', 1 ] ], {
+ ...config.addresses.customer.billing,
+ // these are Klarna-specific values:
+ // https://docs.klarna.com/resources/test-environment/sample-customer-data/#united-states-of-america
+ email: 'customer@email.us',
+ phone: '+13106683312',
+ firstname: 'Test',
+ lastname: 'Person-us',
+ } );
+
+ await shopperPage
+ .locator( '.wc_payment_methods' )
+ .getByText( 'Klarna' )
+ .click();
+
+ await shopper.placeOrder( shopperPage );
+
+ // Since we don't have control over the html in the Klarna playground page,
+ // verifying the redirect is all we can do consistently without introducing a
+ // flaky test.
+ await expect( shopperPage ).toHaveURL( /.*klarna\.com/ );
+ } );
+} );
diff --git a/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts b/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts
new file mode 100644
index 00000000000..fa5453b70e4
--- /dev/null
+++ b/tests/e2e-pw/specs/shopper/multi-currency-checkout.spec.ts
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+/**
+ * Internal dependencies
+ */
+import { getMerchant, getShopper } from '../../utils/helpers';
+import {
+ activateMulticurrency,
+ addCurrency,
+ deactivateMulticurrency,
+ removeCurrency,
+} from '../../utils/merchant';
+import { emptyCart, placeOrderWithCurrency } from '../../utils/shopper';
+import * as navigation from '../../utils/shopper-navigation';
+
+test.describe( 'Multi-currency checkout', () => {
+ let merchantPage: Page;
+ let shopperPage: Page;
+ let wasMulticurrencyEnabled: boolean;
+ const currenciesOrders = {
+ USD: null,
+ EUR: null,
+ };
+
+ test.beforeAll( async ( { browser } ) => {
+ shopperPage = ( await getShopper( browser ) ).shopperPage;
+ merchantPage = ( await getMerchant( browser ) ).merchantPage;
+ wasMulticurrencyEnabled = await activateMulticurrency( merchantPage );
+
+ await addCurrency( merchantPage, 'EUR' );
+ } );
+
+ test.afterAll( async () => {
+ await removeCurrency( merchantPage, 'EUR' );
+ await emptyCart( shopperPage );
+
+ if ( ! wasMulticurrencyEnabled ) {
+ await deactivateMulticurrency( merchantPage );
+ }
+ } );
+
+ test.describe( `Checkout with multiple currencies`, async () => {
+ Object.keys( currenciesOrders ).forEach( ( currency: string ) => {
+ test( `checkout with ${ currency }`, async () => {
+ await test.step( `pay with ${ currency }`, async () => {
+ currenciesOrders[ currency ] = await placeOrderWithCurrency(
+ shopperPage,
+ currency
+ );
+ } );
+
+ await test.step(
+ `should display ${ currency } in the order received page`,
+ async () => {
+ await expect(
+ shopperPage.locator(
+ '.woocommerce-order-overview__total'
+ )
+ ).toHaveText( new RegExp( currency ) );
+ }
+ );
+
+ await test.step(
+ `should display ${ currency } in the customer order page`,
+ async () => {
+ await navigation.goToOrder(
+ shopperPage,
+ currenciesOrders[ currency ]
+ );
+ await expect(
+ shopperPage.locator(
+ '.woocommerce-table--order-details tfoot tr:last-child td'
+ )
+ ).toHaveText( new RegExp( currency ) );
+ }
+ );
+ } );
+ } );
+ } );
+
+ test.describe( 'My account', () => {
+ test( 'should display the correct currency in the my account order history table', async () => {
+ await navigation.goToOrders( shopperPage );
+
+ for ( const currency in currenciesOrders ) {
+ if ( currenciesOrders[ currency ] ) {
+ await expect(
+ shopperPage.locator( 'tr' ).filter( {
+ has: shopperPage.getByText(
+ `#${ currenciesOrders[ currency ] }`
+ ),
+ } )
+ ).toHaveText( new RegExp( currency ) );
+ }
+ }
+ } );
+ } );
+} );
diff --git a/tests/e2e-pw/utils/merchant-navigation.ts b/tests/e2e-pw/utils/merchant-navigation.ts
new file mode 100644
index 00000000000..b91c31ba097
--- /dev/null
+++ b/tests/e2e-pw/utils/merchant-navigation.ts
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+import { dataHasLoaded } from './merchant';
+
+export const goToOrder = async ( page: Page, orderId: string ) => {
+ await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` );
+};
+
+export const goToWooPaymentsSettings = async ( page: Page ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments'
+ );
+};
+
+export const goToMultiCurrencySettings = async ( page: Page ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=wcpay_multi_currency',
+ { waitUntil: 'load' }
+ );
+ await dataHasLoaded( page );
+};
+
+export const goToWidgets = async ( page: Page ) => {
+ await page.goto( '/wp-admin/widgets.php', {
+ waitUntil: 'load',
+ } );
+};
+
+export const goToNewPost = async ( page: Page ) => {
+ await page.goto( '/wp-admin/post-new.php', {
+ waitUntil: 'load',
+ } );
+};
+
+export const goToThemes = async ( page: Page ) => {
+ await page.goto( '/wp-admin/themes.php', {
+ waitUntil: 'load',
+ } );
+};
+
+export const goToMultiCurrencyOnboarding = async ( page: Page ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=%2Fpayments%2Fmulti-currency-setup',
+ { waitUntil: 'load' }
+ );
+ await dataHasLoaded( page );
+};
diff --git a/tests/e2e-pw/utils/merchant.ts b/tests/e2e-pw/utils/merchant.ts
index 0f358e24491..2cbefa58615 100644
--- a/tests/e2e-pw/utils/merchant.ts
+++ b/tests/e2e-pw/utils/merchant.ts
@@ -1,11 +1,252 @@
/**
* External dependencies
*/
-import { Page } from 'playwright/test';
+import { Page, expect } from 'playwright/test';
+import * as navigation from './merchant-navigation';
-export const goToOrder = async (
+/**
+ * Checks if the data has loaded on the page.
+ * This check only applies to WooPayments settings pages.
+ *
+ * @param {Page} page The page object.
+ */
+export const dataHasLoaded = async ( page: Page ) => {
+ await expect( page.locator( '.is-loadable-placeholder' ) ).toHaveCount( 0 );
+};
+
+export const saveWooPaymentsSettings = async ( page: Page ) => {
+ await page.getByRole( 'button', { name: 'Save changes' } ).click();
+ await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( {
+ timeout: 10000,
+ } );
+};
+
+export const isMulticurrencyEnabled = async ( page: Page ) => {
+ await navigation.goToWooPaymentsSettings( page );
+
+ const checkboxTestId = 'multi-currency-toggle';
+ const isEnabled = await page.getByTestId( checkboxTestId ).isChecked();
+
+ return isEnabled;
+};
+
+export const activateMulticurrency = async ( page: Page ) => {
+ await navigation.goToWooPaymentsSettings( page );
+
+ const checkboxTestId = 'multi-currency-toggle';
+ const wasInitiallyEnabled = await isMulticurrencyEnabled( page );
+
+ if ( ! wasInitiallyEnabled ) {
+ await page.getByTestId( checkboxTestId ).check();
+ await saveWooPaymentsSettings( page );
+ }
+ return wasInitiallyEnabled;
+};
+
+export const deactivateMulticurrency = async ( page: Page ) => {
+ await navigation.goToWooPaymentsSettings( page );
+ await page.getByTestId( 'multi-currency-toggle' ).uncheck();
+ await saveWooPaymentsSettings( page );
+};
+
+export const addMulticurrencyWidget = async ( page: Page ) => {
+ await navigation.goToWidgets( page );
+ // Wait for all widgets to load. This is important to prevent flakiness.
+ await expect( page.locator( '.components-spinner' ) ).toHaveCount( 0 );
+
+ if ( await page.getByRole( 'button', { name: 'Close' } ).isVisible() ) {
+ await page.getByRole( 'button', { name: 'Close' } ).click();
+ }
+
+ const isWidgetAdded = await page
+ .getByRole( 'heading', { name: 'Currency Switcher Widget' } )
+ .isVisible();
+
+ if ( ! isWidgetAdded ) {
+ await page.getByRole( 'button', { name: 'Add block' } ).click();
+ await page
+ .locator( 'input[placeholder="Search"]' )
+ .pressSequentially( 'switcher', { delay: 20 } );
+ await expect(
+ page.locator( 'button.components-button[role="option"]' ).first()
+ ).toBeVisible( { timeout: 5000 } );
+ await page
+ .locator( 'button.components-button[role="option"]' )
+ .first()
+ .click();
+ await page.waitForTimeout( 2000 );
+ await expect(
+ page.getByRole( 'button', { name: 'Update' } )
+ ).toBeEnabled();
+ await page.getByRole( 'button', { name: 'Update' } ).click();
+ await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( {
+ timeout: 10000,
+ } );
+ }
+};
+
+export const getActiveThemeSlug = async ( page: Page ) => {
+ await navigation.goToThemes( page );
+
+ const activeTheme = await page.locator( '.theme.active' );
+
+ return ( await activeTheme.getAttribute( 'data-slug' ) ) ?? '';
+};
+
+export const activateTheme = async ( page: Page, slug: string ) => {
+ await navigation.goToThemes( page );
+
+ const isThemeActive = ( await getActiveThemeSlug( page ) ) === slug;
+
+ if ( ! isThemeActive ) {
+ await page
+ .locator( `.theme[data-slug="${ slug }"] .button.activate` )
+ .click();
+ await expect(
+ await page.locator( '.notice.updated' ).innerText()
+ ).toContain( 'New theme activated.' );
+ }
+};
+
+export const disableAllEnabledCurrencies = async ( page: Page ) => {
+ await navigation.goToMultiCurrencySettings( page );
+ await expect(
+ await page.locator( '.enabled-currencies-list li' ).first()
+ ).toBeVisible();
+
+ const deleteButtons = await page
+ .locator( '.enabled-currency .enabled-currency__action.delete' )
+ .all();
+
+ if ( deleteButtons.length === 0 ) {
+ return;
+ }
+
+ for ( let i = 0; i < deleteButtons.length; i++ ) {
+ await page
+ .locator( '.enabled-currency .enabled-currency__action.delete' )
+ .first()
+ .click();
+
+ const snackbar = await page.getByLabel( 'Dismiss this notice' );
+
+ await expect( snackbar ).toBeVisible( { timeout: 10000 } );
+ await snackbar.click();
+ await expect( snackbar ).toBeHidden( { timeout: 10000 } );
+ }
+};
+
+export const addCurrency = async ( page: Page, currencyCode: string ) => {
+ // Default currency.
+ if ( currencyCode === 'USD' ) {
+ return;
+ }
+
+ await navigation.goToMultiCurrencySettings( page );
+ await page.getByTestId( 'enabled-currencies-add-button' ).click();
+
+ const checkbox = await page.locator(
+ `input[type="checkbox"][code="${ currencyCode }"]`
+ );
+
+ if ( ! ( await checkbox.isChecked() ) ) {
+ await checkbox.check();
+ }
+
+ await page.getByRole( 'button', { name: 'Update selected' } ).click();
+ await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( {
+ timeout: 10000,
+ } );
+ await expect(
+ page.locator( `li.enabled-currency.${ currencyCode.toLowerCase() }` )
+ ).toBeVisible();
+};
+
+export const removeCurrency = async ( page: Page, currencyCode: string ) => {
+ await navigation.goToMultiCurrencySettings( page );
+ await page
+ .locator(
+ `li.enabled-currency.${ currencyCode.toLowerCase() } .enabled-currency__action.delete`
+ )
+ .click();
+ await expect( page.getByLabel( 'Dismiss this notice' ) ).toBeVisible( {
+ timeout: 10000,
+ } );
+ await expect(
+ page.locator( `li.enabled-currency.${ currencyCode.toLowerCase() }` )
+ ).toBeHidden();
+};
+
+export const editCurrency = async ( page: Page, currencyCode: string ) => {
+ await navigation.goToMultiCurrencySettings( page );
+ await page
+ .locator(
+ `.enabled-currency.${ currencyCode.toLowerCase() } .enabled-currency__action.edit`
+ )
+ .click();
+ await dataHasLoaded( page );
+};
+
+export const setCurrencyRate = async (
page: Page,
- orderId: string
-): Promise< void > => {
- await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` );
+ currencyCode: string,
+ rate: string
+) => {
+ await editCurrency( page, currencyCode );
+ await page
+ .locator( '#single-currency-settings__manual_rate_radio' )
+ .click();
+ await page.getByTestId( 'manual_rate_input' ).fill( rate );
+ await saveWooPaymentsSettings( page );
+};
+
+export const setCurrencyPriceRounding = async (
+ page: Page,
+ currencyCode: string,
+ rounding: string
+) => {
+ await editCurrency( page, currencyCode );
+ await page.getByTestId( 'price_rounding' ).selectOption( rounding );
+ await saveWooPaymentsSettings( page );
+};
+
+export const setCurrencyCharmPricing = async (
+ page: Page,
+ currencyCode: string,
+ charmPricing: string
+) => {
+ await editCurrency( page, currencyCode );
+ await page.getByTestId( 'price_charm' ).selectOption( charmPricing );
+ await saveWooPaymentsSettings( page );
+};
+
+export const enablePaymentMethods = async (
+ page: Page,
+ paymentMethods: string[]
+) => {
+ await navigation.goToWooPaymentsSettings( page );
+
+ for ( const paymentMethodName of paymentMethods ) {
+ await page.getByLabel( paymentMethodName ).check();
+ }
+
+ await saveWooPaymentsSettings( page );
+};
+
+export const disablePaymentMethods = async (
+ page: Page,
+ paymentMethods: string[]
+) => {
+ await navigation.goToWooPaymentsSettings( page );
+
+ for ( const paymentMethodName of paymentMethods ) {
+ const checkbox = await page.getByLabel( paymentMethodName );
+
+ if ( await checkbox.isChecked() ) {
+ await checkbox.click();
+ await page.getByRole( 'button', { name: 'Remove' } ).click();
+ }
+ }
+
+ await saveWooPaymentsSettings( page );
};
diff --git a/tests/e2e-pw/utils/shopper-navigation.ts b/tests/e2e-pw/utils/shopper-navigation.ts
new file mode 100644
index 00000000000..7f96a1e9055
--- /dev/null
+++ b/tests/e2e-pw/utils/shopper-navigation.ts
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+/**
+ * Internal dependencies
+ */
+import { isUIUnblocked } from './shopper';
+
+export const goToShop = async ( page: Page ) => {
+ await page.goto( `/shop/`, { waitUntil: 'load' } );
+};
+
+export const goToShopWithCurrency = async ( page: Page, currency: string ) => {
+ await page.goto( `/shop/?currency=${ currency }`, { waitUntil: 'load' } );
+};
+
+export const goToProductPageBySlug = async (
+ page: Page,
+ productSlug: string
+) => {
+ await page.goto( `/product/${ productSlug }`, { waitUntil: 'load' } );
+};
+
+export const goToCart = async ( page: Page ) => {
+ await page.goto( '/cart/', { waitUntil: 'load' } );
+ isUIUnblocked( page );
+};
+
+export const goToCheckout = async ( page: Page ) => {
+ await page.goto( '/checkout/', { waitUntil: 'load' } );
+ isUIUnblocked( page );
+};
+
+export const goToOrders = async ( page: Page ) => {
+ await page.goto( '/my-account/orders/', {
+ waitUntil: 'load',
+ } );
+};
+
+export const goToOrder = async ( page: Page, orderId: string ) => {
+ await page.goto( `/my-account/view-order/${ orderId }`, {
+ waitUntil: 'load',
+ } );
+};
diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts
index 9c984811cb8..b39869b8426 100644
--- a/tests/e2e-pw/utils/shopper.ts
+++ b/tests/e2e-pw/utils/shopper.ts
@@ -1,18 +1,21 @@
/**
* External dependencies
*/
-import { Page } from 'playwright/test';
-
+import { Page, expect } from 'playwright/test';
/**
* Internal dependencies
*/
-
+import * as navigation from './shopper-navigation';
import { config, CustomerAddress } from '../config/default';
+export const isUIUnblocked = async ( page: Page ) => {
+ await expect( page.locator( '.blockUI' ) ).toHaveCount( 0 );
+};
+
export const fillBillingAddress = async (
page: Page,
billingAddress: CustomerAddress
-): Promise< void > => {
+) => {
await page
.locator( '#billing_first_name' )
.fill( billingAddress.firstname );
@@ -34,21 +37,30 @@ export const fillBillingAddress = async (
await page.locator( '#billing_email' ).fill( billingAddress.email );
};
-export const placeOrder = async ( page: Page ): Promise< void > => {
- await page.locator( '#place_order' ).click();
+// This is currently the source of some flaky tests since sometimes the form is not submitted
+// after the first click, so we retry until the ui is blocked.
+export const placeOrder = async ( page: Page ) => {
+ let orderPlaced = false;
+ while ( ! orderPlaced ) {
+ await page.locator( '#place_order' ).click();
+
+ if ( await page.$( '.blockUI' ) ) {
+ orderPlaced = true;
+ }
+ }
};
export const addCartProduct = async (
page: Page,
productId = 16 // Beanie
-): Promise< void > => {
+) => {
await page.goto( `/shop/?add-to-cart=${ productId }` );
};
export const fillCardDetails = async (
page: Page,
card = config.cards.basic
-): Promise< void > => {
+) => {
if (
await page.$(
'#payment .payment_method_woocommerce_payments .wcpay-upe-element'
@@ -96,7 +108,7 @@ export const fillCardDetails = async (
export const confirmCardAuthentication = async (
page: Page,
authorize = true
-): Promise< void > => {
+) => {
// Stripe card input also uses __privateStripeFrame as a prefix, so need to make sure we wait for an iframe that
// appears at the top of the DOM.
await page.waitForSelector(
@@ -122,3 +134,162 @@ export const confirmCardAuthentication = async (
await button.click();
};
+
+/**
+ * Retrieves the product price from the current product page.
+ *
+ * This function assumes that the page object has already navigated to a product page.
+ */
+export const getPriceFromProduct = async ( page: Page, slug: string ) => {
+ await navigation.goToProductPageBySlug( page, slug );
+
+ const priceText = await page
+ .locator( 'ins .woocommerce-Price-amount.amount' )
+ .first()
+ .textContent();
+
+ return priceText?.replace( /[^0-9.,]/g, '' ) ?? '';
+};
+
+/**
+ * Adds a product to the cart from the shop page.
+ *
+ * @param {Page} page The Playwright page object.
+ * @param {string|number} product The product ID or title to add to the cart.
+ */
+export const addToCartFromShopPage = async (
+ page: Page,
+ product: string | number
+) => {
+ if ( Number.isInteger( product ) ) {
+ const addToCartSelector = `a[data-product_id="${ product }"]`;
+
+ await page.locator( addToCartSelector ).click();
+ await expect(
+ page.locator( `${ addToCartSelector }.added` )
+ ).toBeVisible();
+ } else {
+ // These unicode characters are the smart (or curly) quotes: “ ”.
+ const addToCartRegex = new RegExp(
+ `Add to cart: \u201C${ product }\u201D`
+ );
+
+ await page.getByLabel( addToCartRegex ).click();
+ await expect( page.getByLabel( addToCartRegex ) ).toHaveAttribute(
+ 'class',
+ /added/
+ );
+ }
+};
+
+export const setupCheckout = async (
+ page: Page,
+ billingAddress: CustomerAddress
+) => {
+ await navigation.goToCheckout( page );
+ await fillBillingAddress( page, billingAddress );
+ // Woo core blocks and refreshes the UI after 1s after each key press
+ // in a text field or immediately after a select field changes.
+ // We need to wait to make sure that all key presses were processed by that mechanism.
+ await page.waitForTimeout( 1000 );
+ await isUIUnblocked( page );
+ await page
+ .locator( '.wc_payment_method.payment_method_woocommerce_payments' )
+ .click();
+};
+
+/**
+ * Sets up checkout with any number of products.
+ *
+ * @param {Array<[string, number]>} lineItems A 2D array of line items where each line item is an array
+ * that contains the product title as the first element, and the quantity as the second.
+ * For example, if you want to checkout x2 "Hoodie" and x3 "Belt" then set this parameter like this:
+ *
+ * `[ [ "Hoodie", 2 ], [ "Belt", 3 ] ]`.
+ * @param {CustomerAddress} billingAddress The billing address to use for the checkout.
+ */
+export async function setupProductCheckout(
+ page: Page,
+ lineItems: Array< [ string, number ] > = [
+ [ config.products.simple.name, 1 ],
+ ],
+ billingAddress: CustomerAddress = config.addresses.customer.billing
+) {
+ const cartSizeText = await page
+ .locator( '.cart-contents .count' )
+ .textContent();
+ let cartSize = Number( cartSizeText?.replace( /\D/g, '' ) ?? '0' );
+
+ for ( const line of lineItems ) {
+ let [ productTitle, qty ] = line;
+
+ while ( qty-- ) {
+ await addToCartFromShopPage( page, productTitle );
+ // Make sure the number of items in the cart is incremented before adding another item.
+ await expect( page.locator( '.cart-contents .count' ) ).toHaveText(
+ new RegExp( `${ ++cartSize } items?` ),
+ {
+ timeout: 30000,
+ }
+ );
+ }
+ }
+
+ await setupCheckout( page, billingAddress );
+}
+
+/**
+ * Places an order with a specified currency.
+ *
+ * @param {Page} page The Playwright page object.
+ * @param {string} currency The currency code to use for the order.
+ * @return {Promise
} The order ID.
+ */
+export const placeOrderWithCurrency = async (
+ page: Page,
+ currency: string
+) => {
+ await navigation.goToShopWithCurrency( page, currency );
+ await setupProductCheckout( page, [ [ config.products.simple.name, 1 ] ] );
+ await fillCardDetails( page, config.cards.basic );
+ // Takes off the focus out of the Stripe elements to let Stripe logic
+ // wrap up and make sure the Place Order button is clickable.
+ await page.locator( '#place_order' ).focus();
+ await page.waitForTimeout( 1000 );
+ await placeOrder( page );
+ await page.waitForURL( /\/order-received\//, { waitUntil: 'load' } );
+ await expect(
+ page.getByRole( 'heading', { name: 'Order received' } )
+ ).toBeVisible();
+
+ const url = await page.url();
+ return url.match( /\/order-received\/(\d+)\// )?.[ 1 ] ?? '';
+};
+
+export const emptyCart = async ( page: Page ) => {
+ await navigation.goToCart( page );
+
+ // Remove products if they exist.
+ let products = await page.locator( '.remove' ).all();
+
+ while ( products.length ) {
+ await products[ 0 ].click();
+ await isUIUnblocked( page );
+
+ products = await page.locator( '.remove' ).all();
+ }
+
+ // Remove coupons if they exist.
+ let coupons = await page.locator( '.woocommerce-remove-coupon' ).all();
+
+ while ( coupons.length ) {
+ await coupons[ 0 ].click();
+ await isUIUnblocked( page );
+
+ coupons = await page.locator( '.woocommerce-remove-coupon' ).all();
+ }
+
+ await expect( page.locator( '.cart-empty.woocommerce-info' ) ).toHaveText(
+ 'Your cart is currently empty.'
+ );
+};
diff --git a/tests/e2e/config/jest.setup.js b/tests/e2e/config/jest.setup.js
index 9caf9592668..791c22a46ba 100644
--- a/tests/e2e/config/jest.setup.js
+++ b/tests/e2e/config/jest.setup.js
@@ -36,7 +36,8 @@ const ERROR_MESSAGES_TO_IGNORE = [
'Failed to load resource: the server responded with a status of 400 (Bad Request)',
'No Amplitude API key provided',
'is registered with an invalid category',
- '"Heading" is not a supported class',
+ 'is not a supported class', // Silence stripe.js warnings regarding styles.
+ 'is not a supported property for', // Silence stripe.js warnings regarding styles.
];
ERROR_MESSAGES_TO_IGNORE.forEach( ( errorMessage ) => {
diff --git a/tests/e2e/env/down.sh b/tests/e2e/env/down.sh
index e70ba901cda..6dc112bef7f 100755
--- a/tests/e2e/env/down.sh
+++ b/tests/e2e/env/down.sh
@@ -15,3 +15,8 @@ if [[ "$E2E_USE_LOCAL_SERVER" != false ]]; then
step "Stopping server containers"
docker compose -f $E2E_ROOT/deps/wcp-server/docker-compose.yml down
fi
+
+# Remove auth credentials from the Playwright config.
+# This must be kept when we fully migrate.
+step "Removing Playwright auth credentials"
+rm -rf "$E2E_ROOT/../e2e-pw/.auth"
diff --git a/tests/e2e/env/up.sh b/tests/e2e/env/up.sh
index 671e0343553..1a5998b9047 100755
--- a/tests/e2e/env/up.sh
+++ b/tests/e2e/env/up.sh
@@ -9,9 +9,9 @@ if [[ -f "$E2E_ROOT/config/local.env" ]]; then
fi
step "Starting client containers"
-docker compose -f "$E2E_ROOT/env/docker-compose.yml" start
+docker compose -f "$E2E_ROOT/env/docker-compose.yml" up -d
if [[ "$E2E_USE_LOCAL_SERVER" != false ]]; then
step "Starting server containers"
- docker compose -f "$E2E_ROOT/deps/wcp-server/docker-compose.yml" start
+ docker compose -f "$E2E_ROOT/deps/wcp-server/docker-compose.yml" up -d
fi
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js
deleted file mode 100644
index 51a0d36b930..00000000000
--- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-on-boarding.spec.js
+++ /dev/null
@@ -1,362 +0,0 @@
-/**
- * External dependencies
- */
-const { merchant, WP_ADMIN_DASHBOARD } = require( '@woocommerce/e2e-utils' );
-/**
- * Internal dependencies
- */
-import {
- merchantWCP,
- setCheckboxState,
- takeScreenshot,
- uiLoaded,
-} from '../../../utils';
-
-// Shared selector constants.
-const THEME_SELECTOR = ( themeSlug ) => `.theme[data-slug="${ themeSlug }"]`;
-const ACTIVATE_THEME_BUTTON_SELECTOR = ( themeSlug ) =>
- `${ THEME_SELECTOR( themeSlug ) } .button.activate`;
-const MULTI_CURRENCY_TOGGLE_SELECTOR = "[data-testid='multi-currency-toggle']";
-const RECOMMENDED_CURRENCY_LIST_SELECTOR =
- 'li[data-testid="recommended-currency"]';
-const CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR =
- 'li.enabled-currency-checkbox:not([data-testid="recommended-currency"])';
-const ENABLED_CURRENCY_LIST_SELECTOR = 'li.enabled-currency-checkbox';
-const GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR =
- 'input[data-testid="enable_auto_currency"]';
-const PREVIEW_STORE_BTN_SELECTOR = '.multi-currency-setup-preview-button';
-const PREVIEW_STORE_IFRAME_SELECTOR =
- '.multi-currency-store-settings-preview-iframe';
-const SUBMIT_STEP_BTN_SELECTOR =
- '.add-currencies-task.is-active .task-collapsible-body.is-active > button.is-primary';
-const STOREFRONT_SWITCH_CHECKBOX_SELECTOR =
- 'input[data-testid="enable_storefront_switcher"]';
-
-let wasMulticurrencyEnabled;
-
-const goToThemesPage = async () => {
- await page.goto( `${ WP_ADMIN_DASHBOARD }themes.php`, {
- waitUntil: 'networkidle0',
- } );
-};
-
-const activateTheme = async ( themeSlug ) => {
- await goToThemesPage();
-
- // Check if the theme is already active.
- const isActive = await page.evaluate( ( selector ) => {
- const themeElement = document.querySelector( selector );
- return themeElement && themeElement.classList.contains( 'active' );
- }, THEME_SELECTOR( themeSlug ) );
-
- // Activate the theme if it's not already active.
- if ( ! isActive ) {
- await page.click( ACTIVATE_THEME_BUTTON_SELECTOR( themeSlug ) );
- await page.waitForNavigation( { waitUntil: 'networkidle0' } );
- }
-};
-
-const goToOnboardingPage = async () => {
- await page.goto(
- `${ WP_ADMIN_DASHBOARD }admin.php?page=wc-admin&path=%2Fpayments%2Fmulti-currency-setup`,
- {
- waitUntil: 'networkidle0',
- }
- );
- await uiLoaded();
-};
-
-const goToNextOnboardingStep = async () => {
- await page.click( SUBMIT_STEP_BTN_SELECTOR );
-};
-
-describe( 'Merchant On-boarding', () => {
- let activeThemeSlug;
-
- beforeAll( async () => {
- await merchant.login();
- // Get initial multi-currency feature status.
- await merchantWCP.openWCPSettings();
- await page.waitForSelector( MULTI_CURRENCY_TOGGLE_SELECTOR );
- wasMulticurrencyEnabled = await page.evaluate( ( selector ) => {
- const checkbox = document.querySelector( selector );
- return checkbox ? checkbox.checked : false;
- }, MULTI_CURRENCY_TOGGLE_SELECTOR );
- await merchantWCP.activateMulticurrency();
-
- await goToThemesPage();
-
- // Get current theme slug.
- activeThemeSlug = await page.evaluate( () => {
- const theme = document.querySelector( '.theme.active' );
- return theme ? theme.getAttribute( 'data-slug' ) : '';
- } );
- } );
-
- afterAll( async () => {
- // Restore original theme.
- await activateTheme( activeThemeSlug );
-
- // Disable multi-currency if it was not initially enabled.
- if ( ! wasMulticurrencyEnabled ) {
- await merchant.login();
- await merchantWCP.deactivateMulticurrency();
- }
- await merchant.logout();
- } );
-
- describe( 'Currency Selection and Management', () => {
- beforeAll( async () => {
- await merchantWCP.disableAllEnabledCurrencies();
- } );
-
- beforeEach( async () => {
- await goToOnboardingPage();
- } );
-
- it( 'Should disable the submit button when no currencies are selected', async () => {
- await takeScreenshot( 'merchant-on-boarding-multicurrency-screen' );
- await setCheckboxState(
- `${ ENABLED_CURRENCY_LIST_SELECTOR } .components-checkbox-control__input`,
- false
- );
-
- await page.waitForTimeout( 1000 );
-
- const button = await page.$( SUBMIT_STEP_BTN_SELECTOR );
- expect( button ).not.toBeNull();
-
- const isDisabled = await page.evaluate(
- ( btn ) => btn.disabled,
- button
- );
-
- expect( isDisabled ).toBeTruthy();
- } );
-
- it( 'Should allow multiple currencies to be selectable', async () => {
- await page.waitForSelector(
- CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR,
- {
- timeout: 3000,
- }
- );
-
- // Ensure the checkbox within the list item is present and not disabled.
- const checkbox = await page.$(
- `${ CURRENCY_NOT_IN_RECOMMENDED_LIST_SELECTOR } input[type="checkbox"]`
- );
- expect( checkbox ).not.toBeNull();
- const isDisabled = await (
- await checkbox.getProperty( 'disabled' )
- ).jsonValue();
- expect( isDisabled ).toBe( false );
-
- // Click the checkbox to select the currency and verify it's checked.
- await checkbox.click();
-
- const isChecked = await (
- await checkbox.getProperty( 'checked' )
- ).jsonValue();
- expect( isChecked ).toBe( true );
- } );
-
- it( 'Should exclude already enabled currencies from the currency screen', async () => {
- await merchantWCP.addCurrency( 'GBP' );
-
- await goToOnboardingPage();
-
- await page.waitForSelector( ENABLED_CURRENCY_LIST_SELECTOR, {
- timeout: 3000,
- } );
-
- // Get the list of currencies as text
- const currencies = await page.$$eval(
- ENABLED_CURRENCY_LIST_SELECTOR,
- ( items ) => items.map( ( item ) => item.textContent.trim() )
- );
-
- expect( currencies ).not.toContain( 'GBP' );
-
- await merchantWCP.removeCurrency( 'GBP' );
- } );
-
- it( 'Should display some suggested currencies at the beginning of the list', async () => {
- await page.waitForSelector( RECOMMENDED_CURRENCY_LIST_SELECTOR, {
- timeout: 3000,
- } );
-
- // Get the list of recommended currencies
- const recommendedCurrencies = await page.$$eval(
- RECOMMENDED_CURRENCY_LIST_SELECTOR,
- ( items ) =>
- items.map( ( item ) => ( {
- code: item
- .querySelector( 'input' )
- .getAttribute( 'code' ),
- name: item
- .querySelector(
- 'span.enabled-currency-checkbox__code'
- )
- .textContent.trim(),
- } ) )
- );
-
- expect( recommendedCurrencies.length ).toBeGreaterThan( 0 );
- } );
-
- it( 'Should ensure selected currencies are enabled after submitting the form', async () => {
- const testCurrencies = [ 'GBP', 'EUR', 'CAD', 'AUD' ];
- const addCurrenciesContentSelector =
- '.add-currencies-task__content';
- const currencyCheckboxSelector = `${ addCurrenciesContentSelector } li input[type="checkbox"]`;
-
- await page.waitForSelector( addCurrenciesContentSelector, {
- timeout: 3000,
- } );
-
- // Select the currencies
- for ( const currency of testCurrencies ) {
- await setCheckboxState(
- `${ currencyCheckboxSelector }[code="${ currency }"]`,
- true
- );
- }
-
- // Submit the form.
- await goToNextOnboardingStep();
-
- await merchantWCP.openMultiCurrency();
-
- // Ensure the currencies are enabled.
- for ( const currency of testCurrencies ) {
- const selector = `li.enabled-currency.${ currency.toLowerCase() }`;
- await page.waitForSelector( selector, { timeout: 10000 } );
- const element = await page.$( selector );
-
- expect( element ).not.toBeNull();
- }
- } );
- } );
-
- describe( 'Geolocation Features', () => {
- beforeAll( async () => {
- await merchantWCP.disableAllEnabledCurrencies();
- } );
-
- beforeEach( async () => {
- await goToOnboardingPage();
- } );
-
- it( 'Should offer currency switch by geolocation', async () => {
- await goToNextOnboardingStep();
-
- const checkbox = await page.$(
- GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR
- );
-
- // Check if exists and not disabled.
- expect( checkbox ).not.toBeNull();
- const isDisabled = await (
- await checkbox.getProperty( 'disabled' )
- ).jsonValue();
- expect( isDisabled ).toBe( false );
-
- // Click the checkbox to select it.
- await page.click( GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR );
-
- // Check if the checkbox is selected.
- const isChecked = await (
- await checkbox.getProperty( 'checked' )
- ).jsonValue();
- expect( isChecked ).toBe( true );
- } );
-
- it( 'Should preview currency switch by geolocation correctly with USD and GBP', async () => {
- page.setViewport( { width: 1280, height: 1280 } ); // To take a better screenshot of the iframe preview.
-
- await goToNextOnboardingStep();
-
- await takeScreenshot(
- 'merchant-on-boarding-multicurrency-screen-2'
- );
-
- // Enable feature.
- await setCheckboxState(
- GEO_CURRENCY_SWITCH_CHECKBOX_SELECTOR,
- true
- );
-
- // Click preview button.
- await page.click( PREVIEW_STORE_BTN_SELECTOR );
-
- await page.waitForSelector( PREVIEW_STORE_IFRAME_SELECTOR, {
- timeout: 3000,
- } );
-
- const iframeElement = await page.$( PREVIEW_STORE_IFRAME_SELECTOR );
- const iframe = await iframeElement.contentFrame();
-
- await iframe.waitForSelector( '.woocommerce-store-notice', {
- timeout: 3000,
- } );
-
- await takeScreenshot(
- 'merchant-on-boarding-multicurrency-geolocation-switcher-preview'
- );
-
- const noticeText = await iframe.$eval(
- '.woocommerce-store-notice',
- ( element ) => element.innerText
- );
- expect( noticeText ).toContain(
- // eslint-disable-next-line max-len
- "We noticed you're visiting from United Kingdom (UK). We've updated our prices to Pound sterling for your shopping convenience."
- );
- } );
- } );
-
- describe( 'Currency Switcher Widget', () => {
- it( 'Should offer the currency switcher widget while Storefront theme is active', async () => {
- await activateTheme( 'storefront' );
-
- await goToOnboardingPage();
- await goToNextOnboardingStep();
-
- const checkbox = await page.$(
- STOREFRONT_SWITCH_CHECKBOX_SELECTOR
- );
-
- // Check if exists and not disabled.
- expect( checkbox ).not.toBeNull();
- const isDisabled = await (
- await checkbox.getProperty( 'disabled' )
- ).jsonValue();
- expect( isDisabled ).toBe( false );
-
- // Click the checkbox to select it.
- await page.click( STOREFRONT_SWITCH_CHECKBOX_SELECTOR );
-
- // Check if the checkbox is selected.
- const isChecked = await (
- await checkbox.getProperty( 'checked' )
- ).jsonValue();
- expect( isChecked ).toBe( true );
- } );
-
- it( 'Should not offer the currency switcher widget when an unsupported theme is active', async () => {
- await activateTheme( 'twentytwentyfour' );
-
- await goToOnboardingPage();
- await goToNextOnboardingStep();
-
- const checkbox = await page.$(
- STOREFRONT_SWITCH_CHECKBOX_SELECTOR
- );
-
- expect( checkbox ).toBeNull();
-
- await activateTheme( 'storefront' );
- } );
- } );
-} );
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js
deleted file mode 100644
index 50a4dd9683d..00000000000
--- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency-setup.spec.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * External dependencies
- */
-const { merchant } = require( '@woocommerce/e2e-utils' );
-/**
- * Internal dependencies
- */
-import {
- getProductPriceFromProductPage,
- merchantWCP,
- shopperWCP,
-} from '../../../utils';
-
-let wasMulticurrencyEnabled;
-
-describe( 'Merchant Multi-Currency Settings', () => {
- beforeAll( async () => {
- await merchant.login();
- // Get initial multi-currency feature status.
- await merchantWCP.openWCPSettings();
- await page.waitForSelector( "[data-testid='multi-currency-toggle']" );
- wasMulticurrencyEnabled = await page.evaluate( () => {
- const checkbox = document.querySelector(
- "[data-testid='multi-currency-toggle']"
- );
- return checkbox ? checkbox.checked : false;
- } );
- } );
-
- afterAll( async () => {
- // Disable multi-currency if it was not initially enabled.
- if ( ! wasMulticurrencyEnabled ) {
- await merchant.login();
- await merchantWCP.deactivateMulticurrency();
- }
- await merchant.logout();
- } );
-
- it( 'can enable multi-currency feature', async () => {
- // Assertions are in the merchantWCP.wcpSettingsSaveChanges() flow.
- await merchantWCP.activateMulticurrency();
- } );
-
- it( 'can disable multi-currency feature', async () => {
- // Assertions are in the merchantWCP.wcpSettingsSaveChanges() flow.
- await merchantWCP.deactivateMulticurrency();
- } );
-
- describe( 'Currency Management', () => {
- const testCurrency = 'CHF';
-
- beforeAll( async () => {
- await merchantWCP.activateMulticurrency();
- } );
-
- it( 'can add a new currency', async () => {
- await merchantWCP.addCurrency( testCurrency );
- } );
-
- it( 'can remove a currency', async () => {
- await merchantWCP.removeCurrency( testCurrency );
- } );
- } );
-
- describe( 'Currency Settings', () => {
- const testData = {
- currencyCode: 'CHF',
- rate: '1.25',
- charmPricing: '-0.01',
- rounding: '0.5',
- currencyPrecision: 2,
- };
-
- let beanieRegularPrice;
-
- beforeAll( async () => {
- await merchantWCP.activateMulticurrency();
- await merchantWCP.disableAllEnabledCurrencies();
-
- await shopperWCP.goToShopWithCurrency( 'USD' );
- await shopperWCP.goToProductPageBySlug( 'beanie' );
- beanieRegularPrice = await getProductPriceFromProductPage();
- } );
-
- beforeEach( async () => {
- await merchantWCP.openMultiCurrency();
- await merchantWCP.addCurrency( testData.currencyCode );
- } );
-
- afterEach( async () => {
- await merchantWCP.openMultiCurrency();
- await merchantWCP.removeCurrency( testData.currencyCode );
- } );
-
- it( 'can change the currency rate manually', async () => {
- await merchantWCP.setCurrencyRate(
- testData.currencyCode,
- testData.rate
- );
- await merchantWCP.setCurrencyPriceRounding(
- testData.currencyCode,
- '0'
- );
-
- await shopperWCP.goToShopWithCurrency( testData.currencyCode );
- await shopperWCP.goToProductPageBySlug( 'beanie' );
- const beaniePriceOnCurrency = await getProductPriceFromProductPage();
-
- expect(
- parseFloat( beaniePriceOnCurrency ).toFixed(
- testData.currencyPrecision
- )
- ).toBe(
- ( parseFloat( beanieRegularPrice ) * testData.rate ).toFixed(
- testData.currencyPrecision
- )
- );
- } );
-
- it( 'can change the charm price manually', async () => {
- await merchantWCP.setCurrencyRate( testData.currencyCode, '1.00' );
- await merchantWCP.setCurrencyPriceRounding(
- testData.currencyCode,
- '0'
- );
- await merchantWCP.setCurrencyCharmPricing(
- testData.currencyCode,
- testData.charmPricing
- );
-
- await shopperWCP.goToShopWithCurrency( testData.currencyCode );
- await shopperWCP.goToProductPageBySlug( 'beanie' );
- const beaniePriceOnCurrency = await getProductPriceFromProductPage();
-
- expect(
- parseFloat( beaniePriceOnCurrency ).toFixed(
- testData.currencyPrecision
- )
- ).toBe(
- (
- parseFloat( beanieRegularPrice ) +
- parseFloat( testData.charmPricing )
- ).toFixed( testData.currencyPrecision )
- );
- } );
-
- it( 'can change the rounding precision manually', async () => {
- const rateForTest = 1.2;
-
- await merchantWCP.setCurrencyRate(
- testData.currencyCode,
- rateForTest.toString()
- );
- await merchantWCP.setCurrencyPriceRounding(
- testData.currencyCode,
- testData.rounding
- );
-
- await shopperWCP.goToShopWithCurrency( testData.currencyCode );
- await shopperWCP.goToProductPageBySlug( 'beanie' );
- const beaniePriceOnCurrency = await getProductPriceFromProductPage();
-
- expect(
- parseFloat( beaniePriceOnCurrency ).toFixed(
- testData.currencyPrecision
- )
- ).toBe(
- (
- Math.ceil(
- parseFloat( beanieRegularPrice ) *
- rateForTest *
- ( 1 / testData.rounding )
- ) * testData.rounding
- ).toFixed( testData.currencyPrecision )
- );
- } );
- } );
-
- describe( 'Currency decimal points', () => {
- const currencyDecimalMap = {
- JPY: 0,
- GBP: 2,
- };
-
- beforeAll( async () => {
- await merchantWCP.activateMulticurrency();
-
- for ( const currency of Object.keys( currencyDecimalMap ) ) {
- await merchantWCP.addCurrency( currency );
- }
- } );
-
- it.each( Object.keys( currencyDecimalMap ) )(
- 'sees the correct decimal points for %s',
- async ( currency ) => {
- await shopperWCP.goToShopWithCurrency( currency );
- await shopperWCP.goToProductPageBySlug( 'beanie' );
- const priceOnCurrency = await getProductPriceFromProductPage();
-
- const decimalPart = priceOnCurrency.split( '.' )[ 1 ] || '';
- expect( decimalPart.length ).toBe(
- currencyDecimalMap[ currency ]
- );
- }
- );
- } );
-} );
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js
deleted file mode 100644
index 96064318e2c..00000000000
--- a/tests/e2e/specs/wcpay/merchant/merchant-admin-multi-currency.spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * External dependencies
- */
-const { merchant, WP_ADMIN_DASHBOARD } = require( '@woocommerce/e2e-utils' );
-
-/**
- * Internal dependencies
- */
-import { merchantWCP, takeScreenshot } from '../../../utils';
-
-describe( 'Admin Multi-Currency', () => {
- let wasMulticurrencyEnabled;
-
- beforeAll( async () => {
- await merchant.login();
- wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency();
- } );
-
- afterAll( async () => {
- if ( ! wasMulticurrencyEnabled ) {
- await merchantWCP.deactivateMulticurrency();
- }
- await merchant.logout();
- } );
-
- it( 'page should load without any errors', async () => {
- await merchantWCP.openMultiCurrency();
- await expect( page ).toMatchElement( 'h2', {
- text: 'Enabled currencies',
- } );
- await takeScreenshot( 'merchant-admin-multi-currency' );
- } );
-
- it( 'should be possible to add the currency switcher to the sidebar', async () => {
- await merchantWCP.addMulticurrencyWidget();
- } );
-
- it( 'should be possible to add the currency switcher to a post/page', async () => {
- await page.goto( `${ WP_ADMIN_DASHBOARD }post-new.php`, {
- waitUntil: 'load',
- } );
-
- const closeWelcomeModal = await page.$( 'button[aria-label="Close"]' );
- if ( closeWelcomeModal ) {
- await closeWelcomeModal.click();
- }
-
- await page.click( 'button[aria-label="Add block"]' );
-
- const searchInput = await page.waitForSelector(
- 'input[placeholder="Search"]'
- );
- searchInput.type( 'switcher', { delay: 20 } );
-
- await page.waitForSelector( 'button[role="option"]' );
- await expect( page ).toMatchElement( 'button[role="option"]', {
- text: 'Currency Switcher Block',
- } );
- await page.waitForTimeout( 1000 );
- } );
-} );
diff --git a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js
index 6a2224d2d58..5ff98fe002f 100644
--- a/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js
+++ b/tests/e2e/specs/wcpay/merchant/merchant-disputes-save-draft-challenge.spec.js
@@ -11,9 +11,10 @@ const { merchant, shopper, evalAndClick } = require( '@woocommerce/e2e-utils' );
import { fillCardDetails, setupProductCheckout } from '../../../utils/payments';
import { uiLoaded } from '../../../utils';
-let orderId;
-
describe( 'Disputes > Merchant can save and resume draft dispute challenge', () => {
+ let orderId;
+ let paymentDetailsLink;
+
beforeAll( async () => {
await page.goto( config.get( 'url' ), { waitUntil: 'networkidle0' } );
@@ -36,7 +37,7 @@ describe( 'Disputes > Merchant can save and resume draft dispute challenge', ()
await merchant.goToOrder( orderId );
// Get the payment details link from the order page.
- const paymentDetailsLink = await page.$eval(
+ paymentDetailsLink = await page.$eval(
'p.order_number > a',
( anchor ) => anchor.getAttribute( 'href' )
);
@@ -107,11 +108,24 @@ describe( 'Disputes > Merchant can save and resume draft dispute challenge', ()
}
);
- // Reload the page
- await page.reload();
-
+ // The merchant will be redirected to the dispute list page here, wait for it to load.
await uiLoaded();
+ // Open the payment details page again and wait for it to load.
+ await Promise.all( [
+ page.goto( paymentDetailsLink, {
+ waitUntil: 'networkidle0',
+ } ),
+ uiLoaded(),
+ ] );
+
+ // Click the challenge dispute button.
+ await evalAndClick( '[data-testid="challenge-dispute-button"]' );
+ await Promise.all( [
+ page.waitForNavigation( { waitUntil: 'networkidle0' } ),
+ uiLoaded(),
+ ] );
+
// Verify the previously selected Product type was saved
await expect( page ).toMatchElement(
'[data-testid="dispute-challenge-product-type-selector"]',
diff --git a/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js
deleted file mode 100644
index 98e275122f2..00000000000
--- a/tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * External dependencies
- */
-import config from 'config';
-const { shopper, merchant } = require( '@woocommerce/e2e-utils' );
-/**
- * Internal dependencies
- */
-import { fillCardDetails, setupProductCheckout } from '../../../utils/payments';
-import { merchantWCP, shopperWCP } from '../../../utils';
-
-const ORDER_RECEIVED_ORDER_TOTAL_SELECTOR =
- '.woocommerce-order-overview__total';
-const ORDER_HISTORY_ORDER_ROW_SELECTOR = '.woocommerce-orders-table__row';
-const ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR =
- '.woocommerce-orders-table__cell-order-number';
-const ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR =
- '.woocommerce-orders-table__cell-order-total';
-const ORDER_DETAILS_ORDER_TOTAL_SELECTOR =
- '.woocommerce-table--order-details tfoot tr:last-child td';
-
-const placeOrderWithCurrency = async ( currency ) => {
- try {
- await shopperWCP.goToShopWithCurrency( currency );
- await setupProductCheckout(
- config.get( 'addresses.customer.billing' ),
- [ [ config.get( 'products.simple.name' ), 1 ] ],
- currency
- );
- const card = config.get( 'cards.basic' );
- await fillCardDetails( page, card );
- await shopper.placeOrder();
- await expect( page ).toMatchTextContent( 'Order received' );
-
- const url = await page.url();
- return url.match( /\/order-received\/(\d+)\// )[ 1 ];
- } catch ( error ) {
- // eslint-disable-next-line no-console
- console.error(
- `Error placing order with currency ${ currency }: `,
- error
- );
- throw error;
- }
-};
-
-const getOrderTotalTextForOrder = async ( orderId ) => {
- return await page.$$eval(
- ORDER_HISTORY_ORDER_ROW_SELECTOR,
- ( rows, currentOrderId, orderNumberColSelector, totalColSelector ) => {
- const orderSelector = `${ orderNumberColSelector } a[href*="view-order/${ currentOrderId }/"]`;
- return rows
- .filter( ( row ) => row.querySelector( orderSelector ) )
- .map( ( row ) =>
- row.querySelector( totalColSelector )?.textContent.trim()
- )
- .find( ( text ) => text !== null );
- },
- orderId,
- ORDER_HISTORY_ORDER_ROW_NUMBER_COL_SELECTOR,
- ORDER_HISTORY_ORDER_ROW_TOTAL_COL_SELECTOR
- );
-};
-
-describe( 'Shopper Multi-Currency checkout', () => {
- let wasMulticurrencyEnabled;
- const currenciesOrders = {
- USD: null,
- EUR: null,
- };
- beforeAll( async () => {
- // Enable multi-currency
- await merchant.login();
-
- wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency();
- for ( const currency in currenciesOrders ) {
- await merchantWCP.addCurrency( currency );
- }
-
- await merchant.logout();
-
- await shopper.login();
- } );
-
- afterAll( async () => {
- await shopperWCP.emptyCart();
- await shopper.logout();
-
- // Disable multi-currency if it was not initially enabled.
- if ( ! wasMulticurrencyEnabled ) {
- await merchant.login();
- await merchantWCP.deactivateMulticurrency();
- await merchant.logout();
- }
- } );
-
- describe.each( Object.keys( currenciesOrders ) )(
- 'Checkout process with %s currency',
- ( testCurrency ) => {
- it( 'should allow checkout', async () => {
- currenciesOrders[ testCurrency ] = await placeOrderWithCurrency(
- testCurrency
- );
- } );
-
- it( 'should display the correct currency on the order received page', async () => {
- expect(
- await page.$eval(
- ORDER_RECEIVED_ORDER_TOTAL_SELECTOR,
- ( el ) => el.textContent
- )
- ).toMatch( new RegExp( testCurrency ) );
- } );
- }
- );
-
- describe.each( Object.keys( currenciesOrders ) )(
- 'My account order details for %s order',
- ( testCurrency ) => {
- beforeEach( async () => {
- await shopperWCP.goToOrder( currenciesOrders[ testCurrency ] );
- } );
-
- it( 'should show the correct currency in the order page for the order', async () => {
- expect(
- await page.$eval(
- ORDER_DETAILS_ORDER_TOTAL_SELECTOR,
- ( el ) => el.textContent
- )
- ).toMatch( new RegExp( testCurrency ) );
- } );
- }
- );
-
- describe( 'My account order history', () => {
- it( 'should show the correct currency in the order history table', async () => {
- await shopperWCP.goToOrders();
-
- for ( const currency in currenciesOrders ) {
- const orderTotalText = await getOrderTotalTextForOrder(
- currenciesOrders[ currency ]
- );
- expect( orderTotalText ).toMatch( new RegExp( currency ) );
- }
- } );
- } );
-} );
diff --git a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
deleted file mode 100644
index 7b10010a9cc..00000000000
--- a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
+++ /dev/null
@@ -1,167 +0,0 @@
-/**
- * External dependencies
- */
-const { shopper, merchant } = require( '@woocommerce/e2e-utils' );
-import config from 'config';
-import { uiUnblocked } from '@woocommerce/e2e-utils/build/page-utils';
-/**
- * Internal dependencies
- */
-import { merchantWCP, shopperWCP } from '../../../utils';
-import { setupProductCheckout } from '../../../utils/payments';
-
-const UPE_METHOD_CHECKBOXES = [
- "//label[contains(text(), 'Klarna')]/preceding-sibling::span/input[@type='checkbox']",
-];
-
-describe( 'Klarna checkout', () => {
- let wasMulticurrencyEnabled;
- beforeAll( async () => {
- await merchant.login();
- wasMulticurrencyEnabled = await merchantWCP.activateMulticurrency();
- await merchantWCP.deactivateMulticurrency();
- await merchantWCP.enablePaymentMethod( UPE_METHOD_CHECKBOXES );
- await merchant.logout();
- await shopper.login();
- } );
-
- afterAll( async () => {
- await shopperWCP.emptyCart();
- await shopperWCP.logout();
- await merchant.login();
- if ( wasMulticurrencyEnabled ) {
- await merchantWCP.activateMulticurrency();
- }
- await merchantWCP.disablePaymentMethod( UPE_METHOD_CHECKBOXES );
- await merchant.logout();
- } );
-
- it( 'should show the product messaging on the product page', async () => {
- await shopperWCP.goToProductPageBySlug( 'belt' );
-
- // waiting for the "product messaging" component to be rendered, so we can click on it.
- const paymentMethodMessageFrameHandle = await page.waitForSelector(
- '#payment-method-message iframe'
- );
- const paymentMethodMessageIframe = await paymentMethodMessageFrameHandle.contentFrame();
-
- // Click on Klarna link to open the modal.
- await paymentMethodMessageIframe.evaluate( ( selector ) => {
- const element = document.querySelector( selector );
- if ( element ) {
- element.click();
- }
- }, 'a[aria-label="Open Learn More Modal"]' );
-
- // Wait for the iframe to be added by Stripe JS after clicking on the element.
- await page.waitForTimeout( 1000 );
-
- const paymentMethodMessageModalIframeHandle = await page.waitForSelector(
- 'iframe[src*="js.stripe.com/v3/elements-inner-payment-method-messaging-modal"]'
- );
- const paymentMethodMessageModalIframe = await paymentMethodMessageModalIframeHandle.contentFrame();
-
- await expect( paymentMethodMessageModalIframe ).toMatchElement(
- '[data-testid="ModalHeader"] > p',
- {
- text: 'Buy Now. Pay Later.',
- }
- );
-
- await expect( paymentMethodMessageModalIframe ).toMatchElement(
- '[data-testid="ModalDescription"] > p',
- {
- text:
- 'Select Klarna as your payment method at checkout to pay in installments.',
- }
- );
- } );
-
- it( `should successfully place an order with Klarna`, async () => {
- await setupProductCheckout(
- {
- ...config.get( 'addresses.customer.billing' ),
- // these are Klarna-specific values:
- // https://docs.klarna.com/resources/test-environment/sample-customer-data/#united-states-of-america
- email: 'customer@email.us',
- phone: '+13106683312',
- firstname: 'Test',
- lastname: 'Person-us',
- },
- [ [ 'Beanie', 3 ] ]
- );
-
- await uiUnblocked();
-
- await page.evaluate( async () => {
- const paymentMethodLabel = document.querySelector(
- 'label[for="payment_method_woocommerce_payments_klarna"]'
- );
- if ( paymentMethodLabel ) {
- paymentMethodLabel.click();
- }
- } );
-
- await shopper.placeOrder();
-
- await page.waitForSelector( '#phone' );
-
- await page.waitForTimeout( 2000 );
-
- await page
- .waitForSelector( '#onContinue' )
- .then( ( button ) => button.click() );
-
- // This is where the OTP code is entered.
- await page.waitForSelector( '#phoneOtp' );
-
- await page.waitForTimeout( 2000 ); // Wait for animations
-
- await expect( page ).toFill( 'input#otp_field', '123456' );
-
- await page.waitForSelector( '[role="heading"]', {
- visible: true,
- text: /(Confirm and pay)|(Choose how to pay)/i,
- } );
-
- let readyToBuy;
- try {
- await page.waitForSelector( 'button#buy_button', {
- visible: true,
- timeout: 10000,
- } );
- readyToBuy = true;
- } catch ( err ) {
- readyToBuy = false;
- }
-
- if ( ! readyToBuy ) {
- // Select Payment Plan - 4 weeks & click continue.
- await page
- .waitForSelector(
- '[id="payinparts_kp.0-ui"] input[type="radio"]'
- )
- .then( ( radio ) => radio.click() );
-
- await page.waitForTimeout( 2000 );
-
- await expect( page ).toClick( 'button', {
- text: 'Continue',
- } );
- }
-
- // Confirm payment.
- await page.waitForSelector( 'button#buy_button' );
- await page.waitForTimeout( 2000 ); // We need to wait a bit for the button to become clickable.
- await expect( page ).toClick( 'button#buy_button' );
-
- // Wait for the order confirmation page to load.
- await page.waitForNavigation( {
- waitUntil: 'networkidle0',
- } );
-
- await page.waitForSelector( 'h1.entry-title' );
-
- await expect( page ).toMatchTextContent( 'Order received' );
- } );
-} );
diff --git a/tests/e2e/utils/payments.js b/tests/e2e/utils/payments.js
index 25c69b47a23..9754bc85418 100644
--- a/tests/e2e/utils/payments.js
+++ b/tests/e2e/utils/payments.js
@@ -277,9 +277,7 @@ export async function setupCheckout( billingDetails ) {
// field changes. Need to wait to make sure that all key presses were processed by that mechanism.
await page.waitForTimeout( 1000 );
await uiUnblocked();
- await expect( page ).toClick(
- '.wc_payment_method.payment_method_woocommerce_payments'
- );
+ await page.click( 'label[for="payment_method_woocommerce_payments"]' );
}
// Copy of the fillBillingDetails function from woocommerce/e2e-utils/src/flows/shopper.js
diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php
index 2000b18e0e2..25612ab0f9a 100644
--- a/tests/unit/admin/test-class-wc-payments-admin.php
+++ b/tests/unit/admin/test-class-wc-payments-admin.php
@@ -201,13 +201,17 @@ private function mock_current_user_is_admin() {
/**
* @dataProvider data_maybe_redirect_from_payments_admin_child_pages
*/
- public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $is_stripe_connected, $get_params ) {
+ public function test_maybe_redirect_from_payments_admin_child_pages( $expected_times_redirect_called, $has_working_jetpack_connection, $is_stripe_account_valid, $get_params ) {
$this->mock_current_user_is_admin();
$_GET = $get_params;
$this->mock_account
- ->method( 'is_stripe_connected' )
- ->willReturn( $is_stripe_connected );
+ ->method( 'has_working_jetpack_connection' )
+ ->willReturn( $has_working_jetpack_connection );
+
+ $this->mock_account
+ ->method( 'is_stripe_account_valid' )
+ ->willReturn( $is_stripe_account_valid );
$this->mock_account
->expects( $this->exactly( $expected_times_redirect_called ) )
@@ -224,11 +228,13 @@ public function data_maybe_redirect_from_payments_admin_child_pages() {
'no_get_params' => [
0,
false,
+ false,
[],
],
'empty_page_param' => [
0,
false,
+ false,
[
'path' => '/payments/overview',
],
@@ -236,6 +242,7 @@ public function data_maybe_redirect_from_payments_admin_child_pages() {
'incorrect_page_param' => [
0,
false,
+ false,
[
'page' => 'wc-settings',
'path' => '/payments/overview',
@@ -244,6 +251,7 @@ public function data_maybe_redirect_from_payments_admin_child_pages() {
'empty_path_param' => [
0,
false,
+ false,
[
'page' => 'wc-admin',
],
@@ -251,25 +259,37 @@ public function data_maybe_redirect_from_payments_admin_child_pages() {
'incorrect_path_param' => [
0,
false,
+ false,
[
'page' => 'wc-admin',
'path' => '/payments/does-not-exist',
],
],
- 'stripe_connected' => [
- 0,
+ 'working Jetpack connection - invalid Stripe account' => [
+ 1,
true,
+ false,
[
'page' => 'wc-admin',
- 'path' => '/payments/overview',
+ 'path' => '/payments/deposits',
],
],
- 'happy_path' => [
+ 'not working Jetpack connection - valid Stripe account' => [
1,
false,
+ true,
[
'page' => 'wc-admin',
- 'path' => '/payments/overview',
+ 'path' => '/payments/deposits',
+ ],
+ ],
+ 'working Jetpack connection - valid Stripe account' => [
+ 0,
+ true,
+ true,
+ [
+ 'page' => 'wc-admin',
+ 'path' => '/payments/transactions',
],
],
];
diff --git a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
index e7bc456c46b..07a00a2b690 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
@@ -140,4 +140,87 @@ public function test_get_progressive_onboarding_not_eligible() {
$response->get_data()
);
}
+
+ public function test_get_embedded_kyc_session() {
+ $kyc_session = [
+ 'clientSecret' => 'accs_secret__XXX',
+ 'expiresAt' => time() + 120,
+ 'accountId' => 'acct_XXX',
+ 'isLive' => false,
+ 'accountCreated' => true,
+ 'publishableKey' => 'pk_test_XXX',
+ ];
+
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'create_embedded_kyc_session' )
+ ->willReturn(
+ $kyc_session
+ );
+
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'set_embedded_kyc_in_progress' );
+
+ $request = new WP_REST_Request( 'GET' );
+ $request->set_query_params(
+ [
+ 'progressive' => true,
+ 'create_live_account' => true,
+ ]
+ );
+
+ $response = $this->controller->get_embedded_kyc_session( $request );
+ $this->assertSame( 200, $response->status );
+ $this->assertSame(
+ array_merge(
+ $kyc_session,
+ [
+ 'locale' => 'en_US',
+ ]
+ ),
+ $response->get_data()
+ );
+ }
+
+ public function test_finalize_embedded_kyc() {
+ $response_data = [
+ 'success' => true,
+ 'account_id' => 'acct_1PvxJQQujq4nxoo6',
+ 'details_submitted' => true,
+ 'mode' => 'test',
+ 'promotion_id' => null,
+ ];
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'finalize_embedded_kyc' )
+ ->willReturn(
+ $response_data
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'source' => 'embedded',
+ 'from' => 'wcpay-connect',
+ ]
+ );
+
+ $response = $this->controller->finalize_embedded_kyc( $request );
+ $this->assertSame( 200, $response->status );
+ $this->assertSame(
+ array_merge(
+ $response_data,
+ [
+ 'params' => [
+ 'promo' => '',
+ 'from' => 'wcpay-connect',
+ 'source' => 'embedded',
+ 'wcpay-connection-success' => '1',
+ ],
+ ]
+ ),
+ $response->get_data()
+ );
+ }
}
diff --git a/tests/unit/test-class-wc-payments-account-capital.php b/tests/unit/test-class-wc-payments-account-capital.php
index 240d2bc2e51..cf48a9ceb26 100644
--- a/tests/unit/test-class-wc-payments-account-capital.php
+++ b/tests/unit/test-class-wc-payments-account-capital.php
@@ -50,11 +50,11 @@ class WC_Payments_Account_Capital_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -80,13 +80,13 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$this->wcpay_account->init_hooks();
}
@@ -105,7 +105,7 @@ public function tear_down() {
public function test_maybe_redirect_by_get_param_will_run() {
$wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethodsExcept( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$wcpay_account->init_hooks();
diff --git a/tests/unit/test-class-wc-payments-account-link.php b/tests/unit/test-class-wc-payments-account-link.php
index 4ab2c92a8af..e8711f10b88 100644
--- a/tests/unit/test-class-wc-payments-account-link.php
+++ b/tests/unit/test-class-wc-payments-account-link.php
@@ -48,11 +48,11 @@ class WC_Payments_Account_Server_Links_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -78,13 +78,13 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
// Mock WC_Payments_Account without redirect_to to prevent headers already sent error.
$this->wcpay_account = $this->getMockBuilder( WC_Payments_Account::class )
->setMethods( [ 'init_hooks' ] )
- ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service ] )
+ ->setConstructorArgs( [ $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service ] )
->getMock();
$this->wcpay_account->init_hooks();
diff --git a/tests/unit/test-class-wc-payments-account.php b/tests/unit/test-class-wc-payments-account.php
index 61215b93466..7b26455c9a2 100644
--- a/tests/unit/test-class-wc-payments-account.php
+++ b/tests/unit/test-class-wc-payments-account.php
@@ -51,11 +51,11 @@ class WC_Payments_Account_Test extends WCPAY_UnitTestCase {
private $mock_action_scheduler_service;
/**
- * Mock WC_Payments_Session_Service.
+ * Mock WC_Payments_Onboarding_Service.
*
- * @var WC_Payments_Session_Service|MockObject
+ * @var WC_Payments_Onboarding_Service|MockObject
*/
- private $mock_session_service;
+ private $mock_onboarding_service;
/**
* Mock WC_Payments_Redirect_Service.
@@ -82,10 +82,10 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_database_cache = $this->createMock( Database_Cache::class );
$this->mock_action_scheduler_service = $this->createMock( WC_Payments_Action_Scheduler_Service::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $this->mock_onboarding_service = $this->createMock( WC_Payments_Onboarding_Service::class );
$this->mock_redirect_service = $this->createMock( WC_Payments_Redirect_Service::class );
- $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $this->mock_redirect_service );
+ $this->wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $this->mock_redirect_service );
$this->wcpay_account->init_hooks();
}
@@ -95,6 +95,7 @@ public function tear_down() {
unset( $_GET );
unset( $_REQUEST );
parent::tear_down();
+ delete_option( '_wcpay_feature_embedded_kyc' );
}
public function test_filters_registered_properly() {
@@ -239,7 +240,9 @@ public function test_maybe_handle_onboarding_return_from_jetpack_connection_with
->with(
'Connection to WordPress.com failed. Please connect to WordPress.com to start using WooPayments.',
WC_Payments_Onboarding_Service::FROM_WPCOM_CONNECTION,
- [ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE ]
+ [
+ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE,
+ ]
);
// Act.
@@ -268,7 +271,7 @@ public function test_maybe_handle_onboarding_connect_from_known_from(
->disableOriginalConstructor()
->onlyMethods( [ 'redirect_to' ] )
->getMock();
- $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_session_service, $mock_redirect_service );
+ $wcpay_account = new WC_Payments_Account( $this->mock_api_client, $this->mock_database_cache, $this->mock_action_scheduler_service, $this->mock_onboarding_service, $mock_redirect_service );
$_GET['wcpay-connect'] = 'connect-from';
$_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
@@ -618,7 +621,12 @@ public function test_maybe_handle_onboarding_test_mode_to_live() {
$this->mock_redirect_service
->expects( $this->once() )
->method( 'redirect_to_onboarding_wizard' )
- ->with( WC_Payments_Onboarding_Service::FROM_TEST_TO_LIVE, [ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS ] );
+ ->with(
+ WC_Payments_Onboarding_Service::FROM_TEST_TO_LIVE,
+ [
+ 'source' => WC_Payments_Onboarding_Service::SOURCE_WCPAY_SETUP_LIVE_PAYMENTS,
+ ]
+ );
// Act.
$this->wcpay_account->maybe_handle_onboarding();
@@ -796,6 +804,71 @@ public function test_maybe_handle_onboarding_init_stripe_onboarding() {
$this->wcpay_account->maybe_handle_onboarding();
}
+ public function test_maybe_handle_onboarding_init_embedded_kyc() {
+ // Arrange.
+ // We need to be in the WP admin dashboard.
+ $this->set_is_admin( true );
+ // Test as an admin user.
+ wp_set_current_user( 1 );
+
+ $_GET['wcpay-connect'] = 'connect-from';
+ $_REQUEST['_wpnonce'] = wp_create_nonce( 'wcpay-connect' );
+ // Set the request as if the user is on some bogus page. It doesn't matter.
+ $_GET['page'] = 'wc-admin';
+ $_GET['path'] = '/payments/some-bogus-page';
+ // We need to come from the onboarding wizard to initialize an account!
+ $_GET['from'] = WC_Payments_Onboarding_Service::FROM_ONBOARDING_WIZARD;
+ $_GET['source'] = WC_Payments_Onboarding_Service::SOURCE_WCADMIN_INCENTIVE_PAGE;
+ // Make sure important flags are carried over.
+ $_GET['promo'] = 'incentive_id';
+ $_GET['progressive'] = 'true';
+ // There is no `test_mode` param and no test mode is set. It should end up as a live mode onboarding.
+
+ // The Jetpack connection is in working order.
+ $this->mock_jetpack_connection();
+
+ $this->mock_database_cache
+ ->expects( $this->any() )
+ ->method( 'get_or_add' )
+ ->willReturn( [] ); // Empty array means no Stripe account connected.
+
+ // Assert.
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_overview_page' );
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_connect_page' );
+ $this->mock_redirect_service
+ ->expects( $this->never() )
+ ->method( 'redirect_to_onboarding_wizard' );
+
+ update_option( '_wcpay_feature_embedded_kyc', '1' );
+
+ // If embedded KYC is in progress, we expect different URL.
+ $this->mock_onboarding_service
+ ->expects( $this->once() )
+ ->method( 'is_embedded_kyc_in_progress' )
+ ->willReturn( true );
+
+ $this->mock_api_client
+ ->expects( $this->never() )
+ ->method( 'get_onboarding_data' );
+
+ $this->mock_redirect_service
+ ->expects( $this->once() )
+ ->method( 'redirect_to' )
+ ->with(
+ $this->logicalOr(
+ $this->stringContains( 'page=wc-admin&path=/payments/onboarding/kyc' ),
+ $this->stringContains( 'page=wc-admin&path=%2Fpayments%2Fonboarding%2Fkyc' )
+ )
+ );
+
+ // Act.
+ $this->wcpay_account->maybe_handle_onboarding();
+ }
+
public function test_maybe_handle_onboarding_init_stripe_onboarding_existing_account() {
// Arrange.
// We need to be in the WP admin dashboard.
diff --git a/tests/unit/test-class-wc-payments-checkout.php b/tests/unit/test-class-wc-payments-checkout.php
index 34175b8dc70..1c471baa5fd 100644
--- a/tests/unit/test-class-wc-payments-checkout.php
+++ b/tests/unit/test-class-wc-payments-checkout.php
@@ -374,7 +374,6 @@ public function test_link_payment_method_provided_when_card_enabled() {
->willReturn( $payment_methods );
$this->assertSame(
- $this->system_under_test->get_payment_fields_js_config()['paymentMethodsConfig'],
[
'card' => [
'isReusable' => true,
@@ -383,7 +382,7 @@ public function test_link_payment_method_provided_when_card_enabled() {
'darkIcon' => $dark_icon_url,
'showSaveOption' => true,
'countries' => [],
- 'testingInstructions' => 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.',
+ 'testingInstructions' => 'Test mode: use test card or refer to our testing guide.',
'forceNetworkSavedCards' => false,
],
'link' => [
@@ -396,7 +395,8 @@ public function test_link_payment_method_provided_when_card_enabled() {
'testingInstructions' => '',
'forceNetworkSavedCards' => false,
],
- ]
+ ],
+ $this->system_under_test->get_payment_fields_js_config()['paymentMethodsConfig']
);
}
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index 217bb7be513..e9405375d8f 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -30,6 +30,7 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase {
'_wcpay_feature_documents' => 'documents',
'_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled',
'_wcpay_feature_stripe_ece' => 'isStripeEceEnabled',
+ '_wcpay_feature_embedded_kyc' => 'isEmbeddedKycEnabled',
];
public function set_up() {
@@ -300,6 +301,35 @@ public function test_is_frt_review_feature_active_returns_false_when_flag_is_not
$this->assertFalse( WC_Payments_Features::is_frt_review_feature_active() );
}
+ public function test_is_embedded_kyc_enabled_returns_true() {
+ add_filter(
+ 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME,
+ function ( $pre_option, $option, $default ) {
+ return '1';
+ },
+ 10,
+ 3
+ );
+ $this->assertTrue( WC_Payments_Features::is_embedded_kyc_enabled() );
+ }
+
+ public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_false() {
+ add_filter(
+ 'pre_option_' . WC_Payments_Features::EMBEDDED_KYC_FLAG_NAME,
+ function ( $pre_option, $option, $default ) {
+ return '0';
+ },
+ 10,
+ 3
+ );
+ $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() );
+ $this->assertArrayNotHasKey( 'isEmbeddedKycEnabled', WC_Payments_Features::to_array() );
+ }
+
+ public function test_is_embedded_kyc_enabled_returns_false_when_flag_is_not_set() {
+ $this->assertFalse( WC_Payments_Features::is_embedded_kyc_enabled() );
+ }
+
private function setup_enabled_flags( array $enabled_flags ) {
foreach ( array_keys( self::FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING ) as $flag ) {
add_filter(
diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php
index 7ec162205e4..a8d9686f763 100644
--- a/tests/unit/test-class-wc-payments-onboarding-service.php
+++ b/tests/unit/test-class-wc-payments-onboarding-service.php
@@ -34,6 +34,13 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase {
*/
private $mock_database_cache;
+ /**
+ * Mock WC_Payments_Session_Service
+ *
+ * @var MockObject
+ */
+ private $mock_session_service;
+
/**
* Example business types array.
*
@@ -129,10 +136,11 @@ class WC_Payments_Onboarding_Service_Test extends WCPAY_UnitTestCase {
public function set_up() {
parent::set_up();
- $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
- $this->mock_database_cache = $this->createMock( Database_Cache::class );
+ $this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
+ $this->mock_database_cache = $this->createMock( Database_Cache::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache );
+ $this->onboarding_service = new WC_Payments_Onboarding_Service( $this->mock_api_client, $this->mock_database_cache, $this->mock_session_service );
$this->onboarding_service->init_hooks();
}
@@ -202,6 +210,18 @@ public function test_set_test_mode() {
delete_option( WC_Payments_Onboarding_Service::TEST_MODE_OPTION );
}
+ public function test_is_embedded_kyc_in_progress() {
+ $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() );
+
+ $this->onboarding_service->set_embedded_kyc_in_progress();
+
+ $this->assertTrue( $this->onboarding_service->is_embedded_kyc_in_progress() );
+
+ $this->onboarding_service->clear_embedded_kyc_in_progress();
+
+ $this->assertFalse( $this->onboarding_service->is_embedded_kyc_in_progress() );
+ }
+
/**
* @dataProvider data_get_from
*/
diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php
index de372ddc989..db1c564d44c 100644
--- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php
+++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php
@@ -430,6 +430,7 @@ public function test_get_shipping_options_returns_chosen_option() {
[
'label' => 'Shipping',
'amount' => $flat_rate['amount'],
+ 'key' => 'total_shipping',
],
];
diff --git a/tests/unit/test-class-wc-payments-utils.php b/tests/unit/test-class-wc-payments-utils.php
index 08a9420d674..a7aba06f9ab 100644
--- a/tests/unit/test-class-wc-payments-utils.php
+++ b/tests/unit/test-class-wc-payments-utils.php
@@ -931,6 +931,12 @@ public function test_get_cached_minimum_amount_returns_null_without_cache() {
$this->assertNull( $result );
}
+ public function test_get_cached_minimum_amount_returns_amount_fallbacking_from_stripe_list() {
+ delete_transient( 'wcpay_minimum_amount_usd' );
+ $result = WC_Payments_Utils::get_cached_minimum_amount( 'usd', true );
+ $this->assertSame( 50, $result );
+ }
+
public function test_get_last_refund_from_order_id_returns_correct_refund() {
$order = WC_Helper_Order::create_order();
$refund_1 = wc_create_refund( [ 'order_id' => $order->get_id() ] );
diff --git a/webpack/shared.js b/webpack/shared.js
index 953830ba44f..7ef039967cd 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -41,6 +41,7 @@ module.exports = {
'product-details': './client/product-details/index.js',
'cart-block': './client/cart/blocks/index.js',
'plugins-page': './client/plugins-page/index.js',
+ 'frontend-tracks': './client/frontend-tracks/index.js',
},
// Override webpack public path dynamically on every entry.
// Required for chunks loading to work on sites with JS concatenation.
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 16cecd888db..149100add8e 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -11,7 +11,7 @@
* WC tested up to: 9.2.0
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 8.1.1
+ * Version: 8.2.0
* Requires Plugins: woocommerce
*
* @package WooCommerce\Payments
@@ -43,7 +43,7 @@ function wcpay_activated() {
// Only redirect to onboarding when activated on its own. Either with a link...
isset( $_GET['action'] ) && 'activate' === $_GET['action'] // phpcs:ignore WordPress.Security.NonceVerification
// ...or with a bulk action.
- || isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && 1 === count( $_POST['checked'] ) // phpcs:ignore WordPress.Security.NonceVerification
+ || isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && 1 === count( $_POST['checked'] ) // phpcs:ignore WordPress.Security.NonceVerification, Generic.CodeAnalysis.RequireExplicitBooleanOperatorPrecedence.MissingParentheses
) {
update_option( 'wcpay_should_redirect_to_onboarding', true );
}