( {
+ createRegistryControl: jest.fn(),
+ dispatch: jest.fn( () => ( {
+ setIsMatching: jest.fn(),
+ onLoad: jest.fn(),
+ } ) ),
+ registerStore: jest.fn(),
+ combineReducers: jest.fn(),
+ select: jest.fn(),
+ useSelect: jest.fn(),
+ useDispatch: jest.fn( () => ( {
+ createNotice: jest.fn(),
+ createErrorNotice: jest.fn(),
+ } ) ),
+ withDispatch: jest.fn( () => jest.fn() ),
+ withSelect: jest.fn( () => jest.fn() ),
+} ) );
+
+jest.mock( 'wcpay/data', () => ( {
+ usePaymentActivityData: jest.fn(),
+} ) );
+
+const mockUsePaymentActivityData = usePaymentActivityData as jest.MockedFunction<
+ typeof usePaymentActivityData
+>;
+
+mockUsePaymentActivityData.mockReturnValue( {
+ paymentActivityData: {
+ total_payment_volume: 123456,
+ charges: 9876,
+ fees: 1234,
+ disputes: 5555,
+ refunds: 4444,
+ },
+ isLoading: false,
+} );
+
declare const global: {
wcpaySettings: {
lifetimeTPV: number;
+ isOverviewSurveySubmitted?: boolean;
accountStatus: {
deposits: {
restrictions: string;
@@ -78,7 +117,10 @@ describe( 'PaymentActivity component', () => {
} );
it( 'should render', () => {
- const { container } = render(
);
+ const { container, getByText } = render(
);
+
+ // Check survey is rendered.
+ getByText( 'Are those metrics helpful?' );
expect( container ).toMatchSnapshot();
} );
@@ -91,4 +133,14 @@ describe( 'PaymentActivity component', () => {
expect( getByText( 'No paymentsβ¦yet!' ) ).toBeInTheDocument();
expect( container ).toMatchSnapshot();
} );
+
+ it( 'should not render survey if survey is already submitted', () => {
+ global.wcpaySettings.isOverviewSurveySubmitted = true;
+
+ const { queryByText } = render(
);
+
+ expect(
+ queryByText( 'Are those metrics helpful?' )
+ ).not.toBeInTheDocument();
+ } );
} );
diff --git a/client/globals.d.ts b/client/globals.d.ts
index 9bbf2d80d27..ac9a3c041fc 100644
--- a/client/globals.d.ts
+++ b/client/globals.d.ts
@@ -128,6 +128,7 @@ declare global {
trackingInfo?: {
hosting_provider: string;
};
+ isOverviewSurveySubmitted: boolean;
lifetimeTPV: number;
};
diff --git a/docker-compose.yml b/docker-compose.yml
index 507259626b2..86f7f52bf4b 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
volumes:
## Kludge for not having the ./docker directory bound recursively
dockerdirectory:
@@ -30,7 +28,7 @@ services:
- dockerdirectory:/var/www/html/wp-content/plugins/woocommerce-payments/docker
- ./docker/bin:/var/scripts
extra_hosts:
- - "host.docker.internal:host-gateway"
+ - "host.docker.internal:host-gateway"
db:
container_name: woocommerce_payments_mysql
image: mariadb:10.5.8
diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php
index 8bfd30c11c2..e6b5cc42909 100644
--- a/includes/admin/class-wc-payments-admin.php
+++ b/includes/admin/class-wc-payments-admin.php
@@ -854,6 +854,7 @@ private function get_js_settings(): array {
],
'dismissedDuplicateNotices' => get_option( 'wcpay_duplicate_payment_method_notices_dismissed', [] ),
'locale' => WC_Payments_Utils::get_language_data( get_locale() ),
+ 'isOverviewSurveySubmitted' => get_option( 'wcpay_survey_payment_overview_submitted', false ),
'trackingInfo' => $this->account->get_tracking_info(),
'lifetimeTPV' => $this->account->get_lifetime_total_payment_volume(),
];
diff --git a/includes/admin/class-wc-rest-payments-survey-controller.php b/includes/admin/class-wc-rest-payments-survey-controller.php
index b670653cc48..55ab076fcb0 100644
--- a/includes/admin/class-wc-rest-payments-survey-controller.php
+++ b/includes/admin/class-wc-rest-payments-survey-controller.php
@@ -8,11 +8,143 @@
defined( 'ABSPATH' ) || exit;
/**
- * REST dummy controller for settings. Needs to remain to prevent errors on update from v7.2 and earlier.
+ * REST controller for settings.
*/
class WC_REST_Payments_Survey_Controller extends WP_REST_Controller {
+
+ /**
+ * Endpoint namespace.
+ *
+ * @var string
+ */
+ protected $namespace = 'wc/v3';
+
+ /**
+ * Endpoint path.
+ *
+ * @var string
+ */
+ protected $rest_base = 'payments/upe_survey';
+
+ /**
+ * The HTTP client, used to forward the request to WPCom.
+ *
+ * @var WC_Payments_Http
+ */
+ protected $http_client;
+
+ /**
+ * The constructor.
+ * WC_REST_Payments_Survey_Controller constructor.
+ *
+ * @param WC_Payments_Http_Interface $http_client - The HTTP client, used to forward the request to WPCom.
+ */
+ public function __construct( WC_Payments_Http_Interface $http_client ) {
+ $this->http_client = $http_client;
+ }
+
+ /**
+ * Configure REST API routes.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/payments-overview',
+ [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'submit_payments_overview_survey' ],
+ 'permission_callback' => [ $this, 'check_permission' ],
+ 'args' => [
+ 'rating' => [
+ 'type' => 'string',
+ 'items' => [
+ 'type' => 'string',
+ 'enum' => [
+ 'very-unhappy',
+ 'unhappy',
+ 'neutral',
+ 'happy',
+ 'very-happy',
+ ],
+ ],
+ 'validate_callback' => 'rest_validate_request_arg',
+ ],
+ 'comments' => [
+ 'type' => 'string',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'sanitize_callback' => 'wp_filter_nohtml_kses',
+ ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Submits the overview survey trhough the WPcom API.
+ *
+ * @param WP_REST_Request $request the request being made.
+ *
+ * @return WP_REST_Response
+ */
+ public function submit_payments_overview_survey( WP_REST_Request $request ): WP_REST_Response {
+ $comments = $request->get_param( 'comments' ) ?? '';
+ $rating = $request->get_param( 'rating' ) ?? '';
+
+ if ( empty( $comments ) && empty( $rating ) ) {
+ return new WP_REST_Response(
+ [
+ 'success' => false,
+ 'err' => 'No answers provided',
+ ],
+ 400
+ );
+ }
+
+ // Jetpack connection 1.27.0 created a default value for this constant, but we're using an older version of the package
+ // https://github.com/Automattic/jetpack/blob/master/projects/packages/connection/CHANGELOG.md#1270---2021-05-25
+ // - Connection: add the default value of JETPACK__WPCOM_JSON_API_BASE to the Connection Utils class
+ // this is just a patch so that we don't need to upgrade.
+ // as an alternative, I could have also used the `jetpack_constant_default_value` filter, but this is shorter and also provides a fallback.
+ defined( 'JETPACK__WPCOM_JSON_API_BASE' ) || define( 'JETPACK__WPCOM_JSON_API_BASE', 'https://public-api.wordpress.com' );
+
+ $wpcom_request = $this->http_client->wpcom_json_api_request_as_user(
+ '/marketing/survey',
+ '2',
+ [
+ 'method' => 'POST',
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'X-Forwarded-For' => \WC_Geolocation::get_ip_address(),
+ ],
+ ],
+ [
+ 'site_id' => $this->http_client->get_blog_id(),
+ 'survey_id' => 'wcpay-payments-overview',
+ 'survey_responses' => [
+ 'rating' => $rating,
+ 'comments' => [ 'text' => $comments ],
+ 'wcpay-version' => [ 'text' => WCPAY_VERSION_NUMBER ],
+ ],
+ ]
+ );
+
+ $wpcom_request_body = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
+
+ if ( ! is_wp_error( $wpcom_request ) ) {
+ update_option( 'wcpay_survey_payment_overview_submitted', true );
+ }
+
+ return new WP_REST_Response( $wpcom_request_body, wp_remote_retrieve_response_code( $wpcom_request ) );
+ }
+
/**
- * Dummy register routes.
+ * Verify access.
+ *
+ * Override this method if custom permissions required.
+ *
+ * @return bool
*/
- public function register_routes() {}
+ public function check_permission() {
+ return current_user_can( 'manage_woocommerce' );
+ }
}
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index ea2e566416a..9dbd87c85df 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -1047,6 +1047,10 @@ public static function init_rest_api() {
$refunds_controller = new WC_REST_Payments_Refunds_Controller( self::$api_client );
$refunds_controller->register_routes();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-survey-controller.php';
+ $survey_controller = new WC_REST_Payments_Survey_Controller( self::get_wc_payments_http() );
+ $survey_controller->register_routes();
+
if ( WC_Payments_Features::is_documents_section_enabled() ) {
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-documents-controller.php';
$documents_controller = new WC_REST_Payments_Documents_Controller( self::$api_client );
diff --git a/includes/multi-currency/Analytics.php b/includes/multi-currency/Analytics.php
index d2c05dbd2df..ef3f5b17a5a 100644
--- a/includes/multi-currency/Analytics.php
+++ b/includes/multi-currency/Analytics.php
@@ -114,6 +114,11 @@ public function register_admin_scripts() {
* @return void
*/
public function register_customer_currencies() {
+ $data_registry = Package::container()->get( AssetDataRegistry::class );
+ if ( $data_registry->exists( 'customerCurrencies' ) ) {
+ return;
+ }
+
$currencies = $this->multi_currency->get_all_customer_currencies();
$available_currencies = $this->multi_currency->get_available_currencies();
$currency_options = [];
@@ -137,8 +142,7 @@ public function register_customer_currencies() {
];
}
- $data_registry = Package::container()->get( AssetDataRegistry::class );
- $data_registry->add( 'customerCurrencies', $currency_options, true );
+ $data_registry->add( 'customerCurrencies', $currency_options );
}
/**
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 5758cef5c08..8c3e6c29ba9 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -1861,6 +1861,9 @@ protected function request( $params, $api, $method, $is_site_specific = true, $u
$response_code = null;
$last_exception = null;
+ // The header intention is to give us insights into request latency between store and backend.
+ $headers['X-Request-Initiated'] = microtime( true );
+
try {
$response = $this->http_client->remote_request(
[
diff --git a/package-lock.json b/package-lock.json
index ed9bbc6f32b..ad92d050ba0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"devDependencies": {
"@automattic/color-studio": "2.3.1",
"@jest/test-sequencer": "29.5.0",
+ "@playwright/test": "1.43.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.7",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "11.2.5",
@@ -33,6 +34,7 @@
"@types/canvas-confetti": "1.6.4",
"@types/intl-tel-input": "17.0.4",
"@types/lodash": "4.14.170",
+ "@types/node": "20.9.0",
"@types/react": "17.0.2",
"@types/react-transition-group": "4.4.6",
"@types/wordpress__components": "19.10.5",
@@ -7542,6 +7544,21 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
+ "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.43.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -8587,10 +8604,13 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
- "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
- "dev": true
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -10294,9 +10314,9 @@
}
},
"node_modules/@woocommerce/csv-export/node_modules/@types/node": {
- "version": "16.18.65",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz",
- "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==",
+ "version": "16.18.68",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.68.tgz",
+ "integrity": "sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==",
"dev": true
},
"node_modules/@woocommerce/currency": {
@@ -19158,9 +19178,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001565",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
- "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+ "version": "1.0.30001488",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
+ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"dev": true,
"funding": [
{
@@ -33877,6 +33897,36 @@
"node": ">=8"
}
},
+ "node_modules/playwright": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
+ "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.43.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
+ "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -40375,6 +40425,12 @@
"through": "^2.3.8"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -48210,6 +48266,15 @@
"fastq": "^1.6.0"
}
},
+ "@playwright/test": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
+ "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
+ "dev": true,
+ "requires": {
+ "playwright": "1.43.1"
+ }
+ },
"@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -49007,10 +49072,13 @@
"dev": true
},
"@types/node": {
- "version": "20.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
- "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
- "dev": true
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~5.26.4"
+ }
},
"@types/normalize-package-data": {
"version": "2.4.1",
@@ -50468,9 +50536,9 @@
},
"dependencies": {
"@types/node": {
- "version": "16.18.65",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz",
- "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==",
+ "version": "16.18.68",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.68.tgz",
+ "integrity": "sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==",
"dev": true
}
}
@@ -57506,9 +57574,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30001565",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
- "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+ "version": "1.0.30001488",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
+ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"dev": true
},
"canvas-confetti": {
@@ -69129,6 +69197,22 @@
}
}
},
+ "playwright": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
+ "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
+ "dev": true,
+ "requires": {
+ "fsevents": "2.3.2",
+ "playwright-core": "1.43.1"
+ }
+ },
+ "playwright-core": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
+ "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
+ "dev": true
+ },
"please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -74145,6 +74229,12 @@
"through": "^2.3.8"
}
},
+ "undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
index 9a9b04c3508..60416341b5d 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,9 @@
"test:e2e": "NODE_CONFIG_DIR='tests/e2e/config' JEST_PUPPETEER_CONFIG='tests/e2e/config/jest-puppeteer-headless.config.js' wp-scripts test-e2e --config tests/e2e/config/jest.config.js",
"test:e2e-dev": "NODE_CONFIG_DIR='tests/e2e/config' JEST_PUPPETEER_CONFIG='tests/e2e/config/jest-puppeteer.config.js' wp-scripts test-e2e --config tests/e2e/config/jest.config.js --puppeteer-interactive",
"test:e2e-performance": "NODE_CONFIG_DIR='tests/e2e/config' wp-scripts test-e2e --config tests/e2e/config/jest.performance.config.js",
+ "test:e2e-pw": "./tests/e2e-pw/test-e2e-pw.sh",
+ "test:e2e-pw-ui": "./tests/e2e-pw/test-e2e-pw-ui.sh",
+ "test:e2e-pw-ci": "npx playwright test --config=tests/e2e-pw/playwright.config.ts",
"test:update-snapshots": "npm run test:js -- --updateSnapshot",
"test:php": "./bin/run-tests.sh",
"test:php-coverage": "./bin/check-test-coverage.sh",
@@ -85,6 +88,7 @@
"devDependencies": {
"@automattic/color-studio": "2.3.1",
"@jest/test-sequencer": "29.5.0",
+ "@playwright/test": "1.43.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.7",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "11.2.5",
@@ -93,6 +97,7 @@
"@types/canvas-confetti": "1.6.4",
"@types/intl-tel-input": "17.0.4",
"@types/lodash": "4.14.170",
+ "@types/node": "20.9.0",
"@types/react": "17.0.2",
"@types/react-transition-group": "4.4.6",
"@types/wordpress__components": "19.10.5",
diff --git a/tests/e2e-pw/README.md b/tests/e2e-pw/README.md
new file mode 100644
index 00000000000..84bf18a95ff
--- /dev/null
+++ b/tests/e2e-pw/README.md
@@ -0,0 +1,102 @@
+# Playwright end-to-end tests π
+
+Playwright e2e tests can be found in the `./tests/e2e-pw/specs` directory. These will run in parallel with the existing Puppeteer e2e tests and are intended to replace them as they are migrated.
+
+## Setup local e2e environment
+
+See [tests/e2e/README.md](/tests/e2e/README.md) for detailed e2e environment setup instructions.
+
+1. `npm run test:e2e-setup`
+1. `npm run test:e2e-up`
+
+## Running Playwright e2e tests
+
+- `npm run test:e2e-pw` headless run from within a linux docker container.
+- `npm run test:e2e-pw-ui` runs tests in interactive UI mode from within a linux docker container β recommended for authoring tests and re-running failed tests.
+- `npm run test:e2e-pw keyword` runs tests only with a specific keyword in the file name, e.g. `dispute` or `checkout`.
+- `npm run test:e2e-pw --update-snapshots` updates snapshots.
+
+## FAQs
+
+**How do I wait for a page or element to load?**
+
+Since [Playwright automatically waits](https://playwright.dev/docs/actionability) for elements to be present in the page before interacting with them, you probably don't need to explicitly wait for elements to load. For example, all of the following locators will automatically wait for the element to be present and stable before asserting or interacting with it:
+
+```ts
+await expect( page.getByRole( 'heading', { name: 'Sign up' } ) ).toBeVisible();
+await page.getByRole( 'checkbox', { name: 'Subscribe' } ).check();
+await page.getByRole( 'button', { name: /submit/i } ).click();
+```
+
+In some cases, you may need to wait for the page to reach a certain load state before interacting with it. You can use `await page.waitForLoadState( 'domcontentloaded' );` to wait for the page to finish loading.
+
+**What is the best way to target elements in the page?**
+
+Prefer the use of [user-facing attribute or test-id locators](https://playwright.dev/docs/locators#locating-elements) to target elements in the page. This will make the tests more resilient to changes to implementation details, such as class names.
+
+```ts
+// Prefer locating by role, label, text, or test id when possible. See https://playwright.dev/docs/locators
+await page.getByRole( 'button', { name: 'All deposits' } ).click();
+await page.getByLabel( 'Select a deposit status' ).selectOption( 'Pending' );
+await expect( page.getByText( 'Order received' ) ).toBeVisible();
+await page.getByTestId( 'accept-dispute-button' ).click();
+
+// Use CSS selectors as a last resort
+await page.locator( 'button.components-button.is-secondary' );
+```
+
+**How do I create a visual regression test?**
+
+Visual regression tests are captured by the [`toHaveScreenshot()` function](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-2). This function takes a screenshot of a page or element and compares it to a reference image. If the images are different, the test will fail.
+
+```ts
+await expect( page ).toHaveScreenshot();
+
+await expect(
+ page.getByRole( 'button', { name: 'All deposits' } )
+).toHaveScreenshot();
+```
+
+**How can I act as shopper or merchant in a test?**
+
+1. To switch between `shopper` and `merchant` role in a test, use the `getShopper` and `getMerchant` function:
+
+```ts
+import { getShopper, getMerchant } from './utils/helpers';
+
+test( 'should do things as shopper and merchant', async ( { browser } ) => {
+ const { shopperPage } = await getShopper( browser );
+ const { merchantPage } = await getMerchant( browser );
+
+ // do things as shopper
+ await shopperPage.goto( '/cart/' );
+
+ // do things as merchant
+ await merchantPage.goto( '/wp-admin/admin.php?page=wc-settings' );
+} );
+```
+
+2. To act as `shopper` or `merchant` for an entire test suite (`describe`), use the helper function `useShopper` or `useMerchant` from `tests/e2e-pw/utils/helpers.ts`:
+
+```ts
+import { useShopper } from '../utils/helpers';
+
+test.describe( 'Sign in as customer', () => {
+ useShopper();
+ test( 'Load customer my account page', async ( { page } ) => {
+ // do things as shopper
+ await page.goto( '/my-account' );
+ } );
+} );
+```
+
+**How can I investigate and interact with a test failures?**
+
+- **Github Action test runs**
+ - View GitHub checks in the "Checks" tab of a PR
+ - Click on the "E2E Playwright Tests" job to see the job summary
+ - Download the `playwright-report.zip` artifact, extract and copy the `playwright-report` directory to the root of the WooPayments repository
+ - Run `npx playwright show-report` to open the report in a browser
+- **Local test runs**:
+ - Local test reports will output in the `playwright-report` directory
+ - Run `npx playwright show-report` to open the report in a browser
diff --git a/tests/e2e-pw/config/default.ts b/tests/e2e-pw/config/default.ts
new file mode 100644
index 00000000000..a3742804047
--- /dev/null
+++ b/tests/e2e-pw/config/default.ts
@@ -0,0 +1,275 @@
+export const config = {
+ users: {
+ admin: {
+ username: 'admin',
+ password: 'password',
+ email: 'e2e-wcpay-admin@woo.com',
+ },
+ customer: {
+ username: 'customer',
+ password: 'password',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ 'subscriptions-customer': {
+ username: 'subscriptions-customer',
+ password: 'password',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ guest: {
+ email: 'e2e-wcpay-guest@woo.com',
+ },
+ },
+ products: {
+ simple: {
+ name: 'Beanie',
+ },
+ variable: {
+ name: 'Variable Product with Three Variations',
+ },
+ grouped: {
+ name: 'Grouped Product with Three Children',
+ },
+ },
+ addresses: {
+ admin: {
+ store: {
+ firstname: 'I am',
+ lastname: 'Admin',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'store',
+ countryandstate: 'United States (US) β California',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ },
+ customer: {
+ billing: {
+ firstname: 'I am',
+ lastname: 'Customer',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'billing',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ shipping: {
+ firstname: 'I am',
+ lastname: 'Recipient',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'shipping',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ },
+ 'subscriptions-customer': {
+ billing: {
+ first_name: 'I am',
+ last_name: 'Subscriptions Customer',
+ company: 'Automattic',
+ country: 'United States (US)',
+ address_1: '60 29th Street #343',
+ address_2: 'billing',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ shipping: {
+ first_name: 'I am',
+ last_name: 'Subscriptions Recipient',
+ company: 'Automattic',
+ country: 'United States (US)',
+ address_1: '60 29th Street #343',
+ address_2: 'shipping',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ },
+ },
+ cards: {
+ basic: {
+ number: '4242424242424242',
+ expires: {
+ month: '02',
+ year: '45',
+ },
+ cvc: '424',
+ label: 'Visa ending in 4242',
+ },
+ basic2: {
+ number: '4111111111111111',
+ expires: {
+ month: '11',
+ year: '45',
+ },
+ cvc: '123',
+ label: 'Visa ending in 1111',
+ },
+ basic3: {
+ number: '378282246310005',
+ expires: {
+ month: '11',
+ year: '45',
+ },
+ cvc: '1234',
+ label: 'Amex ending in 0005',
+ },
+ '3ds': {
+ number: '4000002760003184',
+ expires: {
+ month: '03',
+ year: '45',
+ },
+ cvc: '525',
+ label: 'Visa ending in 3184',
+ },
+ '3dsOTP': {
+ number: '4000002500003155',
+ expires: {
+ month: '04',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 3155',
+ },
+ '3ds2': {
+ number: '4000000000003220',
+ expires: {
+ month: '04',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 3220',
+ },
+ 'disputed-fraudulent': {
+ number: '4000000000000259',
+ expires: {
+ month: '05',
+ year: '45',
+ },
+ cvc: '525',
+ label: 'Visa ending in 0259',
+ },
+ 'disputed-unreceived': {
+ number: '4000000000002685',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 2685',
+ },
+ declined: {
+ number: '4000000000000002',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0002',
+ },
+ 'declined-funds': {
+ number: '4000000000009995',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 9995',
+ },
+ 'declined-incorrect': {
+ number: '4242424242424241',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 4241',
+ },
+ 'declined-expired': {
+ number: '4000000000000069',
+ expires: {
+ month: '06',
+ year: '25',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0069',
+ },
+ 'declined-cvc': {
+ number: '4000000000000127',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0127',
+ },
+ 'declined-processing': {
+ number: '4000000000000119',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0119',
+ },
+ 'declined-3ds': {
+ number: '4000008400001629',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 1629',
+ },
+ 'invalid-exp-date': {
+ number: '4242424242424242',
+ expires: {
+ month: '11',
+ year: '12',
+ },
+ cvc: '123',
+ label: 'Visa ending in 4242',
+ },
+ 'invalid-cvv-number': {
+ number: '4242424242424242',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '11',
+ label: 'Visa ending in 4242',
+ },
+ },
+ onboardingwizard: {
+ industry: 'Test industry',
+ numberofproducts: '1 - 10',
+ sellingelsewhere: 'No',
+ },
+ settings: {
+ shipping: {
+ zonename: 'United States',
+ zoneregions: 'United States (US)',
+ shippingmethod: 'Free shipping',
+ },
+ },
+};
+
+export type CustomerAddress = typeof config.addresses.customer.billing;
diff --git a/tests/e2e-pw/docker-compose.yml b/tests/e2e-pw/docker-compose.yml
new file mode 100644
index 00000000000..2919d6ab568
--- /dev/null
+++ b/tests/e2e-pw/docker-compose.yml
@@ -0,0 +1,12 @@
+services:
+ playwright:
+ # When updating the Playwright version in the image tag below, make sure to update the npm `@playwright/test` package.json version as well.
+ image: mcr.microsoft.com/playwright:v1.43.1-jammy
+ working_dir: /woopayments
+ volumes:
+ - $PWD:/woopayments
+ environment:
+ - "BASE_URL=http://host.docker.internal:8084"
+ ports:
+ - "8077:8077"
+ ipc: host
diff --git a/tests/e2e-pw/playwright.config.ts b/tests/e2e-pw/playwright.config.ts
new file mode 100644
index 00000000000..74ab435eb9a
--- /dev/null
+++ b/tests/e2e-pw/playwright.config.ts
@@ -0,0 +1,70 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+/**
+ * External dependencies
+ */
+import { defineConfig, devices } from '@playwright/test';
+
+const { BASE_URL } = process.env;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig( {
+ testDir: './specs/',
+ /* Run tests in files in parallel */
+ fullyParallel: false,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !! process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests. */
+ workers: 1,
+ /* Reporters to use. See https://playwright.dev/docs/test-reporters */
+ reporter: process.env.CI
+ ? [
+ // If running on CI, also use the GitHub Actions reporter
+ [ 'github' ],
+ [ 'html' ],
+ ]
+ : [ [ 'html', { open: 'never' } ] ],
+ outputDir: './test-results',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL: BASE_URL ?? 'http://localhost:8084',
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ video: 'on-first-retry',
+ viewport: { width: 1280, height: 720 },
+ },
+ timeout: 60 * 1000, // Default is 30s, somteimes it is not enough for local tests due to long setup.
+ expect: {
+ toHaveScreenshot: { maxDiffPixelRatio: 0.025 },
+ //=* Increase expect timeout to 10 seconds. See https://playwright.dev/docs/test-timeouts#set-expect-timeout-in-the-config.*/
+ timeout: 10 * 1000,
+ },
+ snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'basic',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testMatch: /basic.spec.ts/,
+ dependencies: [ 'setup' ],
+ },
+ {
+ name: 'merchant',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testDir: './specs/merchant',
+ dependencies: [ 'setup' ],
+ },
+ {
+ name: 'shopper',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testDir: './specs/shopper',
+ dependencies: [ 'setup' ],
+ },
+ // Setup project
+ { name: 'setup', testMatch: /.*\.setup\.ts/ },
+ ],
+} );
diff --git a/tests/e2e-pw/specs/auth.setup.ts b/tests/e2e-pw/specs/auth.setup.ts
new file mode 100644
index 00000000000..595ff6c94dd
--- /dev/null
+++ b/tests/e2e-pw/specs/auth.setup.ts
@@ -0,0 +1,129 @@
+/* eslint-disable no-console */
+/**
+ * External dependencies
+ */
+import { test as setup, expect } from '@playwright/test';
+import fs from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import { config } from '../config/default';
+import {
+ merchantStorageFile,
+ customerStorageFile,
+ wpAdminLogin,
+} from '../utils/helpers';
+
+// See https://playwright.dev/docs/auth#multiple-signed-in-roles
+const {
+ users: { admin, customer },
+} = config;
+
+const isAuthStateStale = ( authStateFile: string ) => {
+ const authFileExists = fs.existsSync( authStateFile );
+
+ if ( ! authFileExists ) {
+ return true;
+ }
+
+ const authStateMtimeMs = fs.statSync( authStateFile ).mtimeMs;
+ const hourInMs = 1000 * 60 * 60;
+ // Invalidate auth state if it's older than a 3 hours.
+ const isStale = Date.now() - authStateMtimeMs > hourInMs * 3;
+ return isStale;
+};
+
+setup( 'authenticate as admin', async ( { page } ) => {
+ // For local development, use existing state if it exists and isn't stale.
+ if ( ! process.env.CI ) {
+ if ( ! isAuthStateStale( merchantStorageFile ) ) {
+ console.log( 'Using existing merchant state.' );
+ return;
+ }
+ }
+
+ // Sign in as admin user and save state
+ let adminLoggedIn = false;
+ const adminRetries = 5;
+ for ( let i = 0; i < adminRetries; i++ ) {
+ try {
+ console.log( 'Trying to log-in as admin...' );
+ await wpAdminLogin( page, admin );
+ await page.waitForLoadState( 'domcontentloaded' );
+ await page.goto( `/wp-admin` );
+ await page.waitForLoadState( 'domcontentloaded' );
+
+ await expect(
+ page.getByRole( 'heading', { name: 'Dashboard' } )
+ ).toBeVisible();
+
+ console.log( 'Logged-in as admin successfully.' );
+ adminLoggedIn = true;
+ break;
+ } catch ( e ) {
+ console.log(
+ `Admin log-in failed, Retrying... ${ i }/${ adminRetries }`
+ );
+ console.log( e );
+ }
+ }
+
+ if ( ! adminLoggedIn ) {
+ throw new Error(
+ 'Cannot proceed e2e test, as admin login failed. Please check if the test site has been setup correctly.'
+ );
+ }
+
+ // End of authentication steps.
+
+ await page.context().storageState( { path: merchantStorageFile } );
+} );
+
+setup( 'authenticate as customer', async ( { page } ) => {
+ // For local development, use existing state if it exists and isn't stale.
+ if ( ! process.env.CI ) {
+ if ( ! isAuthStateStale( customerStorageFile ) ) {
+ console.log( 'Using existing customer state.' );
+ return;
+ }
+ }
+
+ // Sign in as customer user and save state
+ let customerLoggedIn = false;
+ const customerRetries = 5;
+ for ( let i = 0; i < customerRetries; i++ ) {
+ try {
+ console.log( 'Trying to log-in as customer...' );
+ await wpAdminLogin( page, customer );
+
+ await page.goto( `/my-account` );
+ await expect(
+ page.locator(
+ '.woocommerce-MyAccount-navigation-link--customer-logout'
+ )
+ ).toBeVisible();
+ await expect(
+ page.locator( 'div.woocommerce-MyAccount-content > p >> nth=0' )
+ ).toContainText( 'Hello' );
+
+ console.log( 'Logged-in as customer successfully.' );
+ customerLoggedIn = true;
+ break;
+ } catch ( e ) {
+ console.log(
+ `Customer log-in failed. Retrying... ${ i }/${ customerRetries }`
+ );
+ console.log( e );
+ }
+ }
+
+ if ( ! customerLoggedIn ) {
+ throw new Error(
+ 'Cannot proceed e2e test, as customer login failed. Please check if the test site has been setup correctly.'
+ );
+ }
+ // End of authentication steps.
+
+ await page.context().storageState( { path: customerStorageFile } );
+} );
diff --git a/tests/e2e-pw/specs/basic.spec.ts b/tests/e2e-pw/specs/basic.spec.ts
new file mode 100644
index 00000000000..910176fdc21
--- /dev/null
+++ b/tests/e2e-pw/specs/basic.spec.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+import { useMerchant, useShopper } from '../utils/helpers';
+
+test.describe(
+ 'A basic set of tests to ensure WP, wp-admin and my-account load',
+ () => {
+ test( 'Load the home page', async ( { page } ) => {
+ await page.goto( '/' );
+ const title = page.locator( 'h1.site-title' );
+ await expect( title ).toHaveText(
+ /WooCommerce Payments E2E site/i
+ );
+ } );
+
+ test.describe( 'Sign in as admin', () => {
+ useMerchant();
+ test( 'Load Payments Overview', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/overview'
+ );
+ await page.waitForLoadState( 'domcontentloaded' );
+ const logo = page.getByAltText( 'WooPayments logo' );
+ await expect( logo ).toBeVisible();
+ } );
+ } );
+
+ test.describe( 'Sign in as customer', () => {
+ useShopper();
+ test( 'Load customer my account page', async ( { page } ) => {
+ await page.goto( '/my-account' );
+ const title = page.locator( 'h1.entry-title' );
+ await expect( title ).toHaveText( 'My account' );
+ } );
+ } );
+ }
+);
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png
new file mode 100644
index 00000000000..a982090d756
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png
new file mode 100644
index 00000000000..843ee4afe8b
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts b/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts
new file mode 100644
index 00000000000..c0e20768b36
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+import { useMerchant } from '../../utils/helpers';
+
+test.describe( 'Merchant deposits', () => {
+ // Use the merchant user for this test suite.
+ useMerchant();
+
+ test( 'Load the deposits list page', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/deposits'
+ );
+
+ // Wait for the deposits table to load.
+ await page
+ .locator( '.woocommerce-table__table.is-loading' )
+ .waitFor( { state: 'hidden' } );
+
+ expect(
+ page.getByRole( 'heading', {
+ name: 'Deposit history',
+ } )
+ ).toBeVisible();
+ } );
+
+ test( 'Select deposits list advanced filters', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/deposits'
+ );
+
+ // Wait for the deposits table to load.
+ await page
+ .locator( '.woocommerce-table__table.is-loading' )
+ .waitFor( { state: 'hidden' } );
+
+ // Open the advanced filters.
+ await page.getByRole( 'button', { name: 'All deposits' } ).click();
+ await page.getByRole( 'button', { name: 'Advanced filters' } ).click();
+
+ // Select a filter
+ await page.getByRole( 'button', { name: 'Add a Filter' } ).click();
+ await page.getByRole( 'button', { name: 'Status' } ).click();
+
+ // Select a filter option
+ await page
+ .getByLabel( 'Select a deposit status', {
+ exact: true,
+ } )
+ .selectOption( 'Pending' );
+
+ // Scroll to the top to ensure the sticky header doesn't cover the filters.
+ await page.evaluate( () => {
+ window.scrollTo( 0, 0 );
+ } );
+ await expect(
+ page.locator( '.woocommerce-filters' ).last()
+ ).toHaveScreenshot();
+ } );
+} );
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
new file mode 100644
index 00000000000..cd53d8dce72
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
@@ -0,0 +1,82 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import * as shopper from '../../utils/shopper';
+import { config } from '../../config/default';
+import { getMerchant, getShopper } from '../../utils/helpers';
+import * as merchant from '../../utils/merchant';
+
+test.describe(
+ 'Disputes > View dispute details via disputed order notice',
+ () => {
+ let orderId: string;
+
+ test.beforeEach( async ( { browser } ) => {
+ const { shopperPage } = await getShopper( browser );
+ // Place an order to dispute later
+ await shopperPage.goto( '/cart/' );
+ await shopper.addCartProduct( shopperPage );
+
+ await shopperPage.goto( '/checkout/' );
+ await shopper.fillBillingAddress(
+ shopperPage,
+ config.addresses.customer.billing
+ );
+ await shopper.fillCardDetails(
+ shopperPage,
+ config.cards[ 'disputed-fraudulent' ]
+ );
+ await shopper.placeOrder( shopperPage );
+
+ // Get the order ID
+ const orderIdField = shopperPage.locator(
+ '.woocommerce-order-overview__order.order > strong'
+ );
+ orderId = await orderIdField.innerText();
+ } );
+
+ test( 'should navigate to dispute details when disputed order notice button clicked', async ( {
+ browser,
+ } ) => {
+ const { merchantPage } = await getMerchant( browser );
+ await merchant.goToOrder( merchantPage, orderId );
+
+ // If WC < 7.9, return early since the order dispute notice is not present.
+ const orderPaymentDetailsContainerVisible = await merchantPage
+ .locator( '#wcpay-order-payment-details-container' )
+ .isVisible();
+ if ( ! orderPaymentDetailsContainerVisible ) {
+ // eslint-disable-next-line no-console
+ console.log(
+ 'Skipping test since the order dispute notice is not present in WC < 7.9'
+ );
+ return;
+ }
+
+ // Click the order dispute notice.
+ await merchantPage
+ .getByRole( 'button', {
+ name: 'Respond now',
+ } )
+ .click();
+
+ // Verify we see the dispute details on the transaction details merchantPage.
+ await expect(
+ merchantPage.getByText(
+ 'The cardholder claims this is an unauthorized transaction.',
+ { exact: true }
+ )
+ ).toBeVisible();
+
+ // Visual regression test for the dispute notice.
+ await expect(
+ merchantPage.locator( '.dispute-notice' )
+ ).toHaveScreenshot();
+ } );
+ }
+);
diff --git a/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
new file mode 100644
index 00000000000..6197cfa5fcb
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
@@ -0,0 +1,115 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { useMerchant } from '../../utils/helpers';
+
+test.describe( 'payment gateways disable confirmation', () => {
+ useMerchant();
+
+ const getToggle = ( page: Page ) =>
+ page.getByRole( 'link', {
+ name: '"WooPayments" payment method is currently',
+ } );
+
+ const getModalHeading = ( page: Page ) =>
+ page.getByRole( 'heading', { name: 'Disable WooPayments' } );
+
+ const getSaveButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Save changes' } );
+
+ const getCancelButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Cancel' } );
+
+ const getDisableButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Disable' } );
+
+ const waitForToggleLoading = ( page: Page ) =>
+ page
+ .locator( '.woocommerce-input-toggle--loading' )
+ .waitFor( { state: 'hidden' } );
+
+ test.beforeEach( async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion'
+ );
+
+ // If WCPay enabled, disable it
+ if ( ( await getToggle( page ).innerText() ) === 'Yes' ) {
+ // Click the "Disable WCPay" toggle button
+ await getToggle( page ).click();
+
+ // Modal should be displayed
+ await expect( getModalHeading( page ) ).toBeVisible();
+ }
+ } );
+
+ test.afterAll( async ( { browser } ) => {
+ // Ensure WCPay is enabled after the tests, even if they fail
+ const page = await browser.newPage();
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion'
+ );
+
+ if ( ( await getToggle( page ).innerText() ) === 'No' ) {
+ await getToggle( page ).click();
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+ }
+
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should show the confirmation modal when disabling WCPay', async ( {
+ page,
+ } ) => {
+ // Clicking "Cancel" should not disable WCPay
+ await getCancelButton( page ).click();
+
+ // After clicking "Cancel", the modal should close and WCPay should still be enabled, even after refresh
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+ await getSaveButton( page ).click();
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should disable WCPay after confirming, then enable again without confirming', async ( {
+ page,
+ } ) => {
+ // Clicking "Disable" should disable WCPay
+ await getDisableButton( page ).click();
+
+ // After clicking "Disable", the modal should close
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+
+ // and refreshing the page should show WCPay become disabled
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+
+ // now we can re-enable it with no issues
+ await getToggle( page ).click();
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should show the modal even after clicking the cancel button multiple times', async ( {
+ page,
+ } ) => {
+ // Clicking "Cancel" should not disable WCPay
+ await getCancelButton( page ).click();
+
+ // After clicking "Cancel", the modal should close and WCPay should still be enabled
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+ await expect( getToggle( page ) ).not.toHaveClass(
+ 'woocommerce-input-toggle--disabled'
+ );
+
+ // trying again to disable it - the modal should display again
+ await getToggle( page ).click();
+ await expect( getModalHeading( page ) ).toBeVisible();
+ } );
+} );
diff --git a/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts b/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts
new file mode 100644
index 00000000000..b062ab0098a
--- /dev/null
+++ b/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+
+import { config } from '../../config/default';
+import * as shopper from '../../utils/shopper';
+
+test.describe( 'Successful purchase', () => {
+ test.beforeEach( async ( { page } ) => {
+ await shopper.addCartProduct( page );
+
+ await page.goto( '/checkout/' );
+ await shopper.fillBillingAddress(
+ page,
+ config.addresses.customer.billing
+ );
+ } );
+
+ test( 'using a basic card', async ( { page } ) => {
+ await shopper.fillCardDetails( page );
+ await shopper.placeOrder( page );
+
+ await expect(
+ page.getByText( 'Order received' ).first()
+ ).toBeVisible();
+ } );
+
+ test( 'using a 3DS card', async ( { page } ) => {
+ await shopper.fillCardDetails( page, config.cards[ '3ds' ] );
+ await shopper.placeOrder( page );
+ await shopper.confirmCardAuthentication( page );
+
+ await expect(
+ page.getByText( 'Order received' ).first()
+ ).toBeVisible();
+ } );
+} );
diff --git a/tests/e2e-pw/test-e2e-pw-ui.sh b/tests/e2e-pw/test-e2e-pw-ui.sh
new file mode 100755
index 00000000000..d035bb07cd7
--- /dev/null
+++ b/tests/e2e-pw/test-e2e-pw-ui.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+echo "π Running Playwright e2e tests in interactive UI mode.";
+echo "";
+echo "Open http://localhost:8077 in your browser to see the UI.";
+
+docker compose -f ./tests/e2e-pw/docker-compose.yml run --rm -it --service-ports playwright \
+ npx playwright test --config=tests/e2e-pw/playwright.config.ts --ui --ui-host=0.0.0.0 --ui-port=8077 "$@"
diff --git a/tests/e2e-pw/test-e2e-pw.sh b/tests/e2e-pw/test-e2e-pw.sh
new file mode 100755
index 00000000000..33a73f65946
--- /dev/null
+++ b/tests/e2e-pw/test-e2e-pw.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+echo "π Running Playwright e2e tests in default headless mode.";
+
+docker compose -f ./tests/e2e-pw/docker-compose.yml run --rm -it --service-ports playwright \
+ npx playwright test --config=tests/e2e-pw/playwright.config.ts "$@"
diff --git a/tests/e2e-pw/utils/helpers.ts b/tests/e2e-pw/utils/helpers.ts
new file mode 100644
index 00000000000..3112e819f68
--- /dev/null
+++ b/tests/e2e-pw/utils/helpers.ts
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import path from 'path';
+import { test, Page, Browser, BrowserContext } from '@playwright/test';
+
+export const merchantStorageFile = path.resolve(
+ __dirname,
+ '../.auth/merchant.json'
+);
+
+export const customerStorageFile = path.resolve(
+ __dirname,
+ '../.auth/customer.json'
+);
+
+/**
+ * Logs in to the WordPress admin as a given user.
+ */
+export const wpAdminLogin = async (
+ page: Page,
+ user: { username: string; password: string }
+): void => {
+ await page.goto( `/wp-admin` );
+ await page.getByLabel( 'Username or Email Address' ).fill( user.username );
+ await page.getByLabel( 'Password', { exact: true } ).fill( user.password ); // Need exact match to avoid resolving "Show password" button.
+ await page.getByRole( 'button', { name: 'Log In' } ).click();
+};
+
+/**
+ * Sets the shopper as the authenticated user for a test suite (describe).
+ */
+export const useShopper = (): void => {
+ test.use( {
+ storageState: customerStorageFile,
+ } );
+};
+
+/**
+ * Sets the merchant as the authenticated user for a test suite (describe).
+ */
+export const useMerchant = (): void => {
+ test.use( {
+ storageState: merchantStorageFile,
+ } );
+};
+
+/**
+ * Returns the merchant authenticated page and context.
+ * Allows switching between merchant and shopper contexts within a single test.
+ */
+export const getMerchant = async (
+ browser: Browser
+): Promise< {
+ merchantPage: Page;
+ merchantContext: BrowserContext;
+} > => {
+ const merchantContext = await browser.newContext( {
+ storageState: merchantStorageFile,
+ } );
+ const merchantPage = await merchantContext.newPage();
+ return { merchantPage, merchantContext };
+};
+
+/**
+ * Returns the shopper authenticated page and context.
+ * Allows switching between merchant and shopper contexts within a single test.
+ */
+export const getShopper = async (
+ browser: Browser
+): Promise< {
+ shopperPage: Page;
+ shopperContext: BrowserContext;
+} > => {
+ const shopperContext = await browser.newContext( {
+ storageState: customerStorageFile,
+ } );
+ const shopperPage = await shopperContext.newPage();
+ return { shopperPage, shopperContext };
+};
diff --git a/tests/e2e-pw/utils/merchant.ts b/tests/e2e-pw/utils/merchant.ts
new file mode 100644
index 00000000000..0f358e24491
--- /dev/null
+++ b/tests/e2e-pw/utils/merchant.ts
@@ -0,0 +1,11 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+
+export const goToOrder = async (
+ page: Page,
+ orderId: string
+): Promise< void > => {
+ await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` );
+};
diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts
new file mode 100644
index 00000000000..e3f59b9ae02
--- /dev/null
+++ b/tests/e2e-pw/utils/shopper.ts
@@ -0,0 +1,116 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+
+/**
+ * Internal dependencies
+ */
+
+import { config, CustomerAddress } from '../config/default';
+
+export const fillBillingAddress = async (
+ page: Page,
+ billingAddress: CustomerAddress
+): Promise< void > => {
+ await page
+ .locator( '#billing_first_name' )
+ .fill( billingAddress.firstname );
+ await page.locator( '#billing_last_name' ).fill( billingAddress.lastname );
+ await page.locator( '#billing_company' ).fill( billingAddress.company );
+ await page
+ .locator( '#billing_country' )
+ .selectOption( billingAddress.country );
+ await page
+ .locator( '#billing_address_1' )
+ .fill( billingAddress.addressfirstline );
+ await page
+ .locator( '#billing_address_2' )
+ .fill( billingAddress.addresssecondline );
+ await page.locator( '#billing_city' ).fill( billingAddress.city );
+ await page.locator( '#billing_state' ).selectOption( billingAddress.state );
+ await page.locator( '#billing_postcode' ).fill( billingAddress.postcode );
+ await page.locator( '#billing_phone' ).fill( billingAddress.phone );
+ await page.locator( '#billing_email' ).fill( billingAddress.email );
+};
+
+export const placeOrder = async ( page: Page ): Promise< void > => {
+ await page.locator( '#place_order' ).click();
+};
+
+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'
+ )
+ ) {
+ const frameHandle = await page.waitForSelector(
+ '#payment .payment_method_woocommerce_payments .wcpay-upe-element iframe'
+ );
+
+ const stripeFrame = await frameHandle.contentFrame();
+
+ if ( ! stripeFrame ) return;
+
+ await stripeFrame.locator( '[name="number"]' ).fill( card.number );
+
+ await stripeFrame
+ .locator( '[name="expiry"]' )
+ .fill( card.expires.month + card.expires.year );
+
+ await stripeFrame.locator( '[name="cvc"]' ).fill( card.cvc );
+
+ const zip = stripeFrame.locator( '[name="postalCode"]' );
+
+ if ( await zip.isVisible() ) {
+ await zip.fill( '90210' );
+ }
+ } else {
+ const frameHandle = await page.waitForSelector(
+ '#payment #wcpay-card-element iframe[name^="__privateStripeFrame"]'
+ );
+ const stripeFrame = await frameHandle.contentFrame();
+
+ if ( ! stripeFrame ) return;
+
+ await stripeFrame.locator( '[name="cardnumber"]' ).fill( card.number );
+
+ await stripeFrame
+ .locator( '[name="exp-date"]' )
+ .fill( card.expires.month + card.expires.year );
+
+ await stripeFrame.locator( '[name="cvc"]' ).fill( card.cvc );
+ }
+};
+
+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.
+ const stripeFrame = page.frameLocator(
+ 'body>div>iframe[name^="__privateStripeFrame"]'
+ );
+ if ( ! stripeFrame ) return;
+
+ const challengeFrame = stripeFrame.frameLocator(
+ 'iframe[name="stripe-challenge-frame"]'
+ );
+ if ( ! challengeFrame ) return;
+
+ const button = challengeFrame.getByRole( 'button', {
+ name: authorize ? 'Complete' : 'Fail',
+ } );
+ await button.click();
+};
diff --git a/tests/e2e/env/docker-compose.yml b/tests/e2e/env/docker-compose.yml
index e42ce325921..c12211436e9 100644
--- a/tests/e2e/env/docker-compose.yml
+++ b/tests/e2e/env/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
wordpress:
build: ./wordpress-xdebug
@@ -19,6 +17,14 @@ services:
- ${WCP_ROOT}:/var/www/html/wp-content/plugins/woocommerce-payments
- ${E2E_ROOT}/deps/${DEV_TOOLS_DIR}:/var/www/html/wp-content/plugins/${DEV_TOOLS_DIR}
- ${E2E_ROOT}/deps/woocommerce-subscriptions:/var/www/html/wp-content/plugins/woocommerce-subscriptions
+ environment:
+ WORDPRESS_CONFIG_EXTRA: |
+ /* Dynamic WP hostname to allow Playwright container to access site via `host.docker.internal` hostname. */
+ /* `$$_` ensures `$_` is not escaped (https://github.com/docker-library/wordpress/pull/142#issuecomment-478561857) */
+ define('DOCKER_HOST', $$_SERVER['HTTP_X_ORIGINAL_HOST'] ?? $$_SERVER['HTTP_HOST'] ?? 'localhost');
+ define('DOCKER_REQUEST_URL', ( ! empty( $$_SERVER['HTTPS'] ) ? 'https://' : 'http://' ) . DOCKER_HOST);
+ define('WP_SITEURL', DOCKER_REQUEST_URL);
+ define('WP_HOME', DOCKER_REQUEST_URL);
db:
container_name: wcp_e2e_mysql
image: mariadb:10.5.8
diff --git a/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php b/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php
new file mode 100644
index 00000000000..83f08678023
--- /dev/null
+++ b/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php
@@ -0,0 +1,100 @@
+http_client_stub = $this->getMockBuilder( WC_Payments_Http::class )->disableOriginalConstructor()->setMethods( [ 'wpcom_json_api_request_as_user' ] )->getMock();
+ $this->controller = new WC_REST_Payments_Survey_Controller( $this->http_client_stub );
+ }
+
+ public function test_empty_request_returns_400_status_code() {
+ $request = new WP_REST_Request( 'POST', self::ROUTE . '/payments-overview' );
+
+ $response = $this->controller->submit_payments_overview_survey( $request );
+
+ $this->assertEquals( 400, $response->get_status() );
+ }
+
+ public function test_valid_request_forwards_data_to_jetpack() {
+ $this->http_client_stub
+ ->expects( $this->any() )
+ ->method( 'wpcom_json_api_request_as_user' )
+ ->with(
+ $this->stringContains( '/marketing/survey' ),
+ $this->anything(),
+ $this->anything(),
+ $this->logicalAnd(
+ $this->arrayHasKey( 'survey_id' ),
+ $this->arrayHasKey( 'survey_responses' ),
+ $this->callback(
+ function ( $argument ) {
+ return 'wcpay-payments-overview' === $argument['survey_id'];
+ }
+ ),
+ $this->callback(
+ function ( $argument ) {
+ return '4' === $argument['survey_responses']['rating'];
+ }
+ ),
+ $this->callback(
+ function ( $argument ) {
+ return 'test comment' === $argument['survey_responses']['comments']['text'];
+ }
+ )
+ )
+ )
+ ->willReturn(
+ [
+ 'body' => '{"err": ""}',
+ 'response' => [ 'code' => 200 ],
+ ]
+ );
+
+ $request = new WP_REST_Request( 'POST', self::ROUTE . '/payments-overview' );
+ $request->set_body_params(
+ [
+ 'rating' => '4',
+ 'comments' => 'test comment',
+ ]
+ );
+
+ $response = $this->controller->submit_payments_overview_survey( $request );
+
+ $this->assertEquals( 200, $response->get_status() );
+ }
+}
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index a73e0120e4a..4865390b965 100755
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -87,6 +87,7 @@ function () {
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-terminal-locations-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-tos-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-settings-controller.php';
+ require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-survey-controller.php';
require_once $_plugin_dir . 'includes/admin/tracks/class-tracker.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-reader-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-files-controller.php';
diff --git a/tests/unit/multi-currency/test-class-analytics.php b/tests/unit/multi-currency/test-class-analytics.php
index 75da9abafac..15fa5fd3f3a 100644
--- a/tests/unit/multi-currency/test-class-analytics.php
+++ b/tests/unit/multi-currency/test-class-analytics.php
@@ -109,28 +109,18 @@ public function woocommerce_filter_provider() {
];
}
+ /**
+ * Test for the register_customer_currencies method. Note that this function is called in the constructor,
+ * and the customerCurrencies data key cannot be re-registered, so this test is only to ensure that it exists.
+ */
public function test_register_customer_currencies() {
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_all_customer_currencies' )
- ->willReturn( $this->mock_customer_currencies );
-
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_available_currencies' )
- ->willReturn( $this->get_mock_available_currencies() );
-
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_default_currency' )
- ->willReturn( new Currency( 'USD', 1.0 ) );
-
- $this->analytics->register_customer_currencies();
-
$data_registry = Package::container()->get(
AssetDataRegistry::class
);
-
$this->assertTrue( $data_registry->exists( 'customerCurrencies' ) );
}
+
public function test_has_multi_currency_orders() {
// Use reflection to make the private method has_multi_currency_orders accessible.
@@ -143,30 +133,6 @@ public function test_has_multi_currency_orders() {
$this->assertTrue( $result );
}
- public function test_register_customer_currencies_for_empty_customer_currencies() {
- delete_option( MultiCurrency::CUSTOMER_CURRENCIES_KEY );
-
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_all_customer_currencies' )
- ->willReturn( [] );
-
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_available_currencies' )
- ->willReturn( $this->get_mock_available_currencies() );
-
- $this->mock_multi_currency->expects( $this->once() )
- ->method( 'get_default_currency' )
- ->willReturn( new Currency( 'USD', 1.0 ) );
-
- $this->analytics->register_customer_currencies();
-
- $data_registry = Package::container()->get(
- AssetDataRegistry::class
- );
-
- $this->assertTrue( $data_registry->exists( 'customerCurrencies' ) );
- }
-
public function test_update_order_stats_data_with_non_multi_currency_order() {
$args = $this->order_args_provider( 123, 0, 1, 15.50, 1.50, 0, 14.00 );
$order = wc_create_order();
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index 6193794361d..160343004bf 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -337,16 +337,7 @@ public function test_get_onboarding_business_types() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/business_types?test_mode=0',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/business_types?test_mode=0' ),
null,
true,
true // get_onboarding_business_types should use user token auth.
@@ -365,16 +356,7 @@ public function test_get_onboarding_required_verification_information() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/required_verification_information?test_mode=0&country=country&type=type',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/required_verification_information?test_mode=0&country=country&type=type' ),
null,
true,
true // get_onboarding_required_verification_information should use user token auth.
@@ -432,16 +414,7 @@ public function test_get_currency_rates() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/currency/rates?test_mode=0¤cy_from=USD',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/currency/rates?test_mode=0¤cy_from=USD' ),
null,
true,
false
@@ -581,16 +554,7 @@ public function test_delete_terminal_location_success() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/terminal/locations/tml_XXXXXXX?test_mode=0',
- 'method' => 'DELETE',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/terminal/locations/tml_XXXXXXX?test_mode=0' ),
null,
true,
false
@@ -751,16 +715,7 @@ public function test_cancel_subscription() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/subscriptions/sub_test?test_mode=0',
- 'method' => 'DELETE',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/subscriptions/sub_test?test_mode=0' ),
null,
true,
false
+ Your feedback will be only be shared with WooCommerce and treated pursuant to our + + privacy policy + + . +
+