Merge branch 'master' into add/composer-bin-plugin

This commit is contained in:
Christopher Allford 2020-10-06 10:13:47 -07:00
commit b3da18a14b
78 changed files with 11199 additions and 10528 deletions

View File

@ -272,16 +272,16 @@
},
{
"name": "symfony/finder",
"version": "v3.4.44",
"version": "v3.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "5ec813ccafa8164ef21757e8c725d3a57da59200"
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/5ec813ccafa8164ef21757e8c725d3a57da59200",
"reference": "5ec813ccafa8164ef21757e8c725d3a57da59200",
"url": "https://api.github.com/repos/symfony/finder/zipball/52140652ed31cee3dabd0c481b5577201fa769b4",
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4",
"shasum": ""
},
"require": {
@ -331,7 +331,7 @@
"type": "tidelift"
}
],
"time": "2020-02-14T07:34:21+00:00"
"time": "2020-09-02T16:06:40+00:00"
},
{
"name": "wp-cli/i18n-command",

View File

@ -4,8 +4,8 @@ PROTECTED_BRANCH="master"
REMOTE_REF=$(echo "$HUSKY_GIT_STDIN" | cut -d " " -f 3)
if [ -n "$REMOTE_REF" ]; then
if [ "refs/heads/${PROTECTED_BRANCH}" == "$REMOTE_REF" ]; then
if [ "$TERM" == "dumb" ]; then
if [ "refs/heads/${PROTECTED_BRANCH}" = "$REMOTE_REF" ]; then
if [ "$TERM" = "dumb" ]; then
>&2 echo "Sorry, you are unable to push to master using a GUI client! Please use git CLI."
exit 1
fi

View File

@ -1,57 +1,5 @@
== Changelog ==
= 4.6.0 - 2020-09-22 =
**WooCommerce**
* Tweak - Removed the "Features" settings page now that the WooCommerce Admin dashboard is enabled by default. #27047
* Tweak - Deprecate old setup wizard. #26853
* Tweak - Add WC pages when the plugin is activated now that the old setup wizard was deprecated. #26853
* Fix - When adding a grouped product to the cart, quantity is passed through the `woocommerce_stock_amount` filter like it's done for simple and variable products. #27219
* Fix - Several style improvements to notices in theme Twenty Twenty. #27387
* Fix - Fixed countries list sorting and added support to PHP internationalization functions. #27416
* Fix - Remove "There are no notes yet" after adding the first one by AJAX. #27449
* Fix - Fix PHP docblock summary to properly reflect the role of `wc_get_coupon_id_by_code` function. #27453
* Fix - Fix white space character in add-to-cart script. #27459
* Fix - Adding missing css property to email templates to ensure header background color is less likely to be overridden by email client. #27525
* Fix - Encapsulate scope of `c` variable to avoid polluting the global scope and potentially causing problems if other plugins use the same variable name. #27610
* Fix - Fix bug when using tax classes with some non-ASCII characters. #27615
* Fix - Paypal gateway: protect code against a fatal error if WooCommerce is unable to communicate with PayPal. #27616
* Fix - Added WP environment type to tracker. #27685
* Fix - Prevent fatal errors when trying to determine what is the product type taxonomy term. #27441
* Fix - Typo in the error message shown in the customizer. #27008
* Dev - Introduced `woocommerce_return_to_shop_text` filter. #25419
* Dev - Introduced `woocommerce_cart_item_required_stock_is_not_enough` filter. #26196
* Dev - Changed relative `include` paths to absolute `include` paths using `__DIR__`. #27433
* Dev - Fixed duplicated use of `woocommerce_add_$notice_type`. #27437
* Dev - Allow public access to core capabilities for other plugins. #26976
* Dev - Add `woocommerce_should_send_no_stock_notification` filter. #27634
* Dev - New action 'woocommerce_after_order_details' added in a order-details.php template. #26327
* Localization - Remove duplicated entry for Cyprus when listing countries by continent. #27636
* Localization - Add Egypt regions. #27495
* Localization - Updated name for the Hungarian county called Csongrád-Csanád. #27075
* Localization - Adding states for Benin country. #27217
* Localization - Add KR locale info. #27496
* Localization - Fixed the name of the Spain state of `Biscay`. #27548
**WooCommerce Admin - 1.6.0**
**WooCommerce Blocks - 3.2.0, 3.3.0 and 3.4.0**
- Fix an undefined variable PHP notice related to Product REST API. [#2962](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/2962)
- Fixed an issue that was making some blocks not to render correctly in the Empty cart template. [#2904](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/2904)
- Fixes a styling issue in the Product Search block in the editor. [#3014](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3014)
- Improved focus styles of error states on form elements. [#2974](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/2974)
- Deprecate wc.wcSettings.setSetting function. [#3010](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3010)
- Improve behaviour of draft order cleanup to account for clobbered custom shop order status. [#2912](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/2912)
- Fixed styling options of the Product Title block (in All Products). [3095](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3095)
- Fix product reviews schema date fields to use new (WP 5.5) `date-time` format. ([3109](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3109))
- Use wp_login_url instead of hardcoding login path. ([3090](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3090))
- Create DebouncedValidatedTextInput component. ([3108](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3108))
- Merge ProductPrice atomic block and component. ([3065](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3065))
= 4.5.2 - 2020-09-14 =
* Fix - Revert the changes in filtering by attribute that were introduced in WooCommerce 4.4. #27625
* Fix - Adjusted validation to allow for variations with "0" as an attribute value. #27633

View File

@ -15,7 +15,7 @@
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.6.0-beta.1",
"woocommerce/woocommerce-admin": "1.6.0-rc.3",
"woocommerce/woocommerce-blocks": "3.4.0"
},
"require-dev": {

28
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b09bb963fa7ee03c3e4a4ae64b1758aa",
"content-hash": "e4896528efe6b080c014bd55b60d4903",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -380,7 +380,7 @@
},
{
"name": "symfony/css-selector",
"version": "v3.4.44",
"version": "v3.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@ -429,6 +429,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-16T08:31:04+00:00"
},
{
@ -468,16 +482,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "1.6.0-beta.1",
"version": "1.6.0-rc.3",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "a03cafd0a218451d83c42285b02f797555a7450e"
"reference": "a2d0e41675f9c44d49e02fe6ed44310987a4ce88"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/a03cafd0a218451d83c42285b02f797555a7450e",
"reference": "a03cafd0a218451d83c42285b02f797555a7450e",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/a2d0e41675f9c44d49e02fe6ed44310987a4ce88",
"reference": "a2d0e41675f9c44d49e02fe6ed44310987a4ce88",
"shasum": ""
},
"require": {
@ -511,7 +525,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-09-18T15:24:50+00:00"
"time": "2020-10-01T13:28:06+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",

View File

@ -6,7 +6,7 @@
*
* @package WooCommerce\Admin
* @version 2.6.0
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
use Automattic\Jetpack\Constants;
@ -61,19 +61,19 @@ class WC_Admin_Setup_Wizard {
/**
* Hook in tabs.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function __construct() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
* Add admin menus/screens.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function admin_menus() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
add_dashboard_page( '', '', 'manage_options', 'wc-setup', '' );
}
@ -81,11 +81,11 @@ class WC_Admin_Setup_Wizard {
* The theme "extra" should only be shown if the current user can modify themes
* and the store doesn't already have a WooCommerce theme.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_theme() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$support_woocommerce = current_theme_supports( 'woocommerce' ) && ! wc_is_wp_default_theme_active();
return (
@ -100,10 +100,10 @@ class WC_Admin_Setup_Wizard {
* The "automated tax" extra should only be shown if the current user can
* install plugins and the store is in a supported country.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function should_show_automated_tax() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
@ -122,11 +122,11 @@ class WC_Admin_Setup_Wizard {
* Should we show the MailChimp install option?
* True only if the user can install plugins.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_mailchimp() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return current_user_can( 'install_plugins' );
}
@ -135,11 +135,11 @@ class WC_Admin_Setup_Wizard {
* True only if the user can install plugins,
* and up until the end date of the recommendation.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_facebook() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return current_user_can( 'install_plugins' );
}
@ -147,11 +147,11 @@ class WC_Admin_Setup_Wizard {
* Is the WooCommerce Admin actively included in the WooCommerce core?
* Based on presence of a basic WC Admin function.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function is_wc_admin_active() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return function_exists( 'wc_admin_url' );
}
@ -162,11 +162,11 @@ class WC_Admin_Setup_Wizard {
*
* @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_wc_admin() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' );
return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active();
}
@ -174,11 +174,11 @@ class WC_Admin_Setup_Wizard {
/**
* Should we show the new WooCommerce Admin onboarding experience?
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_wc_admin_onboarding() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
// As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package.
// This filter will allow for forcing the old wizard while we migrate e2e tests.
return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false );
@ -188,11 +188,11 @@ class WC_Admin_Setup_Wizard {
* Should we display the 'Recommended' step?
* True if at least one of the recommendations will be displayed.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
protected function should_show_recommended_step() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return $this->should_show_theme()
|| $this->should_show_automated_tax()
|| $this->should_show_mailchimp()
@ -205,19 +205,19 @@ class WC_Admin_Setup_Wizard {
*
* Hooked onto 'admin_enqueue_scripts'.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function enqueue_scripts() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
* Show the setup wizard.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function setup_wizard() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok.
return;
}
@ -304,11 +304,11 @@ class WC_Admin_Setup_Wizard {
* Admin URL if it's the last step.
* Empty string on failure.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @since 3.0.0
*/
public function get_next_step_link( $step = '' ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
if ( ! $step ) {
$step = $this->step;
}
@ -329,10 +329,10 @@ class WC_Admin_Setup_Wizard {
/**
* Setup Wizard Header.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function setup_wizard_header() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
// same as default WP from wp-admin/admin-header.php.
$wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) );
@ -357,10 +357,10 @@ class WC_Admin_Setup_Wizard {
/**
* Setup Wizard Footer.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function setup_wizard_footer() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$current_step = $this->step;
?>
<?php if ( 'new_onboarding' === $current_step || 'store-setup' === $current_step ) : ?>
@ -377,10 +377,10 @@ class WC_Admin_Setup_Wizard {
/**
* Output the steps.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function setup_wizard_steps() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$output_steps = $this->steps;
$selected_features = array_filter( $this->wc_setup_activate_get_feature_list() );
@ -422,10 +422,10 @@ class WC_Admin_Setup_Wizard {
/**
* Output the content for the current step.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function setup_wizard_content() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
echo '<div class="wc-setup-content">';
if ( ! empty( $this->steps[ $this->step ]['view'] ) ) {
call_user_func( $this->steps[ $this->step ]['view'], $this );
@ -436,10 +436,10 @@ class WC_Admin_Setup_Wizard {
/**
* Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_new_onboarding() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
?>
<div class="wc-setup-step__new_onboarding-wrapper">
<p class="wc-setup-step__new_onboarding-welcome"><?php esc_html_e( 'Welcome to', 'woocommerce' ); ?></p>
@ -463,10 +463,10 @@ class WC_Admin_Setup_Wizard {
/**
* Installs WooCommerce admin and redirects to the new onboarding experience.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_new_onboarding_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
@ -474,7 +474,7 @@ class WC_Admin_Setup_Wizard {
* Location, product type, page setup, and tracking opt-in.
*/
public function wc_setup_store_setup() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$address = WC()->countries->get_base_address();
$address_2 = WC()->countries->get_base_address_2();
$city = WC()->countries->get_base_city();
@ -604,10 +604,10 @@ class WC_Admin_Setup_Wizard {
/**
* Template for the usage tracking modal.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function tracking_modal() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
?>
<script type="text/template" id="tmpl-wc-modal-tracking-setup">
<div class="wc-backbone-modal woocommerce-tracker">
@ -655,10 +655,10 @@ class WC_Admin_Setup_Wizard {
/**
* Save initial store settings.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_store_setup_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
@ -667,7 +667,7 @@ class WC_Admin_Setup_Wizard {
* @see https://core.trac.wordpress.org/ticket/41358 .
*/
protected function close_http_connection() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
// Only 1 PHP process can access a session object at a time, close this so the next request isn't kept waiting.
// @codingStandardsIgnoreStart
if ( session_id() ) {
@ -696,10 +696,10 @@ class WC_Admin_Setup_Wizard {
* @see WC_Admin_Setup_Wizard::install_plugin
* @see WC_Admin_Setup_Wizard::install_theme
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function run_deferred_actions() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$this->close_http_connection();
foreach ( $this->deferred_actions as $action ) {
$action['func']( ...$action['args'] );
@ -721,10 +721,10 @@ class WC_Admin_Setup_Wizard {
* @param string $plugin_id Plugin id used for background install.
* @param array $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function install_plugin( $plugin_id, $plugin_info ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
// Make sure we don't trigger multiple simultaneous installs.
if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) {
return;
@ -757,10 +757,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $theme_id Theme id used for background install.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function install_theme( $theme_id ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
if ( empty( $this->deferred_actions ) ) {
add_action( 'shutdown', array( $this, 'run_deferred_actions' ) );
}
@ -776,10 +776,10 @@ class WC_Admin_Setup_Wizard {
/**
* Helper method to install Jetpack.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function install_jetpack() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$this->install_plugin(
'jetpack',
array(
@ -792,10 +792,10 @@ class WC_Admin_Setup_Wizard {
/**
* Helper method to install WooCommerce Services and its Jetpack dependency.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function install_woocommerce_services() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$this->install_jetpack();
$this->install_plugin(
'woocommerce-services',
@ -809,11 +809,11 @@ class WC_Admin_Setup_Wizard {
/**
* Retrieve info for missing WooCommerce Services and/or Jetpack plugin.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return array
*/
protected function get_wcs_requisite_plugins() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$plugins = array();
if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) {
$plugins[] = array(
@ -833,10 +833,10 @@ class WC_Admin_Setup_Wizard {
/**
* Plugin install info message markup with heading.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function plugin_install_info() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
?>
<span class="plugin-install-info">
<span class="plugin-install-info-label"><?php esc_html_e( 'The following plugins will be installed and activated for you:', 'woocommerce' ); ?></span>
@ -851,11 +851,11 @@ class WC_Admin_Setup_Wizard {
* @param string $country_code Country code.
* @param string $currency_code Currency code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return array
*/
protected function get_wizard_shipping_methods( $country_code, $currency_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$shipping_methods = array(
'flat_rate' => array(
'name' => __( 'Flat Rate', 'woocommerce' ),
@ -885,10 +885,10 @@ class WC_Admin_Setup_Wizard {
* @param string $currency_code Currency code.
* @param string $input_prefix Input prefix.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$selected = 'flat_rate';
$shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code );
?>
@ -945,11 +945,11 @@ class WC_Admin_Setup_Wizard {
/**
* Render a product weight unit dropdown.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return string
*/
protected function get_product_weight_selection() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$weight_unit = get_option( 'woocommerce_weight_unit' );
ob_start();
?>
@ -969,11 +969,11 @@ class WC_Admin_Setup_Wizard {
/**
* Render a product dimension unit dropdown.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return string
*/
protected function get_product_dimension_selection() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$dimension_unit = get_option( 'woocommerce_dimension_unit' );
ob_start();
?>
@ -994,10 +994,10 @@ class WC_Admin_Setup_Wizard {
/**
* Shipping.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_shipping() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$country_code = WC()->countries->get_base_country();
$country_name = WC()->countries->countries[ $country_code ];
$prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name;
@ -1163,10 +1163,10 @@ class WC_Admin_Setup_Wizard {
/**
* Save shipping options.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_shipping_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
@ -1175,10 +1175,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_stripe_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$stripe_supported_countries = array(
'AU',
'AT',
@ -1212,10 +1212,10 @@ class WC_Admin_Setup_Wizard {
* @param string $currency Currency code.
* @return boolean
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_paypal_supported_currency( $currency ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_currencies = array(
'AUD',
'BRL',
@ -1253,10 +1253,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_klarna_checkout_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_countries = array(
'SE', // Sweden.
'FI', // Finland.
@ -1271,10 +1271,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_klarna_payments_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_countries = array(
'DK', // Denmark.
'DE', // Germany.
@ -1288,10 +1288,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_square_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$square_supported_countries = array(
'US',
'CA',
@ -1307,10 +1307,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_eway_payments_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_countries = array(
'AU', // Australia.
'NZ', // New Zealand.
@ -1323,10 +1323,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_shipstation_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_countries = array(
'AU', // Australia.
'CA', // Canada.
@ -1340,10 +1340,10 @@ class WC_Admin_Setup_Wizard {
*
* @param string $country_code Country code.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function is_wcs_shipping_labels_supported_country( $country_code ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$supported_countries = array(
'US', // United States.
);
@ -1353,11 +1353,11 @@ class WC_Admin_Setup_Wizard {
/**
* Helper method to retrieve the current user's email address.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return string Email address
*/
protected function get_current_user_email() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$current_user = wp_get_current_user();
$user_email = $current_user->user_email;
@ -1367,11 +1367,11 @@ class WC_Admin_Setup_Wizard {
/**
* Array of all possible "in cart" gateways that can be offered.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return array
*/
protected function get_wizard_available_in_cart_payment_gateways() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$user_email = $this->get_current_user_email();
$stripe_description = '<p>' . sprintf(
@ -1513,11 +1513,11 @@ class WC_Admin_Setup_Wizard {
/**
* Simple array of "in cart" gateways to show in wizard.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return array
*/
public function get_wizard_in_cart_payment_gateways() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$gateways = $this->get_wizard_available_in_cart_payment_gateways();
$country = WC()->countries->get_base_country();
$currency = get_woocommerce_currency();
@ -1583,11 +1583,11 @@ class WC_Admin_Setup_Wizard {
/**
* Simple array of "manual" gateways to show in wizard.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return array
*/
public function get_wizard_manual_payment_gateways() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$gateways = array(
'cheque' => array(
'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ),
@ -1618,10 +1618,10 @@ class WC_Admin_Setup_Wizard {
* @param int $item_id Item ID.
* @param array $item_info Item info array.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function display_service_item( $item_id, $item_info ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$item_class = 'wc-wizard-service-item';
if ( isset( $item_info['class'] ) ) {
$item_class .= ' ' . $item_info['class'];
@ -1725,11 +1725,11 @@ class WC_Admin_Setup_Wizard {
*
* @param array $service Service info array.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
public function is_featured_service( $service ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return ! empty( $service['featured'] );
}
@ -1738,21 +1738,21 @@ class WC_Admin_Setup_Wizard {
*
* @param array $service Service info array.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
* @return boolean
*/
public function is_not_featured_service( $service ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return ! $this->is_featured_service( $service );
}
/**
* Payment Step.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_payment() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) );
$in_cart_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) );
$manual_gateways = $this->get_wizard_manual_payment_gateways();
@ -1825,14 +1825,14 @@ class WC_Admin_Setup_Wizard {
/**
* Payment Step save.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_payment_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
protected function display_recommended_item( $item_info ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$type = $item_info['type'];
$title = $item_info['title'];
$description = $item_info['description'];
@ -1872,10 +1872,10 @@ class WC_Admin_Setup_Wizard {
/**
* Recommended step
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_recommended() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
?>
<h1><?php esc_html_e( 'Recommended for All WooCommerce Stores', 'woocommerce' ); ?></h1>
<p>
@ -1956,17 +1956,17 @@ class WC_Admin_Setup_Wizard {
/**
* Recommended step save.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_recommended_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
* Go to the next step if Jetpack was connected.
*/
protected function wc_setup_activate_actions() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
if (
isset( $_GET['from'] ) &&
'wpcom' === $_GET['from'] &&
@ -1980,7 +1980,7 @@ class WC_Admin_Setup_Wizard {
/**
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function wc_setup_activate_get_feature_list() {
$features = array();
@ -2003,10 +2003,10 @@ class WC_Admin_Setup_Wizard {
/**
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function wc_setup_activate_get_feature_list_str() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$features = $this->wc_setup_activate_get_feature_list();
if ( $features['payment'] && $features['taxes'] && $features['labels'] ) {
return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' );
@ -2029,10 +2029,10 @@ class WC_Admin_Setup_Wizard {
/**
* Activate step.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_activate() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$this->wc_setup_activate_actions();
$jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active();
@ -2173,10 +2173,10 @@ class WC_Admin_Setup_Wizard {
/**
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function get_all_activate_errors() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
return array(
'default' => __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ),
'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ),
@ -2187,10 +2187,10 @@ class WC_Admin_Setup_Wizard {
/**
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
protected function get_activate_error_message( $code = '' ) {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
$errors = $this->get_all_activate_errors();
return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default'];
}
@ -2200,19 +2200,19 @@ class WC_Admin_Setup_Wizard {
*
* Install, activate, and launch connection flow for Jetpack.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_activate_save() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
}
/**
* Final step.
*
* @deprecated 4.5.0
* @deprecated 4.6.0
*/
public function wc_setup_ready() {
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.5.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) );
_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' );
// We've made it! Don't prompt the user to run the wizard again.
WC_Admin_Notices::remove_notice( 'install', true );

View File

@ -304,6 +304,7 @@ class WC_Install {
self::create_cron_jobs();
self::create_files();
self::maybe_create_pages();
self::maybe_set_activation_transients();
self::update_wc_version();
self::maybe_update_db_version();
@ -403,6 +404,17 @@ class WC_Install {
return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' );
}
/**
* See if we need to set redirect transients for activation or not.
*
* @since 4.6.0
*/
private static function maybe_set_activation_transients() {
if ( self::is_new_install() ) {
set_transient( '_wc_activation_redirect', 1, 30 );
}
}
/**
* See if we need to show or run database updates during install.
*

View File

@ -234,9 +234,9 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
if ( ! $parent ) {
return new WP_Error(
// Translators: %d parent ID.
"woocommerce_rest_{$this->post_type}_invalid_parent", sprintf( __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), $variation->get_parent_id() ), array(
'status' => 404,
)
"woocommerce_rest_{$this->post_type}_invalid_parent",
__( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ),
array( 'status' => 404 )
);
}
@ -354,16 +354,21 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
protected function set_variation_image( $variation, $image ) {
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
if ( 0 === $attachment_id && isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( 0 === $attachment_id ) {
if ( isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) {
throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) {
throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 );
}
}
}
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() );
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() );
} else {
$variation->set_image_id( '' );
return $variation;
}
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {

13952
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@
"@typescript-eslint/parser": "3.1.0",
"@woocommerce/e2e-environment": "file:tests/e2e/env",
"@woocommerce/e2e-utils": "file:tests/e2e/utils",
"@woocommerce/model-factories": "file:tests/e2e/factories",
"@woocommerce/api": "file:tests/e2e/api",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-preset-default": "3.0.2",
"@wordpress/e2e-test-utils": "4.6.0",

View File

@ -5,7 +5,8 @@ Automated end-to-end tests for WooCommerce.
## Table of contents
- [Pre-requisites](#pre-requisites)
- [Install NodeJS](#install-nodejs)
- [Install Node.js](#install-node.js)
- [Install NVM](#install-nvm)
- [Install Docker](#install-docker)
- [Configuration](#configuration)
- [Test Environment](#test-environment)
@ -17,21 +18,19 @@ Automated end-to-end tests for WooCommerce.
- [How to run tests in non-headless mode](#how-to-run-tests-in-non-headless-mode)
- [How to run an individual test](#how-to-run-an-individual-test)
- [How to skip tests](#how-to-skip-tests)
- [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress,-php-and-mariadb-versions)
- [Writing tests](#writing-tests)
- [Debugging tests](#debugging-tests)
- [Docker basics](#docker-basics)
- [How to stop and restart Docker](#how-to-stop-and-restart-docker)
- [How to stop Docker and do a clean restart](#how-to-stop-docker-and-do-a-clean-restart)
## Pre-requisites
### Install NodeJS
### Install Node.js
Install NodeJS on Mac:
Follow [instructions on the node.js site](https://nodejs.org/en/download/) to install Node.js.
```bash
brew install node
```
### Install NVM
Follow instructions in the [NVM repository](https://github.com/nvm-sh/nvm) to install NVM.
### Install Docker
@ -88,35 +87,45 @@ Setup Wizard e2e test (located in `activate-and-setup` directory) will run befor
- `git checkout master` or checkout the branch where you need to run tests
- Run `npm install`
- Run `nvm use`
- Run `npm install jest --global`
- Run `npm install`
- Run `composer install --no-dev`
- Run `npm run build:core`
- Run `npm run build:assets`
- Run the following command to build the test site using Docker: `npm run docker:up` and watch the site being built. Note that it may take a few minutes the first time you do that. The process is considered completed when the messages letting you know that WordPress was installed, WooCommerce was activated and users created will be displayed:
- Run `npm install jest --global`
- Run `npm run docker:up` - it will build the test site using Docker.
- Run `docker ps` - to confirm that the Docker containers were built and running. You should see the log that looks similar to below indicating that everything had been built as expected:
```
wordpress-cli_1 | Success: WordPress installed successfully.
wordpress-cli_1 | Plugin 'woocommerce' activated.
wordpress-cli_1 | Success: Activated 1 of 1 plugins.
wordpress-cli_1 | Success: Created user 2.
woocommerce_wordpress-cli_1 exited with code 0
woocommerce_wordpress-cli_1 exited with code 0
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c380e1964506 env_wordpress-cli "entrypoint.sh" 7 seconds ago Up 5 seconds woocommerce_wordpress-cli
2ab8e8439e9f wordpress:5.5.1 "docker-entrypoint.s…" 8 seconds ago Up 7 seconds 0.0.0.0:8084->80/tcp woocommerce_wordpress-www
4c1e3f2a49db mariadb:10.5.5 "docker-entrypoint.s…" 10 seconds ago Up 8 seconds 3306/tcp woocommerce_db
```
For more Docker commands, scroll down to [Docker basics](#docker-basics).
Note that by default, Docker will download the latest images available for WordPress, PHP and MariaDB. In the example above, you can see that WordPress 5.5.1 and MariaDB 10.5.5 were used.
- Open new terminal window and `cd` to the current branch again.
See [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress,-php-and-mariadb-versions) if you'd like to use different versions.
- Run the following command to make sure the containers were built and running: `docker ps`. You should see the 2 following containers:
- Navigate to `http://localhost:8084/`
- `woocommerce_wordpress-woocommerce-dev`
- `mariadb:10.4`
If everything went well, you should be able to access the site. If you changed the port to something other than `8084` as per [Test Variables](#test-variables) section, use the appropriate port to access your site.
- Navigate to `http://localhost:8084/`. If everything went well, you should be able to access the site.
As noted in [Test Variables](#test-variables) section, use the following Admin user details to login:
```
Username: admin
PW: password
```
- Run `npm run docker:down` when you are done with running e2e tests or when making any changes to test suite.
Note that running `npm run docker:down` and then `npm run docker:up` re-initializes the test container.
### How to run tests in headless mode
@ -134,7 +143,20 @@ Tests are being run headless by default. However, sometimes it's useful to obser
npm run test:e2e-dev
```
The dev mode also enables SlowMo mode. SlowMo slows down Puppeteers operations so we can better see what is happening in the browser. You can adjust the SlowMo value by copying `/tests/e2e/env/config/jest.puppetee.config.js` to `/tests/e2e/config` and editing the value in that file. The default `PUPPETEER_SLOWMO=50` means test actions will be slowed down by 50 milliseconds.
The dev mode also enables SlowMo mode. SlowMo slows down Puppeteers operations so we can better see what is happening in the browser.
By default, SlowMo mode is set to slow down running of tests by 50 milliseconds. If you'd like to override it and have the tests run faster or slower in the `-dev` mode, pass `PUPPETEER_SLOWMO` variable when running tests as shown below:
```
PUPPETEER_SLOWMO=10 npm run test:e2e-dev
```
The faster you want the tests to run, the lower the value should be of `PUPPETEER_SLOWMO` should be.
For example:
- `PUPPETEER_SLOWMO=10` - will run tests faster
- `PUPPETEER_SLOWMO=70` - will run tests slower
### How to run an individual test
@ -190,6 +212,20 @@ Finally, you can apply both `.only` and `.skip` to `describe` part of the test:
describe.skip( 'Store owner can go through store Setup Wizard', () => {}
```
### How to run tests using custom WordPress, PHP and MariaDB versions
The following variables can be used to specify the versions of WordPress, PHP and MariaDB that you'd like to use to built your test site with Docker:
- `WP_VERSION`
- `TRAVIS_PHP_VERSION`
- `TRAVIS_MARIADB_VERSION`
The full command to build the site will look as follows:
```
TRAVIS_MARIADB_VERSION=10.5.3 TRAVIS_PHP_VERSION=7.4.5 WP_VERSION=5.4.1 npm run docker:up
```
## Writing tests
We use the following tools to write e2e tests:
@ -208,11 +244,3 @@ The following packages are used to write tests:
## Debugging tests
For Puppeteer debugging, follow [Google's documentation](https://developers.google.com/web/tools/puppeteer/debugging).
## Docker basics
### How to stop and restart Docker
- Press `Ctrl+C` in the terminal window where the containers are running
- Stop the container(s) using the following command: `npm run docker:down`
- Restart the containers using the following command: `npm run docker:up`

View File

@ -10,13 +10,22 @@ module.exports = {
rules: {
'no-unused-vars': 'off',
'no-dupe-class-members': 'off',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 2,
},
'plugins': [
'@typescript-eslint'
],
extends: [
'plugin:@wordpress/eslint-plugin/recommended-with-formatting'
],
overrides: [
{
'files': [ '**/*.ts' ]
'files': [
'**/*.js',
'**/*.ts'
]
},
{
'files': [

77
tests/e2e/api/README.md Normal file
View File

@ -0,0 +1,77 @@
# WooCommerce API Client
An isometric API client for interacting with WooCommerce installations. Here are the current and planned
features:
- [x] TypeScript Definitions
- [x] Axios API Client with support for OAuth & basic auth
- [x] Repositories to simplify interaction with basic data types
- [ ] Service classes for common activities such as changing settings
## Usage
```bash
npm install @woocommerce/api --save-dev
```
Depending on what you're intending to get out of the API client there are a few different ways of using it.
### REST API
The simplest way to use the client is directly:
```javascript
import { HTTPClientFactory } from '@woocommerce/api';
// You can create an API client using the client factory with pre-configured middleware for convenience.
let httpClient = HTTPClientFactory.withBasicAuth(
// The base URL of your REST API.
'https://example.com/wp-json/',
// The username for your WordPress user.
'username',
// The password for your WordPress user.
'password',
);
// You can also create an API client configured for requests using OAuth.
httpClient = HTTPClientFactory.withOAuth(
// The base URL of your REST API.
'https://example.com/wp-json/',
// The OAuth API Key's consumer secret.
'consumer_secret',
// The OAuth API Key's consumer password.
'consumer_pasword',
);
// You can then use the client to make API requests.
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
// Access the status code from the response.
response.statusCode;
// Access the headers from the response.
response.headers;
// Access the data from the response, in this case, the products.
response.data;
} );
```
### Repositories
As a convenience utility we've created repositories for core data types that can simplify interacting with the API.
These repositories provide CRUD methods for ease-of-use:
```javascript
import { SimpleProduct } from '@woocommerce/api';
// Prepare the HTTP client that will be consumed by the repository.
// This is necessary so that it can make requests to the REST API.
const httpClient = HTTPClientFactory.withBasicAuth( 'https://example.com/wp-json/','username','password' );
const repository = SimpleProduct.restRepository( httpClient );
// The repository can now be used to create models.
const product = repository.create( { name: 'Simple Product', regularPrice: '9.99' } );
// The response will be one of the models with structured properties and TypeScript support.
product.id;
```

View File

@ -1,5 +1,6 @@
module.exports = {
preset: 'ts-jest',
rootDir: 'src',
testEnvironment: 'node',
testPathIgnorePatterns: [ '/node_modules/', '/dist/' ],
};

4933
tests/e2e/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{
"name": "@woocommerce/model-factories",
"name": "@woocommerce/api",
"version": "0.1.0",
"author": "Automattic",
"description": "A simple interface for generating models of different types.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/factories/README.md",
"description": "A simple interface for interacting with a WooCommerce installation.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/api/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
@ -17,34 +17,33 @@
"types": "dist/index.d.ts",
"files": [
"/dist/",
"!*.ts.map",
"!*.tsbuildinfo",
"!*.spec.js",
"!*.spec.d.ts",
"!*.test.js",
"!*.test.d.ts"
"!/dist/**/__tests__/",
"!/dist/**/__mocks__/",
"!/dist/**/__snapshops__/"
],
"sideEffects": false,
"scripts": {
"test": "jest",
"clean": "rm -rf ./dist ./tsconfig.tsbuildinfo",
"compile": "tsc -b",
"build": "npm run clean && npm run compile",
"prepare": "npm run build"
"prepare": "npm run build",
"lint": "eslint src",
"test": "jest"
},
"dependencies": {
"axios": "0.19.2",
"create-hmac": "1.1.7",
"faker": "4.1.0",
"fishery": "1.0.0",
"oauth-1.0a": "2.2.6"
},
"devDependencies": {
"@types/create-hmac": "1.1.0",
"@types/faker": "4.1.12",
"@types/jest": "25.2.1",
"@types/moxios": "0.4.9",
"@types/moxios": "^0.4.9",
"@types/node": "13.13.5",
"jest": "25.5.4",
"jest-mock-extended": "^1.0.10",
"moxios": "0.4.0",
"ts-jest": "25.5.0",
"typescript": "3.8.3"

View File

@ -0,0 +1,76 @@
import { Model } from '../../models/model';
import { ModelRepository } from '../model-repository';
class DummyModel extends Model {
public name: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
describe( 'ModelRepository', () => {
it( 'should create', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( callback, null, null, null );
const created = await repository.create( { name: 'test' } );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { name: 'test' } );
} );
it( 'should throw error on create without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.create( { name: 'test' } ) ).toThrowError( /not supported/i );
} );
it( 'should read', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, callback, null, null );
const created = await repository.read( 1 );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1 );
} );
it( 'should throw error on read without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.read( 1 ) ).toThrowError( /not supported/i );
} );
it( 'should update', async () => {
const model = new DummyModel();
const callback = jest.fn().mockResolvedValue( model );
const repository = new ModelRepository< DummyModel >( null, null, callback, null );
const updated = await repository.update( 1, { name: 'new-name' } );
expect( updated ).toBe( model );
expect( callback ).toHaveBeenCalledWith( 1, { name: 'new-name' } );
} );
it( 'should throw error on update without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.update( 1, { name: 'new-name' } ) ).toThrowError( /not supported/i );
} );
it( 'should delete', async () => {
const callback = jest.fn().mockResolvedValue( true );
const repository = new ModelRepository< DummyModel >( null, null, null, callback );
const success = await repository.delete( 1 );
expect( success ).toBe( true );
expect( callback ).toHaveBeenCalledWith( 1 );
} );
it( 'should throw error on delete without callback', () => {
const repository = new ModelRepository< DummyModel >( null, null, null, null );
expect( () => repository.delete( 1 ) ).toThrowError( /not supported/i );
} );
} );

View File

@ -0,0 +1,197 @@
import { Model } from '../models/model';
/**
* A callback for creating a model using a data source.
*
* @callback CreateFn
* @param {Object} properties The properties of the model to create.
* @return {Promise.<Model>} Resolves to the created model.
*/
export type CreateFn< T > = ( properties: Partial< T > ) => Promise< T >;
/**
* A callback for reading a model using a data source.
*
* @callback ReadFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the read model.
*/
export type ReadFn< IDParam, T > = ( id: IDParam ) => Promise< T >;
/**
* A callback for updating a model using a data source.
*
* @callback UpdateFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<Model>} Resolves to the updated model.
*/
export type UpdateFn< IDParam, T > = ( id: IDParam, properties: Partial< T > ) => Promise< T >;
/**
* A callback for deleting a model from a data source.
*
* @callback DeleteFn
* @param {number|Object} id The ID or object used to find the model.
* @return {Promise.<boolean>} Resolves to true once the model has been deleted.
*/
export type DeleteFn< IDParam > = ( id: IDParam ) => Promise< boolean >;
/**
* An interface for repositories that can create models.
*
* @typedef CreatesModels
* @property {CreateFn} create Creates a model using the repository.
*/
export interface CreatesModels< T extends Model > {
create( properties: Partial< T > ): Promise< T >;
}
/**
* An interface for repositories that can read models.
*
* @typedef ReadsModels
* @property {ReadFn} read Reads a model using the repository.
*/
export interface ReadsModels< T extends Model, IDParam = number > {
read( id: IDParam ): Promise< T >;
}
/**
* An interface for repositories that can update models.
*
* @typedef UpdatesModels
* @property {UpdateFn} update Updates a model using the repository.
*/
export interface UpdatesModels< T extends Model, IDParam = number > {
update( id: IDParam, properties: Partial< T > ): Promise< T >;
}
/**
* An interface for repositories that can delete models.
*
* @typedef DeletesModels
* @property {DeleteFn} delete Deletes a model using the repository.
*/
export interface DeletesModels< IDParam = number > {
delete( id: IDParam ): Promise< boolean >;
}
/**
* A class for performing CRUD operations on models using a number of internal hooks.
* Note that if a model does not support a given operation then it will throw an
* error when attempting to perform that action.
*/
export class ModelRepository< T extends Model, IDParam = number > implements
CreatesModels< T >,
ReadsModels< T, IDParam >,
UpdatesModels< T, IDParam >,
DeletesModels< IDParam > {
/**
* The hook used to create models
*
* @type {CreateFn}
* @private
*/
private readonly createHook: CreateFn< T > | null;
/**
* The hook used to read models.
*
* @type {ReadFn}
* @private
*/
private readonly readHook: ReadFn< IDParam, T > | null;
/**
* The hook used to update models.
*
* @type {UpdateFn}
* @private
*/
private readonly updateHook: UpdateFn< IDParam, T > | null;
/**
* The hook used to delete models.
*
* @type {DeleteFn}
* @private
*/
private readonly deleteHook: DeleteFn< IDParam > | null;
/**
* Creates a new repository instance.
*
* @param {CreateFn|null} createHook The hook for model creation.
* @param {ReadFn|null} readHook The hook for model reading.
* @param {UpdateFn|null} updateHook The hook for model updating.
* @param {DeleteFn|null} deleteHook The hook for model deletion.
*/
public constructor(
createHook: CreateFn< T > | null,
readHook: ReadFn< IDParam, T > | null,
updateHook: UpdateFn< IDParam, T > | null,
deleteHook: DeleteFn< IDParam > | null,
) {
this.createHook = createHook;
this.readHook = readHook;
this.updateHook = updateHook;
this.deleteHook = deleteHook;
}
/**
* Creates the given model.
*
* @param {Object} properties The properties for the model we'd like to create.
* @return {Promise.<Model>} A promise that resolves to the model after creation.
*/
public create( properties: Partial< T > ): Promise< T > {
if ( ! this.createHook ) {
throw new Error( 'The \'create\' operation is not supported on this model.' );
}
return this.createHook( properties );
}
/**
* Reads the given model.
*
* @param {number|Object} id The identifier for the model to read.
* @return {Promise.<Model>} A promise that resolves to the model.
*/
public read( id: IDParam ): Promise< T > {
if ( ! this.readHook ) {
throw new Error( 'The \'read\' operation is not supported on this model.' );
}
return this.readHook( id );
}
/**
* Updates the given model.
*
* @param {number|Object} id The identifier for the model to create.
* @param {Object} properties The model properties that we'd like to update.
* @return {Promise.<Model>} A promise that resolves to the model after updating.
*/
public update( id: IDParam, properties: Partial< T > ): Promise< T > {
if ( ! this.updateHook ) {
throw new Error( 'The \'update\' operation is not supported on this model.' );
}
return this.updateHook( id, properties );
}
/**
* Deletes the given model.
*
* @param {number|Object} id The identifier for the model to delete.
* @return {Promise.<boolean>} A promise that resolves to "true" on success.
*/
public delete( id: IDParam ): Promise< boolean > {
if ( ! this.deleteHook ) {
throw new Error( 'The \'delete\' operation is not supported on this model.' );
}
return this.deleteHook( id );
}
}

View File

@ -0,0 +1,56 @@
import * as moxios from 'moxios';
import { AxiosClient } from '../axios-client';
import { HTTPResponse } from '../../http-client';
import { AxiosInterceptor } from '../axios-interceptor';
import { mock } from 'jest-mock-extended';
describe( 'AxiosClient', () => {
let httpClient: AxiosClient;
beforeEach( () => {
moxios.install();
} );
afterEach( () => {
moxios.uninstall();
} );
it( 'should transform to HTTPResponse', async () => {
httpClient = new AxiosClient( { baseURL: 'http://test.test' } );
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await httpClient.get( '/test' );
expect( response ).toBeInstanceOf( HTTPResponse );
expect( response ).toHaveProperty( 'statusCode', 200 );
expect( response ).toHaveProperty( 'headers', { 'content-type': 'application/json' } );
expect( response ).toHaveProperty( 'data', { test: 'value' } );
} );
it( 'should start extra interceptors', async () => {
const interceptor = mock< AxiosInterceptor >();
httpClient = new AxiosClient(
{ baseURL: 'http://test.test' },
[ interceptor ],
);
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
await httpClient.get( '/test' );
expect( interceptor.start ).toHaveBeenCalled();
} );
} );

View File

@ -1,6 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import moxios from 'moxios';
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
import * as moxios from 'moxios';
import { AxiosOAuthInterceptor } from '../axios-oauth-interceptor';
describe( 'AxiosOAuthInterceptor', () => {
let apiAuthInterceptor: AxiosOAuthInterceptor;
@ -22,7 +22,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should not run unless started', async () => {
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
moxios.stubRequest( 'https://api.test', { status: 200 } );
apiAuthInterceptor.stop( axiosInstance );
await axiosInstance.get( 'https://api.test' );
@ -38,7 +38,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should use basic auth for HTTPS', async () => {
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
moxios.stubRequest( 'https://api.test', { status: 200 } );
await axiosInstance.get( 'https://api.test' );
const request = moxios.requests.mostRecent();
@ -51,7 +51,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should use OAuth 1.0a for HTTP', async () => {
moxios.stubOnce( 'GET', 'http://api.test', { status: 200 } );
moxios.stubRequest( 'http://api.test', { status: 200 } );
await axiosInstance.get( 'http://api.test' );
const request = moxios.requests.mostRecent();
@ -65,7 +65,7 @@ describe( 'AxiosOAuthInterceptor', () => {
} );
it( 'should work with base URL', async () => {
moxios.stubOnce( 'GET', '/test', { status: 200 } );
moxios.stubRequest( '/test', { status: 200 } );
await axiosInstance.request( {
method: 'GET',
baseURL: 'https://api.test/',

View File

@ -1,7 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import moxios from 'moxios';
import { APIResponse, APIError } from '../api-service';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
import * as moxios from 'moxios';
import { AxiosResponseInterceptor } from '../axios-response-interceptor';
describe( 'AxiosResponseInterceptor', () => {
let apiResponseInterceptor: AxiosResponseInterceptor;
@ -19,8 +18,8 @@ describe( 'AxiosResponseInterceptor', () => {
moxios.uninstall();
} );
it( 'should transform responses into APIResponse', async () => {
moxios.stubOnce( 'GET', 'http://test.test', {
it( 'should transform responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 200,
headers: {
'Content-Type': 'application/json',
@ -31,7 +30,7 @@ describe( 'AxiosResponseInterceptor', () => {
const response = await axiosInstance.get( 'http://test.test' );
expect( response ).toMatchObject( {
status: 200,
statusCode: 200,
headers: {
'content-type': 'application/json',
},
@ -41,21 +40,34 @@ describe( 'AxiosResponseInterceptor', () => {
} );
} );
it( 'should transform response errors into APIError', async () => {
moxios.stubOnce( 'GET', 'http://test.test', {
it( 'should transform error responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 404,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { code: 'error_code', message: 'value', data: null } ),
responseText: JSON.stringify( { code: 'error_code', message: 'value' } ),
} );
const response = await axiosInstance.get( 'http://test.test' );
expect( response ).toMatchObject( {
statusCode: 404,
headers: {
'content-type': 'application/json',
},
data: {
code: 'error_code',
message: 'value',
},
} );
} );
it( 'should bubble non-response errors', async () => {
moxios.stubTimeout( 'http://test.test' );
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject(
new APIResponse(
404,
{ 'content-type': 'application/json' },
new APIError( 'error_code', 'value', null ),
),
new Error( 'timeout of 0ms exceeded' ),
);
} );
} );

View File

@ -0,0 +1,115 @@
import { HTTPClient, HTTPResponse } from '../http-client';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { AxiosInterceptor } from './axios-interceptor';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
/**
* An HTTPClient implementation that uses Axios to make requests.
*/
export class AxiosClient implements HTTPClient {
/**
* An instance of the axios client for making HTTP requests.
*
* @type {AxiosInstance}
* @private
*/
private readonly client: AxiosInstance;
/**
* An array of interceptors that should be applied to the client.
*
* @type {AxiosInterceptor[]}
* @private
*/
private readonly interceptors: AxiosInterceptor[];
/**
* Creates a new axios client.
*
* @param {AxiosRequestConfig} config The request configuration.
* @param {AxiosInterceptor[]} extraInterceptors An array of additional interceptors to apply to the client.
*/
public constructor( config: AxiosRequestConfig, extraInterceptors: AxiosInterceptor[] = [] ) {
this.client = axios.create( config );
this.interceptors = extraInterceptors;
// The response interceptor needs to be last to prevent the other interceptors from
// receiving the transformed HTTPResponse type instead of an AxiosResponse.
this.interceptors.push( new AxiosResponseInterceptor() );
for ( const interceptor of this.interceptors ) {
interceptor.start( this.client );
}
}
/**
* Performs a GET request.
*
* @param {string} path The path we should send the request to.
* @param {Object} params Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public get< T = any >(
path: string,
params?: object,
): Promise< HTTPResponse< T >> {
return this.client.get( path, { params } );
}
/**
* Performs a POST request.
*
* @param {string} path The path we should send the request to.
* @param {Object} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public post< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.post( path, data );
}
/**
* Performs a PUT request.
*
* @param {string} path The path we should send the request to.
* @param {Object} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public put< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.put( path, data );
}
/**
* Performs a PATCH request.
*
* @param {string} path The path we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public patch< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.patch( path, data );
}
/**
* Performs a DELETE request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
public delete< T = any >(
path: string,
data?: object,
): Promise< HTTPResponse< T >> {
return this.client.delete( path, { data } );
}
}

View File

@ -1,5 +1,13 @@
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
/**
* An object containing the IDs for an interceptor currently applied to a client.
*
* @typedef ActiveInterceptor
* @property {AxiosInstance} client The client the interceptor is tied to.
* @property {number} requestInterceptorID The ID of the request interceptor callbacks.
* @property {number} responseInterceptorID The ID of the response interceptor callbacks.
*/
type ActiveInterceptor = {
client: AxiosInstance;
requestInterceptorID: number;
@ -10,6 +18,12 @@ type ActiveInterceptor = {
* A base class for encapsulating the start and stop functionality required by all axios interceptors.
*/
export abstract class AxiosInterceptor {
/**
* An array of the active interceptor records for all of the clients this interceptor is attached to.
*
* @type {ActiveInterceptor[]}
* @private
*/
private readonly activeInterceptors: ActiveInterceptor[] = [];
/**

View File

@ -1,14 +1,26 @@
import { AxiosRequestConfig } from 'axios';
import createHmac from 'create-hmac';
import OAuth from 'oauth-1.0a';
import type { AxiosRequestConfig } from 'axios';
import * as createHmac from 'create-hmac';
import * as OAuth from 'oauth-1.0a';
import { AxiosInterceptor } from './axios-interceptor';
/**
* A utility class for managing the lifecycle of an authentication interceptor.
*/
export class AxiosOAuthInterceptor extends AxiosInterceptor {
/**
* The OAuth class for signing the request.
*
* @type {Object}
* @private
*/
private oauth: OAuth;
/**
* Creates a new interceptor.
*
* @param {string} consumerKey The consumer key of the API key.
* @param {string} consumerSecret The consumer secret of the API key.
*/
public constructor( consumerKey: string, consumerSecret: string ) {
super();

View File

@ -0,0 +1,34 @@
import { AxiosResponse } from 'axios';
import { AxiosInterceptor } from './axios-interceptor';
import { HTTPResponse } from '../http-client';
export class AxiosResponseInterceptor extends AxiosInterceptor {
/**
* Transforms the Axios response into our HTTP response.
*
* @param {AxiosResponse} response The response that we need to transform.
* @return {Promise} A promise containing the HTTPResponse.
*/
protected onResponseSuccess( response: AxiosResponse ): Promise< HTTPResponse > {
return Promise.resolve< HTTPResponse >(
new HTTPResponse( response.status, response.headers, response.data ),
);
}
/**
* Axios throws HTTP errors so we need to eat those errors and pass them normally.
*
* @param {*} error The error that was caught.
* @return {Promise} A promise containing the HTTPResponse.
*/
protected onResponseRejected( error: any ): Promise< HTTPResponse > {
// Convert HTTP response errors into a form that we can handle them with.
if ( error.response ) {
return Promise.resolve< HTTPResponse >(
new HTTPResponse( error.response.status, error.response.headers, error.response.data ),
);
}
throw error;
}
}

View File

@ -0,0 +1,2 @@
export { AxiosClient } from './axios-client';
export { AxiosOAuthInterceptor } from './axios-oauth-interceptor';

View File

@ -0,0 +1,39 @@
import { HTTPClient } from './http-client';
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
/**
* A class for generating HTTPClient instances with desired configurations.
*/
export class HTTPClientFactory {
/**
* Creates a new client instance prepared for basic auth.
*
* @param {string} apiURL
* @param {string} username
* @param {string} password
* @return {HTTPClient} An HTTP client configured for OAuth requests.
*/
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
return new AxiosClient(
{
baseURL: apiURL,
auth: { username, password },
},
);
}
/**
* Creates a new client instance prepared for oauth.
*
* @param {string} apiURL
* @param {string} consumerKey
* @param {string} consumerSecret
* @return {HTTPClient} An HTTP client configured for OAuth requests.
*/
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
return new AxiosClient(
{ baseURL: apiURL },
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
);
}
}

View File

@ -0,0 +1,88 @@
/**
* A structured response from the HTTP client.
*/
export class HTTPResponse< T = any > {
/**
* The status code from the response.
*
* @type {number}
*/
public readonly statusCode: number;
/**
* The headers from the response.
*
* @type {Object.<string, string|string[]>}
*/
public readonly headers: any;
/**
* The data from the response.
*
* @type {Object}
*/
public readonly data: T;
/**
* Creates a new HTTP response instance.
*
* @param {number} statusCode The status code from the HTTP response.
* @param {Object.<string, string|string[]>} headers The headers from the HTTP response.
* @param {Object} data The data from the HTTP response.
*/
public constructor( statusCode: number, headers: any, data: T ) {
this.statusCode = statusCode;
this.headers = headers;
this.data = data;
}
}
/**
* An interface for clients that make HTTP requests.
*/
export interface HTTPClient {
/**
* Performs a GET request.
*
* @param {string} path The path we should send the request to.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
get< T = any >( path: string, params?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a POST request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
post< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PUT request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
put< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a PATCH request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
patch< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
/**
* Performs a DELETE request.
*
* @param {string} path The path we should send the request to.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise.<HTTPResponse>} The response from the API.
*/
delete< T = any >( path: string, data?: any ): Promise< HTTPResponse< T > >;
}

View File

@ -0,0 +1,3 @@
export { HTTPResponse } from './http-client';
export type { HTTPClient } from './http-client';
export { HTTPClientFactory } from './http-client-factory';

View File

@ -0,0 +1,2 @@
export { HTTPClientFactory } from './http';
export * from './models';

View File

@ -0,0 +1 @@
export { SimpleProduct } from './products/simple-product';

View File

@ -0,0 +1,11 @@
/**
* A base class for all models.
*/
export abstract class Model {
/**
* The ID of the model if it exists.
*
* @type {number|null}
*/
public readonly id: number | null = null;
}

View File

@ -0,0 +1,20 @@
import { Model } from '../model';
/**
* The base class for all product types.
*/
export abstract class AbstractProduct extends Model {
/**
* The name of the product.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The regular price of the product when not discounted.
*
* @type {string}
*/
public readonly regularPrice: string = '';
}

View File

@ -0,0 +1,29 @@
import { AbstractProduct } from './abstract-product';
import { HTTPClient } from '../../http';
import { CreatesModels } from '../../framework/model-repository';
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
/**
* A simple product object.
*/
export class SimpleProduct extends AbstractProduct {
/**
* Creates a new simple product instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< SimpleProduct > = {} ) {
super();
Object.assign( this, properties );
}
/**
* Creates a model repository configured for communicating via the REST API.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
* @return {CreatesModels} The created repository.
*/
public static restRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > {
return simpleProductRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,29 @@
import { simpleProductRESTRepository } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models';
import { CreatesModels } from '../../../../framework/model-repository';
describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: CreatesModels< SimpleProduct >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = simpleProductRESTRepository( httpClient );
} );
it( 'should create', async () => {
httpClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{ id: 123 },
) );
const created = await repository.create( { name: 'Test Product' } );
expect( created ).toBeInstanceOf( SimpleProduct );
expect( created ).toMatchObject( { id: 123 } );
expect( httpClient.post ).toHaveBeenCalledWith( '/wc/v3/products', { type: 'simple', name: 'Test Product' } );
} );
} );

View File

@ -0,0 +1,43 @@
import { HTTPClient } from '../../../http';
import { CreateFn, CreatesModels, ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models';
/**
* Creates a callback for REST model creation.
*
* @param {HTTPClient} httpClient The HTTP client for requests.
* @return {CreateFn} The callback for creating models via the REST API.
*/
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProduct > {
return async ( properties ) => {
const response = await httpClient.post(
'/wc/v3/products',
{
type: 'simple',
name: properties.name,
regular_price: properties.regularPrice,
},
);
return Promise.resolve( new SimpleProduct( {
id: response.data.id,
name: response.data.name,
regularPrice: response.data.regular_price,
} ) );
};
}
/**
* Creates a new ModelRepository instance for interacting with models via the REST API.
*
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {CreatesModels} A repository for interacting with models via the REST API.
*/
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesModels< SimpleProduct > {
return new ModelRepository(
restCreate( httpClient ),
null,
null,
null,
);
}

View File

@ -1,9 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"types": [ "node", "jest", "faker", "axios", "moxios", "create-hmac" ],
"types": [ "node", "jest", "axios", "moxios", "create-hmac" ],
"rootDir": "src",
"outDir": "dist"
"outDir": "dist",
"target": "es5"
},
"include": [ "src/" ]
}

View File

@ -4,7 +4,7 @@ const { spawnSync } = require( 'child_process' );
const program = require( 'commander' );
const path = require( 'path' );
const fs = require( 'fs' );
const { getAppRoot, getTestConfig } = require( '../utils' );
const { getAppRoot, getAppName, getTestConfig } = require( '../utils' );
const dockerArgs = [];
let command = '';
@ -45,7 +45,7 @@ if ( appPath ) {
}
// Provide an "app name" to use in Docker container names.
envVars.APP_NAME = path.basename( appPath );
envVars.APP_NAME = getAppName();
}
// Load test configuration file into an object.

View File

@ -5,4 +5,5 @@ global.process.env = {
...global.process.env,
// Remove the trailing slash from jest sequencer WORDPRESS_URL.
WP_BASE_URL: testConfig.baseUrl,
PUPPETEER_SLOWMO: true,
};

View File

@ -14,6 +14,8 @@ if ( 'no' == global.process.env.node_config_dev ) {
puppeteerConfig = {
launch: {
...jestPuppeteerConfig.launch,
slowMo: process.env.PUPPETEER_SLOWMO ? process.env.PUPPETEER_SLOWMO : 50,
headless: false,
ignoreHTTPSErrors: true,
args: [ '--window-size=1920,1080', '--user-agent=chrome' ],
devtools: true,

216
tests/e2e/env/package-lock.json generated vendored
View File

@ -130,11 +130,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -1065,11 +1065,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -1308,9 +1308,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -1677,9 +1677,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -2584,9 +2584,9 @@
"integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw=="
},
"@types/babel__core": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz",
"integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==",
"version": "7.1.10",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.10.tgz",
"integrity": "sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==",
"requires": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0",
@ -2604,9 +2604,9 @@
}
},
"@types/babel__template": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz",
"integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.3.tgz",
"integrity": "sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==",
"requires": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
@ -2675,9 +2675,9 @@
"integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM="
},
"@types/node": {
"version": "14.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.1.tgz",
"integrity": "sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw=="
"version": "14.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
"integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@ -2705,9 +2705,9 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
},
"@types/yargs": {
"version": "13.0.10",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz",
"integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==",
"version": "13.0.11",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz",
"integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==",
"requires": {
"@types/yargs-parser": "*"
}
@ -2842,9 +2842,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -3041,9 +3041,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -3143,11 +3143,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"fill-range": {
@ -4225,12 +4225,12 @@
}
},
"browserslist": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.3.tgz",
"integrity": "sha512-GcZPC5+YqyPO4SFnz48/B0YaCwS47Q9iPChRGi6t7HhflKBcINzFrJvRfC+jp30sRMKxF+d4EHGs27Z0XP1NaQ==",
"version": "4.14.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz",
"integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==",
"requires": {
"caniuse-lite": "^1.0.30001131",
"electron-to-chromium": "^1.3.570",
"caniuse-lite": "^1.0.30001135",
"electron-to-chromium": "^1.3.571",
"escalade": "^3.1.0",
"node-releases": "^1.1.61"
}
@ -4311,9 +4311,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"caniuse-lite": {
"version": "1.0.30001131",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001131.tgz",
"integrity": "sha512-4QYi6Mal4MMfQMSqGIRPGbKIbZygeN83QsWq1ixpUwvtfgAZot5BrCKzGygvZaV+CnELdTwD0S4cqUNozq7/Cw=="
"version": "1.0.30001137",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz",
"integrity": "sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw=="
},
"capture-exit": {
"version": "2.0.0",
@ -4340,12 +4340,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -4591,9 +4591,9 @@
}
},
"config": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/config/-/config-3.3.1.tgz",
"integrity": "sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/config/-/config-3.3.2.tgz",
"integrity": "sha512-NlGfBn2565YA44Irn7GV5KHlIGC3KJbf0062/zW5ddP9VXIuRj0m7HVyFAWvMZvaHPEglyGfwmevGz3KosIpCg==",
"requires": {
"json5": "^2.1.1"
}
@ -4965,9 +4965,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.570",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz",
"integrity": "sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg=="
"version": "1.3.572",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.572.tgz",
"integrity": "sha512-TKqdEukCCl7JC20SwEoWTbtnGt4YjfHWAv4tcNky0a9qGo0WdM+Lrd60tps+nkaJCmktKBJjr99fLtEBU1ipWQ=="
},
"emoji-regex": {
"version": "8.0.0",
@ -5025,9 +5025,9 @@
}
},
"enzyme-adapter-react-16": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.4.tgz",
"integrity": "sha512-wPzxs+JaGDK2TPYzl5a9YWGce6i2SQ3Cg51ScLeyj2WotUZ8Obcq1ke/U1Y2VGpYlb9rrX2yCjzSMgtKCeAt5w==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz",
"integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==",
"requires": {
"enzyme-adapter-utils": "^1.13.1",
"enzyme-shallow-equal": "^1.0.4",
@ -5212,11 +5212,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"glob-parent": {
@ -5248,9 +5248,9 @@
}
},
"eslint-config-prettier": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz",
"integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz",
"integrity": "sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==",
"dev": true,
"requires": {
"get-stdin": "^6.0.0"
@ -5280,12 +5280,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -5333,9 +5333,9 @@
}
},
"eslint-plugin-react": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz",
"integrity": "sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg==",
"version": "7.21.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.2.tgz",
"integrity": "sha512-j3XKvrK3rpBzveKFbgAeGsWb9uz6iUOrR0jixRfjwdFeGSRsXvVTFtHDQYCjsd1/6Z/xvb8Vy3LiI5Reo7fDrg==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
@ -6222,11 +6222,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6469,9 +6469,9 @@
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-callable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz",
"integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg=="
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
},
"is-ci": {
"version": "2.0.0",
@ -6768,11 +6768,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6962,9 +6962,9 @@
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.7.tgz",
"integrity": "sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==",
"requires": {
"@types/yargs-parser": "*"
}
@ -7109,11 +7109,11 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"detect-newline": {
@ -9127,12 +9127,12 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"fill-range": {
@ -9211,9 +9211,9 @@
}
},
"nearley": {
"version": "2.19.6",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.6.tgz",
"integrity": "sha512-OV3Lx+o5iIGWVY38zs+7aiSnBqaHTFAOQiz83VHJje/wOOaSgzE3H0S/xfISxJhFSoPcX611OEDV9sCT8F283g==",
"version": "2.19.7",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.7.tgz",
"integrity": "sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==",
"requires": {
"commander": "^2.19.0",
"moo": "^0.5.0",
@ -9943,11 +9943,11 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -9991,12 +9991,12 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"https-proxy-agent": {
@ -11678,11 +11678,11 @@
"integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {

View File

@ -50,7 +50,7 @@
"docker:up": "./bin/docker-compose.sh up",
"docker:down": "./bin/docker-compose.sh down",
"docker:clear-all": "docker rmi --force $(docker images -q)",
"docker:ssh": "docker exec -it woocommerce_wordpress-www /bin/bash",
"docker:ssh": "docker exec -it $(node utils/get-app-name.js)_wordpress-www /bin/bash",
"install-wp-tests": "./bin/install-wp-tests.sh",
"test:e2e": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js",
"test:e2e-dev": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev"

View File

@ -16,4 +16,12 @@ const getAppRoot = () => {
return appPath;
};
module.exports = getAppRoot;
const getAppName = () => {
const appRoot = getAppRoot();
return path.basename( appRoot );
};
module.exports = {
getAppRoot,
getAppName,
};

7
tests/e2e/env/utils/get-app-name.js vendored Normal file
View File

@ -0,0 +1,7 @@
/**
* Provide the application name to bash scripts.
*/
const { getAppName } = require( './app-root' );
const appName = getAppName();
console.log( appName );

View File

@ -1,4 +1,7 @@
/**
* Provide the base test URL to bash scripts.
*/
const getTestConfig = require( './test-config' );
const testConfig = getTestConfig();
console.log(testConfig.baseUrl);
console.log( testConfig.baseUrl );

View File

@ -1,7 +1,8 @@
const getAppRoot = require( './app-root' );
const { getAppRoot, getAppName } = require( './app-root' );
const getTestConfig = require( './test-config' );
module.exports = {
getAppRoot,
getTestConfig,
getAppRoot,
getAppName,
getTestConfig,
};

View File

@ -1,6 +1,6 @@
const path = require( 'path' );
const fs = require( 'fs' );
const getAppRoot = require( './app-root' );
const { getAppRoot } = require( './app-root' );
// Copy local test configuration file if it exists.
const appPath = getAppRoot();

View File

@ -1,58 +0,0 @@
# Model Factories
A simple interface for generating models of different types.
## Installation
``bash
npm install @woocommerce/model-factories --save-dev
``
## Usage
Consumers of this package should rely on an instance of `ModelRegistry` to access the factories.
Here is an example of how to initialize and use the package to generate a simple product:
```javascript
import {
AdapterTypes,
initializeUsingBasicAuth,
ModelRegistry,
registerSimpleProduct,
SimpleProduct
} from '@woocommerce/model-factories';
// The ModelRegistry instance is where all of the factories and adapters are stored in an easy-to-access way.
const modelRegistry = new ModelRegistry()
// Call the register functions to add a kind of factory to the model registry.
// This will also add any adapters we've created for the factory, allowing it
// to be created on the server.
registerSimpleProduct( modelRegistry );
// Before you can use the included API adapter you need to initialize it using one of the utility methods.
// If you do not initialize the API adapters they will not be able to make requests to the API.
// Note that these utility functions only set up adapters that have been registered already
// and so further calls to `registeryXXX` functions will have adapters that aren't ready.
initializeUsingBasicAuth( modelRegistry, 'https://test.test/wp-json', 'admin', 'password' );
initializeUsingOAuth( modelRegistry, 'https://test.test/wp-json', 'consumer_key', 'consumer_secret' );
// In order to actually create the models on the server, each registered factory must have an adapter set.
// You can do this on a per-factory basis using
modelRegistry.changeFactoryAdapter( SimpleProduct, AdapterTypes.API );
// You can do this to all factories registered using
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
// Once all of the initialization has been taken care of you can create models!
// Any fields that are not defined will be filled out by random data.
const product = await modelRegistry.getFactory( SimpleProduct ).create( { name: 'Test Product' } );
// You can now access the ID of the created model using `product.id`!
// You can also create models in bulk!
const poducts = await modelRegistry.getFactory( SimpleProduct ).createList( 5 );
// You now have an array of products to work with!
```
## Custom Models
## Custom Adapters

View File

@ -1,16 +0,0 @@
import { Model } from './model';
/**
* An interface for implementing adapters to create models.
*/
export interface Adapter<T extends Model> {
/**
* Creates a model or array of models using a service..
*
* @param {Model|Model[]} model The model or array of models to create.
* @return {Promise} Resolves to the created input model or array of models.
*/
create( model: T ): Promise<T>;
create( model: T[] ): Promise<T[]>;
create( model: T | T[] ): Promise<T> | Promise<T[]>;
}

View File

@ -1,55 +0,0 @@
import { Model } from '../model';
import { APIAdapter } from './api-adapter';
import { SimpleProduct } from '../../models/simple-product';
import { APIResponse, APIService } from './api-service';
class MockAPI implements APIService {
public get = jest.fn();
public post = jest.fn();
public put = jest.fn();
public patch = jest.fn();
public delete = jest.fn();
}
describe( 'APIModelCreator', () => {
let adapter: APIAdapter<Model>;
let mockService: MockAPI;
beforeEach( () => {
adapter = new APIAdapter( '/wc/v3/product', () => 'test' );
mockService = new MockAPI();
adapter.setAPIService( mockService );
} );
it( 'should create single instance', async () => {
mockService.post.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) );
const result = await adapter.create( new SimpleProduct() );
expect( result ).toBeInstanceOf( SimpleProduct );
expect( result.id ).toBe( 1 );
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
} );
it( 'should create multiple instances', async () => {
mockService.post
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) )
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 2 } ) ) )
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 3 } ) ) );
const result = await adapter.create( [ new SimpleProduct(), new SimpleProduct(), new SimpleProduct() ] );
expect( result ).toBeInstanceOf( Array );
expect( result ).toHaveLength( 3 );
expect( result[ 0 ].id ).toBe( 1 );
expect( result[ 1 ].id ).toBe( 2 );
expect( result[ 2 ].id ).toBe( 3 );
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
expect( mockService.post.mock.calls[ 1 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 1 ][ 1 ] ).toBe( 'test' );
expect( mockService.post.mock.calls[ 2 ][ 0 ] ).toBe( '/wc/v3/product' );
expect( mockService.post.mock.calls[ 2 ][ 1 ] ).toBe( 'test' );
} );
} );

View File

@ -1,87 +0,0 @@
import { APIResponse, APIService } from './api-service';
import { Model } from '../model';
import { Adapter } from '../adapter';
/**
* A callback for transforming models into an API request body.
*
* @callback APITransformerFn
* @param {Model} model The model that we want to transform.
* @return {*} The structured request data for the API.
*/
export type APITransformerFn<T extends Model> = ( model: T ) => any;
/**
* A class used for creating data models using a supplied API endpoint.
*/
export class APIAdapter<T extends Model> implements Adapter<T> {
private readonly endpoint: string;
private readonly transformer: APITransformerFn<T>;
private apiService: APIService | null;
public constructor( endpoint: string, transformer: APITransformerFn<T> ) {
this.endpoint = endpoint;
this.transformer = transformer;
this.apiService = null;
}
/**
* Sets the API service that the adapter should use for creation actions.
*
* @param {APIService|null} service The new API service for the adapter to use.
*/
public setAPIService( service: APIService | null ): void {
this.apiService = service;
}
/**
* Creates a model or array of models using the API service.
*
* @param {Model|Model[]} model The model or array of models to create.
* @return {Promise} Resolves to the created input model or array of models.
*/
public create( model: T ): Promise<T>;
public create( model: T[] ): Promise<T[]>;
public create( model: T | T[] ): Promise<T> | Promise<T[]> {
if ( ! this.apiService ) {
throw new Error( 'An API service must be registered for the adapter to work.' );
}
if ( Array.isArray( model ) ) {
return this.createList( model );
}
return this.createSingle( model );
}
/**
* Creates a single model using the API service.
*
* @param {Model} model The model to create.
* @return {Promise} Resolves to the created input model.
*/
private async createSingle( model: T ): Promise<T> {
return this.apiService!.post(
this.endpoint,
this.transformer( model ),
).then( ( data: APIResponse ) => {
model.setID( data.data.id );
return model;
} );
}
/**
* Creates an array of models using the API service.
*
* @param {Model[]} models The array of models to create.
* @return {Promise} Resolves to the array of created input models.
*/
private async createList( models: T[] ): Promise<T[]> {
const promises: Promise<T>[] = [];
for ( const model of models ) {
promises.push( this.createSingle( model ) );
}
return Promise.all( promises );
}
}

View File

@ -1,100 +0,0 @@
/**
* A structured response from the API.
*/
export class APIResponse<T = any> {
public readonly status: number;
public readonly headers: any;
public readonly data: T;
public constructor( status: number, headers: any, data: T ) {
this.status = status;
this.headers = headers;
this.data = data;
}
}
/**
* A structured error from the API.
*/
export class APIError {
public readonly code: string;
public readonly message: string;
public readonly data: any;
public constructor( code: string, message: string, data: any ) {
this.code = code;
this.message = message;
this.data = data;
}
}
/**
* Checks whether or not an APIResponse contains an error.
*
* @param {APIResponse} response The response to evaluate.
*/
export function isAPIError( response: APIResponse ): response is APIResponse<APIError> {
return response.status < 200 || response.status >= 400;
}
/**
* An interface for implementing services to make calls against the API.
*/
export interface APIService {
/**
* Performs a GET request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
get<T>(
endpoint: string,
params?: any
): Promise<APIResponse<T>>;
/**
* Performs a POST request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
post<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
/**
* Performs a PUT request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
put<T>( endpoint: string, data?: any ): Promise<APIResponse<T>>;
/**
* Performs a PATCH request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
patch<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
/**
* Performs a DELETE request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
delete<T>(
endpoint: string,
data?: any
): Promise<APIResponse<T>>;
}

View File

@ -1,57 +0,0 @@
import moxios from 'moxios';
import { APIResponse } from '../api-service';
import { AxiosAPIService } from './axios-api-service';
describe( 'AxiosAPIService', () => {
let apiClient: AxiosAPIService;
beforeEach( () => {
moxios.install();
} );
afterEach( () => {
moxios.uninstall();
} );
it( 'should add OAuth interceptors', async () => {
apiClient = AxiosAPIService.createUsingOAuth(
'http://test.test/wp-json/',
'consumer_key',
'consumer_secret',
);
moxios.stubOnce( 'GET', '/wc/v2/product', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await apiClient.get( '/wc/v2/product' );
expect( response ).toBeInstanceOf( APIResponse );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toMatch( /^OAuth/ );
} );
it( 'should add basic auth interceptors', async () => {
apiClient = AxiosAPIService.createUsingBasicAuth( 'http://test.test/wp-json/', 'test', 'pass' );
moxios.stubOnce( 'GET', '/wc/v2/product', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await apiClient.get( '/wc/v2/product' );
expect( response ).toBeInstanceOf( APIResponse );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toMatch( /^Basic/ );
} );
} );

View File

@ -1,127 +0,0 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { APIResponse, APIService } from '../api-service';
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
import { AxiosInterceptor } from './axios-interceptor';
import { AxiosResponseInterceptor } from './axios-response-interceptor';
/**
* An API service implementation that uses Axios to make requests to the WordPress API.
*/
export class AxiosAPIService implements APIService {
private readonly client: AxiosInstance;
private readonly interceptors: AxiosInterceptor[];
public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) {
this.client = axios.create( config );
this.interceptors = interceptors;
for ( const interceptor of this.interceptors ) {
interceptor.start( this.client );
}
}
/**
* Creates a new Axios API Service using OAuth 1.0a one-legged authentication.
*
* @param {string} apiURL The base URL for the API requests to be sent.
* @param {string} consumerKey The OAuth consumer key.
* @param {string} consumerSecret The OAuth consumer secret.
* @return {AxiosAPIService} The created service.
*/
public static createUsingOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): AxiosAPIService {
return new AxiosAPIService(
{ baseURL: apiURL },
[
new AxiosOAuthInterceptor( consumerKey, consumerSecret ),
new AxiosResponseInterceptor(),
],
);
}
/**
* Creates a new Axios API Service using basic authentication.
*
* @param {string} apiURL The base URL for the API requests to be sent.
* @param {string} username The username for authentication.
* @param {string} password The password for authentication.
* @return {AxiosAPIService} The created service.
*/
public static createUsingBasicAuth( apiURL: string, username: string, password: string ): AxiosAPIService {
return new AxiosAPIService(
{
baseURL: apiURL,
auth: { username, password },
},
[ new AxiosResponseInterceptor() ],
);
}
/**
* Performs a GET request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} params Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public get<T>(
endpoint: string,
params?: any,
): Promise<APIResponse<T>> {
return this.client.get( endpoint, { params } );
}
/**
* Performs a POST request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public post<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.post( endpoint, data );
}
/**
* Performs a PUT request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public put<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.put( endpoint, data );
}
/**
* Performs a PATCH request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public patch<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.patch( endpoint, data );
}
/**
* Performs a DELETE request against the WordPress API.
*
* @param {string} endpoint The API endpoint we should query.
* @param {*} data Any parameters that should be passed in the request.
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
*/
public delete<T>(
endpoint: string,
data?: any,
): Promise<APIResponse<T>> {
return this.client.delete( endpoint, { data } );
}
}

View File

@ -1,39 +0,0 @@
import { AxiosResponse } from 'axios';
import { APIResponse, APIError } from '../api-service';
import { AxiosInterceptor } from './axios-interceptor';
export class AxiosResponseInterceptor extends AxiosInterceptor {
/**
* Transforms the Axios response into our API response to be consumed in a consistent manner.
*
* @param {AxiosResponse} response The respons ethat we need to transform.
* @return {Promise} A promise containing the APIResponse.
*/
protected onResponseSuccess( response: AxiosResponse ): Promise<APIResponse> {
return Promise.resolve<APIResponse>(
new APIResponse( response.status, response.headers, response.data ),
);
}
/**
* Transforms HTTP errors into an API error if the error came from the API.
*
* @param {*} error The error that was caught.
*/
protected onResponseRejected( error: any ): Promise<APIResponse> {
// Only transform API errors.
if ( ! error.response ) {
throw error;
}
throw new APIResponse(
error.response.status,
error.response.headers,
new APIError(
error.response.data.code,
error.response.data.message,
error.response.data.data,
),
);
}
}

View File

@ -1,14 +0,0 @@
/**
* CORE CLASSES
* These exports relate to extending the core functionality of the package.
*/
export { Adapter } from './adapter';
export { ModelFactory } from './model-factory';
export { Model } from './model';
/**
* API ADAPTER
* These exports relate to replacing the underlying HTTP layer of API adapters.
*/
export { APIAdapter } from './api/api-adapter';
export { APIService, APIResponse, APIError } from './api/api-service';

View File

@ -1,41 +0,0 @@
import { ModelFactory } from './model-factory';
import { Adapter } from './adapter';
import { Product } from '../models/product';
import { SimpleProduct } from '../models/simple-product';
class MockAdapter implements Adapter<Product> {
public create = jest.fn();
}
describe( 'ModelFactory', () => {
let mockAdapter: MockAdapter;
let factory: ModelFactory<Product>;
beforeEach( () => {
mockAdapter = new MockAdapter();
factory = ModelFactory.define<Product, any, ModelFactory<Product>>(
( { params } ) => {
return new SimpleProduct( params );
},
);
} );
it( 'should error without adapter', async () => {
expect( () => factory.create() ).toThrowError( /no adapter/ );
} );
it( 'should create using adapter', async () => {
factory.setAdapter( mockAdapter );
const expectedModel = new SimpleProduct( { name: 'test2' } );
expectedModel.setID( 1 );
mockAdapter.create.mockReturnValueOnce( Promise.resolve( expectedModel ) );
const created = await factory.create( { name: 'test' } );
expect( mockAdapter.create.mock.calls ).toHaveLength( 1 );
expect( created ).toBeInstanceOf( Product );
expect( created.id ).toBe( 1 );
expect( created.name ).toBe( 'test2' );
} );
} );

View File

@ -1,52 +0,0 @@
import { DeepPartial, Factory, BuildOptions } from 'fishery';
import { Model } from './model';
import { Adapter } from './adapter';
/**
* A factory that can be used to create models using an adapter.
*/
export class ModelFactory<T extends Model, I = any> extends Factory<T, I> {
private adapter: Adapter<T> | null = null;
/**
* Sets the adapter that the factory will use to create models.
*
* @param {Adapter|null} adapter
*/
public setAdapter( adapter: Adapter<T> | null ): void {
this.adapter = adapter;
}
/**
* Create an object using your factory
*
* @param {DeepPartial} params The parameters that should populate the object.
* @param {BuildOptions} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
public create( params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T> {
if ( ! this.adapter ) {
throw new Error( 'The factory has no adapter to create using.' );
}
const model = this.build( params, options );
return this.adapter.create( model );
}
/**
* Create an array of objects using your factory
*
* @param {number} number The number of models to create.
* @param {DeepPartial} params The parameters that should populate the object.
* @param {BuildOptions} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
public createList( number: number, params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T[]> {
if ( ! this.adapter ) {
throw new Error( 'The factory has no adapter to create using.' );
}
const model = this.buildList( number, params, options );
return this.adapter.create( model );
}
}

View File

@ -1,45 +0,0 @@
import { AdapterTypes, ModelRegistry } from './model-registry';
import { ModelFactory } from './model-factory';
import { Product } from '../models/product';
import { APIAdapter } from './api/api-adapter';
import { SimpleProduct } from '../models/simple-product';
describe( 'ModelRegistry', () => {
let factoryRegistry: ModelRegistry;
beforeEach( () => {
factoryRegistry = new ModelRegistry();
} );
it( 'should register factories once', () => {
const factory = ModelFactory.define<Product, any, ModelFactory<Product>>( ( { params } ) => {
return new SimpleProduct( params );
} );
expect( factoryRegistry.getFactory( SimpleProduct ) ).toBeNull();
factoryRegistry.registerFactory( SimpleProduct, factory );
expect( () => factoryRegistry.registerFactory( SimpleProduct, factory ) )
.toThrowError( /already been registered/ );
const loaded = factoryRegistry.getFactory( SimpleProduct );
expect( loaded ).toBe( factory );
} );
it( 'should register adapters once', () => {
const adapter = new APIAdapter<Product>( '', ( model ) => model );
expect( factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API ) ).toBeNull();
factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter );
expect( () => factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter ) )
.toThrowError( /already been registered/ );
const loaded = factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API );
expect( loaded ).toBe( adapter );
} );
} );

View File

@ -1,125 +0,0 @@
import { Adapter } from './adapter';
import { Model } from './model';
import { ModelFactory } from './model-factory';
type Registry<T> = { [key: string ]: T };
/**
* The types of adapters that can be stored in the registry.
*
* @typedef AdapterTypes
* @property {string} API "api"
* @property {string} Custom "custom"
*/
export enum AdapterTypes {
API = 'api',
Custom = 'custom'
}
/**
* A registry that allows for us to easily manage all of our factories and related state.
*/
export class ModelRegistry {
private readonly factories: Registry<ModelFactory<any>> = {};
private readonly adapters: { [key in AdapterTypes]: Registry<Adapter<any>> } = {
api: {},
custom: {},
};
/**
* Registers a factory for the class.
*
* @param {Function} modelClass The class of model we're registering the factory for.
* @param {ModelFactory} factory The factory that we're registering.
*/
public registerFactory<T extends Model>( modelClass: new () => T, factory: ModelFactory<T> ): void {
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
throw new Error( 'A factory of this type has already been registered for the model class.' );
}
this.factories[ modelClass.name ] = factory;
}
/**
* Fetches a factory that was registered for the class.
*
* @param {Function} modelClass The class of model for the factory we're fetching.
*/
public getFactory<T extends Model>( modelClass: new () => T ): ModelFactory<T> | null {
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
return this.factories[ modelClass.name ];
}
return null;
}
/**
* Registers an adapter for the class.
*
* @param {Function} modelClass The class of model that we're registering the adapter for.
* @param {AdapterTypes} type The type of adapter that we're registering.
* @param {Adapter} adapter The adapter that we're registering.
*/
public registerAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes, adapter: Adapter<T> ): void {
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
throw new Error( 'An adapter of this type has already been registered for the model class.' );
}
this.adapters[ type ][ modelClass.name ] = adapter;
}
/**
* Fetches an adapter registered for the class.
*
* @param {Function} modelClass The class of the model for the adapter we're fetching.
* @param {AdapterTypes} type The type of adapter we're fetching.
*/
public getAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): Adapter<T> | null {
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
return this.adapters[ type ][ modelClass.name ];
}
return null;
}
/**
* Fetches all of the adapters of a given type from the registry.
*
* @param {AdapterTypes} type The type of adapters to fetch.
*/
public getAdapters( type: AdapterTypes ): Adapter<any>[] {
return Object.values( this.adapters[ type ] );
}
/**
* Changes the adapter a factory is using.
*
* @param {Function} modelClass The class of the model factory we're changing.
* @param {AdapterTypes} type The type of adapter to set.
*/
public changeFactoryAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): void {
const factory = this.getFactory( modelClass );
if ( ! factory ) {
throw new Error( 'No factory defined for this model class.' );
}
const adapter = this.getAdapter( modelClass, type );
if ( ! adapter ) {
throw new Error( 'No adapter of this type registered for this model class.' );
}
factory.setAdapter( adapter );
}
/**
* Changes the adapters of all factories to the given type or null if one is not registered for that type.
*
* @param {AdapterTypes} type The type of adapter to set.
*/
public changeAllFactoryAdapters( type: AdapterTypes ): void {
for ( const key in this.factories ) {
this.factories[ key ].setAdapter(
this.adapters[ type ][ key ] || null,
);
}
}
}

View File

@ -1,20 +0,0 @@
import { DeepPartial } from 'fishery';
/**
* A base class for all models.
*/
export abstract class Model {
private _id: number = 0;
protected constructor( partial: DeepPartial<any> = {} ) {
Object.assign( this, partial );
}
public get id(): number {
return this._id;
}
public setID( id: number ): void {
this._id = id;
}
}

View File

@ -1,17 +0,0 @@
/**
* FRAMEWORK CLASSES
* These exports relate to the core classes needed to utilize the package.
*/
export { ModelRegistry, AdapterTypes } from './framework/model-registry';
/**
* MODELS
* This exports all of the models we have defined and their related functions.
*/
export * from './models';
/**
* UTILITIES
* These exports relate to common utilities that can be used to utilize the package.
*/
export { initializeUsingOAuth, initializeUsingBasicAuth } from './utils';

View File

@ -1,2 +0,0 @@
export { Product } from './product';
export { SimpleProduct, registerSimpleProduct } from './simple-product';

View File

@ -1,15 +0,0 @@
import { Model } from '../framework/model';
import { DeepPartial } from 'fishery';
/**
* The base class for all product types.
*/
export abstract class Product extends Model {
public readonly name: string = '';
public readonly regularPrice: string = '';
protected constructor( partial: DeepPartial<Product> = {} ) {
super( partial );
Object.assign( this, partial );
}
}

View File

@ -1,48 +0,0 @@
import { DeepPartial } from 'fishery';
import { Product } from './product';
import { AdapterTypes, ModelRegistry } from '../framework/model-registry';
import { ModelFactory } from '../framework/model-factory';
import { APIAdapter } from '../framework/api/api-adapter';
import faker from 'faker/locale/en';
export class SimpleProduct extends Product {
public constructor( partial: DeepPartial<SimpleProduct> = {} ) {
super( partial );
Object.assign( this, partial );
}
}
/**
* Registers the simple product factory and adapters.
*
* @param {ModelRegistry} registry The registry to hold the model reference.
*/
export function registerSimpleProduct( registry: ModelRegistry ): void {
if ( null !== registry.getFactory( SimpleProduct ) ) {
return;
}
const factory = ModelFactory.define<SimpleProduct, any, ModelFactory<SimpleProduct>>(
( { params } ) => {
return new SimpleProduct(
{
name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(),
},
);
},
);
registry.registerFactory( SimpleProduct, factory );
const apiAdapter = new APIAdapter<SimpleProduct>(
'/wc/v3/products',
( model ) => {
return {
type: 'simple',
name: model.name,
regular_price: model.regularPrice,
};
},
);
registry.registerAdapter( SimpleProduct, AdapterTypes.API, apiAdapter );
}

View File

@ -1,55 +0,0 @@
import { AdapterTypes, ModelRegistry } from './framework/model-registry';
import { APIAdapter } from './framework/api/api-adapter';
import { AxiosAPIService } from './framework/api/axios/axios-api-service';
/**
* Initializes all of the APIAdapters with a client to communicate with the API.
*
* @param {ModelRegistry} registry The model registry that we want to initialize.
* @param {string} apiURL The base URL for the API.
* @param {string} consumerKey The OAuth consumer key for the API service.
* @param {string} consumerSecret The OAuth consumer secret for the API service.
*/
export function initializeUsingOAuth(
registry: ModelRegistry,
apiURL: string,
consumerKey: string,
consumerSecret: string,
): void {
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
if ( ! adapters.length ) {
return;
}
const apiService = AxiosAPIService.createUsingOAuth( apiURL, consumerKey, consumerSecret );
for ( const adapter of adapters ) {
adapter.setAPIService( apiService );
}
}
/**
* Initialize all of the APIAdapters with a client to communicate with the API.
*
*
*
* @param {ModelRegistry} registry The model registry that we want to initialize.
* @param {string} apiURL The base URL for the API.
* @param {string} username The username to use for authentication.
* @param {string} password The password to use for authentication.
*/
export function initializeUsingBasicAuth(
registry: ModelRegistry,
apiURL: string,
username: string,
password: string,
): void {
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
if ( ! adapters.length ) {
return;
}
const apiService = AxiosAPIService.createUsingBasicAuth( apiURL, username, password );
for ( const adapter of adapters ) {
adapter.setAPIService( apiService );
}
}

View File

@ -36,22 +36,14 @@ describe( 'Store owner can login and make sure WooCommerce is activated', () =>
describe( 'Store owner can go through store Setup Wizard', () => {
it( 'can start Setup Wizard when visiting the site for the first time. Skip all other times.', async () => {
// Check if Setup Wizard Notice is visible on the screen.
// If yes - proceed with Setup Wizard, if not - skip Setup Wizard (already been completed).
const setupWizardNotice = await Promise.race( [
new Promise( resolve => setTimeout( () => resolve(), 1000 ) ), // resolves without value after 1s
page.waitForSelector('.updated.woocommerce-message.wc-connect', { visible: true } )
] );
if ( setupWizardNotice ) {
await StoreOwnerFlow.runSetupWizard();
await completeOnboardingWizard();
}
it( 'can start and complete Setup Wizard when visiting the site for the first time.', async () => {
await StoreOwnerFlow.runSetupWizard();
await completeOnboardingWizard();
} );
} );
describe( 'Store owner can go through setup Task List', () => {
it( 'can setup shipping', async () => {
it.skip( 'can setup shipping', async () => {
await page.evaluate( () => {
document.querySelector( '.woocommerce-list__item-title' ).scrollIntoView();
} );

View File

@ -107,6 +107,19 @@
"iconv-lite": "^0.6.2"
}
},
"faker": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/faker/-/faker-5.1.0.tgz",
"integrity": "sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw=="
},
"fishery": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fishery/-/fishery-1.0.1.tgz",
"integrity": "sha512-VV8H4ZuCbZ9cCWkrYWLLPoAfpTp0t+hlJVoNWkRRHdXOgQ08wjd8ab9di8/Ed/QhgwxY3h7Y17HgDZ9osaHSSQ==",
"requires": {
"lodash.mergewith": "^4.6.2"
}
},
"gettext-parser": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz",
@ -139,6 +152,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"memize": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz",

View File

@ -11,8 +11,10 @@
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@woocommerce/api": "file:../api",
"@wordpress/e2e-test-utils": "4.6.0",
"@woocommerce/model-factories": "file:../factories"
"faker": "^5.1.0",
"fishery": "^1.0.1"
},
"publishConfig": {
"access": "public"

View File

@ -6,9 +6,8 @@
* Internal dependencies
*/
import { StoreOwnerFlow } from './flows';
import modelRegistry from './factories';
import { SimpleProduct } from '@woocommerce/model-factories';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './page-utils';
import factories from './factories';
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
@ -29,19 +28,6 @@ const verifyAndPublish = async () => {
* Complete onboarding wizard.
*/
const completeOnboardingWizard = async () => {
// Wait for "Yes please" button to appear and click on it
await page.waitForSelector( 'button[name=save_step]' );
await expect( page ).toMatchElement(
'button[name=save_step]', { text: 'Yes please' }
);
await Promise.all( [
// Click on "Yes please" button to move to the next step
page.click( 'button[name=save_step]', { text: 'Yes please' } ),
// Wait for "Where is your store based?" section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Store Details section
// Fill store's address - first line
@ -90,10 +76,10 @@ const completeOnboardingWizard = async () => {
// Query for the industries checkboxes
const industryCheckboxes = await page.$$( '.components-checkbox-control__input' );
expect( industryCheckboxes ).toHaveLength( 9 );
expect( industryCheckboxes ).toHaveLength( 8 );
// Select all industries including "Other"
for ( let i = 0; i < 9; i++ ) {
for ( let i = 0; i < 8; i++ ) {
await industryCheckboxes[i].click();
}
@ -185,7 +171,7 @@ const completeOnboardingWizard = async () => {
// Benefits section
// Wait for Benefits section to appear
await page.waitForSelector( '.woocommerce-profile-wizard__header-title' );
await page.waitForSelector( '.woocommerce-profile-wizard__benefits' );
// Wait for "No thanks" button to become active
await page.waitForSelector( 'button.is-secondary:not(:disabled)' );
@ -359,7 +345,7 @@ const completeOldSetupWizard = async () => {
* Create simple product.
*/
const createSimpleProduct = async () => {
const product = await modelRegistry.getFactory( SimpleProduct ).create( {
const product = await factories.products.simple.create( {
name: simpleProductName,
regularPrice: '9.99'
} );

View File

@ -1,24 +1,18 @@
import {
AdapterTypes,
initializeUsingBasicAuth,
ModelRegistry,
registerSimpleProduct,
} from '@woocommerce/model-factories';
import { HTTPClientFactory } from '@woocommerce/api';
const config = require( 'config' );
const modelRegistry = new ModelRegistry()
// Register all of the different factories that we're going to need.
registerSimpleProduct( modelRegistry );
// Make sure to perform the initialization AFTER registering all of the factories, otherwise the adapters might be
// missed on subsequent registrations.
initializeUsingBasicAuth( modelRegistry,
const httpClient = HTTPClientFactory.withBasicAuth(
config.get( 'url' ) + '/wp-json',
config.get( 'users.admin.username' ),
config.get( 'users.admin.password' )
config.get( 'users.admin.password' ),
);
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
export default modelRegistry;
import { simpleProductFactory } from './factories/simple-product';
const factories = {
products: {
simple: simpleProductFactory( httpClient ),
},
};
export default factories;

View File

@ -0,0 +1,41 @@
import { Factory } from 'fishery';
/**
* A temporary class until Fishery includes better async support.
*/
export class AsyncFactory extends Factory {
constructor( generator, creator ) {
super( generator );
this.creator = creator;
}
/**
* Create an object using your factory
*
* @param {*} params The parameters that should populate the object.
* @param {*} options The options to be used in the builder.
* @return {Promise} Resolves to the created model.
*/
create( params = {}, options = {} ) {
const model = this.build( params, options );
return this.creator( model );
}
/**
* Create an array of objects using your factory
*
* @param {number} number The number of models to create.
* @param {*} params The parameters that should populate the object.
* @param {*} options The options to be used in the builder.
* @return {Promise} Resolves to the created models.
*/
createList( number, params = {}, options = {} ) {
const models = this.buildList( number, params, options );
const promises = [];
for ( const model of models ) {
promises.push( this.creator( model ) );
}
return Promise.all( promises );
}
}

View File

@ -0,0 +1,23 @@
import { SimpleProduct } from '@woocommerce/api';
import { AsyncFactory } from './async-factory';
import faker from 'faker/locale/en';
/**
* Creates a new factory for creating models.
*
* @param {HTTPClient} httpClient The HTTP client we will give the repository.
* @return {AsyncFactory} The factory for creating models.
*/
export function simpleProductFactory( httpClient ) {
const repository = SimpleProduct.restRepository( httpClient );
return new AsyncFactory(
( { params } ) => {
return new SimpleProduct( {
name: params.name ?? faker.commerce.productName(),
regularPrice: params.regularPrice ?? faker.commerce.price(),
} );
},
( params ) => repository.create( params ),
);
}

View File

@ -18,7 +18,7 @@ const baseUrl = config.get( 'url' );
const WP_ADMIN_LOGIN = baseUrl + 'wp-login.php';
const WP_ADMIN_DASHBOARD = baseUrl + 'wp-admin';
const WP_ADMIN_PLUGINS = baseUrl + 'wp-admin/plugins.php';
const WP_ADMIN_SETUP_WIZARD = baseUrl + 'wp-admin/admin.php?page=wc-setup';
const WP_ADMIN_SETUP_WIZARD = baseUrl + 'wp-admin/admin.php?page=wc-admin';
const WP_ADMIN_ALL_ORDERS_VIEW = baseUrl + 'wp-admin/edit.php?post_type=shop_order';
const WP_ADMIN_NEW_COUPON = baseUrl + 'wp-admin/post-new.php?post_type=shop_coupon';
const WP_ADMIN_NEW_ORDER = baseUrl + 'wp-admin/post-new.php?post_type=shop_order';

View File

@ -1,20 +1,32 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"incremental": true,
"allowJs": true,
"checkJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"target": "es6",
"module": "commonjs",
"incremental": true,
"declaration": true,
"declarationMap": true,
"composite": true,
"emitDeclarationOnly": false,
"isolatedModules": true,
/* Strict Type-Checking Options */
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
/* Module Resolution Options */
"moduleResolution": "node",
/* This needs to be false so our types are possible to consume without setting this */
"esModuleInterop": false,
"resolveJsonModule": true
}
}

View File

@ -1,6 +1,6 @@
{
"references": [
{ "path": "tests/e2e/factories" }
{ "path": "tests/e2e/api" }
],
"files": []
}