merge trunk

This commit is contained in:
Ron Rennick 2021-03-02 15:38:03 -04:00
commit 9675b7ac15
81 changed files with 2061 additions and 177 deletions

View File

@ -26,7 +26,7 @@ If you have questions about the process to contribute code or want to discuss de
- [Minification of SCSS and JS](https://github.com/woocommerce/woocommerce/wiki/Minification-of-SCSS-and-JS)
- [Naming conventions](https://github.com/woocommerce/woocommerce/wiki/Naming-conventions)
- [String localisation guidelines](https://github.com/woocommerce/woocommerce/wiki/String-localisation-guidelines)
- [Running unit tests](https://github.com/woocommerce/woocommerce/blob/master/tests/README.md)
- [Running unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/tests/README.md)
- [Running e2e tests](https://github.com/woocommerce/woocommerce/wiki/End-to-end-Testing)
## Coding Guidelines and Development 🛠
@ -37,7 +37,7 @@ If you have questions about the process to contribute code or want to discuss de
- Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured.
- When committing, reference your issue number (#1234) and include a note about the fix.
- Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file.
- Push the changes to your fork and submit a pull request on the master branch of the WooCommerce repository.
- Push the changes to your fork and submit a pull request on the trunk branch of the WooCommerce repository.
- Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template.
- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team.

View File

@ -1,6 +1,6 @@
### All Submissions:
* [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md)?
* [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)?
* [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change?

View File

@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
build: [master]
build: [trunk]
runs-on: ubuntu-latest
steps:
- name: Checkout code

View File

@ -21,19 +21,6 @@ jobs:
name: woocommerce
path: ${{ steps.build.outputs.zip_path }}
retention-days: 7
- name: Add comment
uses: actions/github-script@v3
if: github.repository_owner == 'woocommerce'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: ':package: Artifacts ready for [download](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!'
})
e2e-tests-cache:
name: Set e2e caches for running tests

View File

@ -1,9 +1,9 @@
version: ~> 1.0
# Specifies that Travis should create builds for master and release branches and also tags.
# Specifies that Travis should create builds for trunk and release branches and also tags.
branches:
only:
- master
- trunk
- /^\d+\.\d+(\.\d+)?(-\S*)?$/
- /^release\//

View File

@ -5,8 +5,8 @@
<a href="https://packagist.org/packages/woocommerce/woocommerce"><img src="https://poser.pugx.org/woocommerce/woocommerce/v/stable" alt="Latest Stable Version"></a>
<img src="https://img.shields.io/wordpress/plugin/dt/woocommerce.svg" alt="WordPress.org downloads">
<img src="https://img.shields.io/wordpress/plugin/r/woocommerce.svg" alt="WordPress.org rating">
<a href="https://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=master" alt="Build Status"></a>
<a href="https://codecov.io/gh/woocommerce/woocommerce"><img src="https://codecov.io/gh/woocommerce/woocommerce/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=trunk" alt="Build Status"></a>
<a href="https://codecov.io/gh/woocommerce/woocommerce"><img src="https://codecov.io/gh/woocommerce/woocommerce/branch/trunk/graph/badge.svg" alt="codecov"></a>
</p>
Welcome to the WooCommerce repository on GitHub. Here you can browse the source, look at open issues and keep track of development. We recommend all developers to follow the [WooCommerce development blog](https://woocommerce.wordpress.com/) to stay up to date about everything happening in the project. You can also [follow @DevelopWC](https://twitter.com/DevelopWC) on Twitter for the latest development updates.
@ -34,4 +34,4 @@ This repository is not suitable for support. Please don't use our issue tracker
Support requests in issues on this repository will be closed on sight.
## Contributing to WooCommerce
If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md) for more information how you can do this.
If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) for more information how you can do this.

View File

@ -1,33 +1,90 @@
/*global wc_geolocation_params */
jQuery( function( $ ) {
/**
* Contains the current geo hash (or false if the hash
* is not set/cannot be determined).
*
* @type {boolean|string}
*/
var geo_hash = false;
var this_page = window.location.toString();
/**
* Obtains the current geo hash from the `woocommerce_geo_hash` cookie, if set.
*
* @returns {boolean}
*/
function get_geo_hash() {
var geo_hash_cookie = Cookies.get( 'woocommerce_geo_hash' );
if ( 'string' === typeof geo_hash_cookie && geo_hash_cookie.length ) {
geo_hash = geo_hash_cookie;
return true;
}
return false;
}
/**
* If we have an active geo hash value but it does not match the `?v=` query var in
* current page URL, that indicates that we need to refresh the page.
*
* @returns {boolean}
*/
function needs_refresh() {
return geo_hash && ( new URLSearchParams( window.location.search ) ).get( 'v' ) !== geo_hash;
}
/**
* Appends (or replaces) the geo hash used for links on the current page.
*/
var $append_hashes = function() {
if ( wc_geolocation_params.hash ) {
$( 'a[href^="' + wc_geolocation_params.home_url + '"]:not(a[href*="v="]), a[href^="/"]:not(a[href*="v="])' ).each( function() {
var $this = $( this ),
href = $this.attr( 'href' ),
href_parts = href.split( '#' );
if ( ! geo_hash ) {
return;
}
href = href_parts[0];
$( 'a[href^="' + wc_geolocation_params.home_url + '"]:not(a[href*="v="]), a[href^="/"]:not(a[href*="v="])' ).each( function() {
var $this = $( this ),
href = $this.attr( 'href' ),
href_parts = href.split( '#' );
if ( href.indexOf( '?' ) > 0 ) {
href = href + '&v=' + wc_geolocation_params.hash;
} else {
href = href + '?v=' + wc_geolocation_params.hash;
}
href = href_parts[0];
if ( typeof href_parts[1] !== 'undefined' && href_parts[1] !== null ) {
href = href + '#' + href_parts[1];
}
if ( href.indexOf( '?' ) > 0 ) {
href = href + '&v=' + geo_hash;
} else {
href = href + '?v=' + geo_hash;
}
$this.attr( 'href', href );
});
if ( typeof href_parts[1] !== 'undefined' && href_parts[1] !== null ) {
href = href + '#' + href_parts[1];
}
$this.attr( 'href', href );
});
};
var $geolocate_customer = {
url: wc_geolocation_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_customer_location' ),
type: 'GET',
success: function( response ) {
if ( response.success && response.data.hash && response.data.hash !== geo_hash ) {
$geolocation_redirect( response.data.hash );
}
}
};
/**
* Once we have a new hash, we redirect so a new version of the current page
* (with correct pricing for the current region, etc) is displayed.
*
* @param {string} hash
*/
var $geolocation_redirect = function( hash ) {
// Updates our (cookie-based) cache of the hash value. Expires in 1 hour.
Cookies.set( 'woocommerce_geo_hash', hash, { expires: 1 / 24 } );
var this_page = window.location.toString();
if ( this_page.indexOf( '?v=' ) > 0 || this_page.indexOf( '&v=' ) > 0 ) {
this_page = this_page.replace( /v=[^&]+/, 'v=' + hash );
} else if ( this_page.indexOf( '?' ) > 0 ) {
@ -39,50 +96,50 @@ jQuery( function( $ ) {
window.location = this_page;
};
var $geolocate_customer = {
url: wc_geolocation_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_customer_location' ),
type: 'GET',
success: function( response ) {
if ( response.success && response.data.hash && response.data.hash !== wc_geolocation_params.hash ) {
$geolocation_redirect( response.data.hash );
}
/**
* Updates any forms on the page so they use the current geo hash.
*/
function update_forms() {
if ( ! geo_hash ) {
return;
}
};
if ( '1' === wc_geolocation_params.is_available ) {
$.ajax( $geolocate_customer );
// Support form elements
$( 'form' ).each( function() {
var $this = $( this );
$( 'form' ).each( function () {
var $this = $( this );
var method = $this.attr( 'method' );
var hasField = $this.find('input[name="v"]').length > 0;
var hasField = $this.find( 'input[name="v"]' ).length > 0;
if ( method && 'get' === method.toLowerCase() && !hasField ) {
$this.append( '<input type="hidden" name="v" value="' + wc_geolocation_params.hash + '" />' );
if ( method && 'get' === method.toLowerCase() && ! hasField ) {
$this.append( '<input type="hidden" name="v" value="' + geo_hash + '" />' );
} else {
var href = $this.attr( 'action' );
if ( href ) {
if ( href.indexOf( '?' ) > 0 ) {
$this.attr( 'action', href + '&v=' + wc_geolocation_params.hash );
$this.attr( 'action', href + '&v=' + geo_hash );
} else {
$this.attr( 'action', href + '?v=' + wc_geolocation_params.hash );
$this.attr( 'action', href + '?v=' + geo_hash );
}
}
}
});
// Append hashes on load
$append_hashes();
}
// Get the current geo hash. If it doesn't exist, or if it doesn't match the current
// page URL, perform a geolocation request.
if ( ! get_geo_hash() || needs_refresh() ) {
$.ajax( $geolocate_customer );
}
// Page updates.
update_forms();
$append_hashes();
$( document.body ).on( 'added_to_cart', function() {
$append_hashes();
});
// Enable user to trigger manual append hashes on AJAX operations
$( document.body ).on( 'woocommerce_append_geo_hashes', function() {
$append_hashes();
});
});

View File

@ -1,26 +1,26 @@
#!/bin/sh
PROTECTED_BRANCH="master"
PROTECTED_BRANCH="trunk"
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
>&2 echo "Sorry, you are unable to push to master using a GUI client! Please use git CLI."
>&2 echo "Sorry, you are unable to push to trunk using a GUI client! Please use git CLI."
exit 1
fi
printf "%sYou're about to push to master, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)"
printf "%sYou're about to push to trunk, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)"
read -r PROCEED < /dev/tty
echo
if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then
echo "$(tput setaf 2)Brace yourself! Pushing to the master branch...$(tput sgr0)"
echo "$(tput setaf 2)Brace yourself! Pushing to the trunk branch...$(tput sgr0)"
echo
exit 0
fi
echo "$(tput setaf 2)Push to master cancelled!$(tput sgr0)"
echo "$(tput setaf 2)Push to trunk cancelled!$(tput sgr0)"
echo
exit 1
fi

View File

@ -21,7 +21,7 @@
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "2.0.1",
"woocommerce/woocommerce-admin": "2.0.2",
"woocommerce/woocommerce-blocks": "4.4.3"
},
"require-dev": {

52
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": "95f29b23c1baa50f079597378d1673f1",
"content-hash": "f24a600ea103061d766dd7b06c13e8f2",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -44,6 +44,9 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
"source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.9.1"
},
"time": "2021-02-05T19:07:06+00:00"
},
{
@ -75,6 +78,9 @@
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1"
},
"time": "2020-10-28T19:00:31+00:00"
},
{
@ -205,6 +211,10 @@
"zend",
"zikula"
],
"support": {
"issues": "https://github.com/composer/installers/issues",
"source": "https://github.com/composer/installers/tree/v1.10.0"
},
"funding": [
{
"url": "https://packagist.com",
@ -279,6 +289,10 @@
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0"
},
"time": "2019-12-19T22:59:03+00:00"
},
{
@ -353,6 +367,10 @@
"email",
"pre-processing"
],
"support": {
"issues": "https://github.com/MyIntervals/emogrifier/issues",
"source": "https://github.com/MyIntervals/emogrifier"
},
"time": "2019-12-26T19:37:31+00:00"
},
{
@ -402,6 +420,10 @@
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/master"
},
"time": "2017-02-14T16:28:37+00:00"
},
{
@ -493,20 +515,24 @@
],
"description": "Action Scheduler for WordPress and WooCommerce",
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/master"
},
"time": "2020-05-12T16:22:33+00:00"
},
{
"name": "woocommerce/woocommerce-admin",
"version": "2.0.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "b7e89c48479348847fc97ae8be8c27d068106d04"
"reference": "c4ffd90ebc72652f9d1bc8943a56d7723acc5bf4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/b7e89c48479348847fc97ae8be8c27d068106d04",
"reference": "b7e89c48479348847fc97ae8be8c27d068106d04",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/c4ffd90ebc72652f9d1bc8943a56d7723acc5bf4",
"reference": "c4ffd90ebc72652f9d1bc8943a56d7723acc5bf4",
"shasum": ""
},
"require": {
@ -538,7 +564,11 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2021-02-12T01:19:48+00:00"
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.0.2"
},
"time": "2021-02-25T07:29:24+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -585,6 +615,10 @@
"gutenberg",
"woocommerce"
],
"support": {
"issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues",
"source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v4.4.3"
},
"time": "2021-02-11T18:07:48+00:00"
}
],
@ -633,6 +667,10 @@
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
},
"time": "2020-05-03T08:27:20+00:00"
}
],
@ -648,5 +686,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -1,11 +1,11 @@
# WooCommerce `includes` files
This directory contains WooCommerce legacy code. Ideally, the code in this folder should only get the minimum required changes for bug fixing, and any new code should go in [the `src` directory](https://github.com/woocommerce/woocommerce/tree/master/src/README.md) instead.
This directory contains WooCommerce legacy code. Ideally, the code in this folder should only get the minimum required changes for bug fixing, and any new code should go in [the `src` directory](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md) instead.
## Interacting with the `src` folder
Whenever you need to get an instance of a class from the `src` directory, please don't instantiate it directly, but instead use [the container](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#the-container). To get an instance of the container itself you can use the `wc_get_container` function, for example:
Whenever you need to get an instance of a class from the `src` directory, please don't instantiate it directly, but instead use [the container](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md#the-container). To get an instance of the container itself you can use the `wc_get_container` function, for example:
```php
$container = wc_get_container();
@ -17,18 +17,18 @@ The exception to this rule might be data-only classes that could be created the
## Adding new actions and filters
Please take a look at [the considerations for creation new hooks in `src` code](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#defining-new-actions-and-filters), as they apply for `includes` code as well. The short version is that **new hooks should be introduced only if they provide a valuable extension point for plugins**, and not with the purpose of driving WooCommerce's internal logic.
Please take a look at [the considerations for creation new hooks in `src` code](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md#defining-new-actions-and-filters), as they apply for `includes` code as well. The short version is that **new hooks should be introduced only if they provide a valuable extension point for plugins**, and not with the purpose of driving WooCommerce's internal logic.
## Writing unit tests
[As it's the case for the `src` folder](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#writing-unit-tests), writing unit tests is generally mandatory if you are a WooCommerce team member or a contributor from another Automattic team, and encouraged if you are an external contributor. Tests should cover any new code (although as mentioned, adding new code in `includes` should be rare) and any modifications to existing code.
[As it's the case for the `src` folder](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md#writing-unit-tests), writing unit tests is generally mandatory if you are a WooCommerce team member or a contributor from another Automattic team, and encouraged if you are an external contributor. Tests should cover any new code (although as mentioned, adding new code in `includes` should be rare) and any modifications to existing code.
In order to make it easier to write unit tests, there are a couple of mechanisms in place that you can use:
* [The code hacker](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/CodeHacking/README.md). Pros: you don't need to do any special changes to your code to make it testable. Cons: it's a hack, the tested code is being actually modified while being loaded by the PHP engine, so not an ideal solution.
* [The code hacker](https://github.com/woocommerce/woocommerce/blob/trunk/tests/Tools/CodeHacking/README.md). Pros: you don't need to do any special changes to your code to make it testable. Cons: it's a hack, the tested code is being actually modified while being loaded by the PHP engine, so not an ideal solution.
* [The legacy proxy and the related helper methods in WC_Unit_Test_case](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#interacting-with-legacy-code): although these are intended in principle for writing tests for code in the `src` directory, they can be used for `includes` code as well. Pros: a clean approach, no hacks involved. Cons: you need to modify your code to use the proxy whenever you need to call a function or static method that makes the code difficult to test.
* [The legacy proxy and the related helper methods in WC_Unit_Test_case](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md#interacting-with-legacy-code): although these are intended in principle for writing tests for code in the `src` directory, they can be used for `includes` code as well. Pros: a clean approach, no hacks involved. Cons: you need to modify your code to use the proxy whenever you need to call a function or static method that makes the code difficult to test.
It's up to you as a contributor to decide which mechanism to use in each case. Choose wisely.

View File

@ -641,7 +641,7 @@ class WC_Admin_Addons {
self::install_woocommerce_services_addon();
break;
case 'woocommerce-payments':
self::install_woocommerce_payments_addon();
self::install_woocommerce_payments_addon( $section );
break;
default:
// Do nothing.
@ -693,9 +693,11 @@ class WC_Admin_Addons {
/**
* Install WooCommerce Payments from the Extensions screens.
*
* @param string $section Optional. Extenstions tab.
*
* @return void
*/
public static function install_woocommerce_payments_addon() {
public static function install_woocommerce_payments_addon( $section = '_featured' ) {
check_admin_referer( 'install-addon_woocommerce-payments' );
$wcpay_plugin_id = 'woocommerce-payments';
@ -704,7 +706,9 @@ class WC_Admin_Addons {
'repo-slug' => 'woocommerce-payments',
);
WC_Install::background_installer( $services_plugin_id, $wcpay_plugin );
WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin );
do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section );
wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) );
exit;

View File

@ -116,15 +116,23 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
$reports = new WC_Admin_Report();
$net_sales_link = 'admin.php?page=wc-reports&tab=orders&range=month';
$top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids=';
$report_data = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data();
if ( ! $is_wc_admin_disabled ) {
$net_sales_link = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period';
$top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products=';
}
echo '<ul class="wc_status_list">';
if ( current_user_can( 'view_woocommerce_reports' ) ) {
$report_data = $this->get_sales_report_data();
if ( $report_data ) {
?>
<li class="sales-this-month">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=orders&range=month' ) ); ?>">
<?php echo $reports->sales_sparkline( '', max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ) ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
<a href="<?php echo esc_url( admin_url( $net_sales_link ) ); ?>">
<?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, '' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
<?php
printf(
/* translators: %s: net sales */
@ -141,8 +149,8 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
if ( $top_seller && $top_seller->qty ) {
?>
<li class="best-seller-this-month">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids=' . $top_seller->product_id ) ); ?>">
<?php echo $reports->sales_sparkline( $top_seller->product_id, max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ), 'count' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
<a href="<?php echo esc_url( admin_url( $top_seller_link . $top_seller->product_id ) ); ?>">
<?php echo $this->sales_sparkline( $reports, $is_wc_admin_disabled, $top_seller->product_id, 'count' ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped ?>
<?php
printf(
/* translators: 1: top seller product title 2: top seller quantity */
@ -432,6 +440,106 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
</div>
<?php
}
/**
* Gets the sales performance data from the new WooAdmin store.
*
* @return stdClass|WP_Error|WP_REST_Response
*/
private function get_wc_admin_performance_data() {
$request = new \WP_REST_Request( 'GET', '/wc-analytics/reports/performance-indicators' );
$start_date = gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp' ) );
$end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) );
$request->set_query_params(
array(
'before' => $end_date,
'after' => $start_date,
'stats' => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold',
)
);
$response = rest_do_request( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( 200 !== $response->get_status() ) {
return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) );
}
$report_keys = array(
'net_revenue' => 'net_sales',
);
$performance_data = new stdClass();
foreach ( $response->get_data() as $indicator ) {
if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) {
$key = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart'];
$performance_data->$key = $indicator['value'];
}
}
return $performance_data;
}
/**
* Overwrites the original sparkline to use the new reports data if WooAdmin is enabled.
* Prepares a sparkline to show sales in the last X days.
*
* @param WC_Admin_Report $reports old class for getting reports.
* @param bool $is_wc_admin_disabled If WC Admin is disabled or not.
* @param int $id ID of the product to show. Blank to get all orders.
* @param string $type Type of sparkline to get. Ignored if ID is not set.
* @return string
*/
private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) {
$days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) );
if ( $is_wc_admin_disabled ) {
return $reports->sales_sparkline( $id, $days, $type );
}
$sales_endpoint = '/wc-analytics/reports/revenue/stats';
$start_date = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) );
$end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) );
$meta_key = 'net_revenue';
$params = array(
'order' => 'asc',
'interval' => 'day',
'per_page' => 100,
'before' => $end_date,
'after' => $start_date,
);
if ( $id ) {
$sales_endpoint = '/wc-analytics/reports/products/stats';
$meta_key = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold';
$params['products'] = $id;
}
$request = new \WP_REST_Request( 'GET', $sales_endpoint );
$params['fields'] = array( $meta_key );
$request->set_query_params( $params );
$response = rest_do_request( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$resp_data = $response->get_data();
$data = $resp_data['intervals'];
$sparkline_data = array();
$total = 0;
foreach ( $data as $d ) {
$total += $d['subtotals']->$meta_key;
array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) );
}
if ( 'sales' === $type ) {
/* translators: 1: total income 2: days */
$tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days );
} else {
/* translators: 1: total items sold 2: days */
$tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days );
}
return '<span class="wc_sparkline ' . ( ( 'sales' === $type ) ? 'lines' : 'bars' ) . ' tips" data-color="#777" data-tip="' . esc_attr( $tooltip ) . '" data-barwidth="' . 60 * 60 * 16 * 1000 . '" data-sparkline="' . wc_esc_json( wp_json_encode( $sparkline_data ) ) . '"></span>';
}
}
endif;

View File

@ -65,7 +65,7 @@ class WC_Admin_Help {
'content' =>
'<h2>' . __( 'Found a bug?', 'woocommerce' ) . '</h2>' .
/* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */
'<p>' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via <a href="%1$s">Github issues</a>. Ensure you read the <a href="%2$s">contribution guide</a> prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your <a href="%3$s">system status report</a>.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/master/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '</p>' .
'<p>' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via <a href="%1$s">Github issues</a>. Ensure you read the <a href="%2$s">contribution guide</a> prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your <a href="%3$s">system status report</a>.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '</p>' .
'<p><a href="https://github.com/woocommerce/woocommerce/issues/new?template=4-Bug-report.md" class="button button-primary">' . __( 'Report a bug', 'woocommerce' ) . '</a> <a href="' . admin_url( 'admin.php?page=wc-status' ) . '" class="button">' . __( 'System status', 'woocommerce' ) . '</a></p>',
)

View File

@ -6,6 +6,8 @@
* @version 3.2.0
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -37,8 +39,13 @@ class WC_Updates_Screen_Updates extends WC_Plugin_Updates {
return;
}
$version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
if ( ! is_string( $version_type ) ) {
$version_type = 'none';
}
$this->new_version = wc_clean( $updateable_plugins['woocommerce/woocommerce.php']->update->new_version );
$this->major_untested_plugins = $this->get_untested_plugins( $this->new_version, 'major' );
$this->major_untested_plugins = $this->get_untested_plugins( $this->new_version, $version_type );
if ( ! empty( $this->major_untested_plugins ) ) {
echo $this->get_extensions_modal_warning(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped

View File

@ -64,10 +64,6 @@ class WC_Settings_Products extends WC_Settings_Page {
$settings = $this->get_settings( $current_section );
WC_Admin_Settings::save_fields( $settings );
// Any time we update the product settings, we should flush the term count cache.
$tools_controller = new WC_REST_System_Status_Tools_Controller();
$tools_controller->execute_tool( 'recount_terms' );
if ( $current_section ) {
do_action( 'woocommerce_update_options_' . $this->id . '_' . $current_section );
}

View File

@ -5,16 +5,12 @@
* @package WooCommerce
*/
use Automattic\Jetpack\Constants;
defined( 'ABSPATH' ) || exit;
global $wpdb;
if ( ! defined( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
// Define if we're checking against major or minor versions.
// Since 5.0 all versions are backwards compatible, so there's no more check.
define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' );
}
$report = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
$environment = $report['environment'];
$database = $report['database'];
@ -27,7 +23,7 @@ $security = $report['security'];
$settings = $report['settings'];
$wp_pages = $report['pages'];
$plugin_updates = new WC_Plugin_Updates();
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE );
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) );
?>
<div class="updated woocommerce-message inline">
<p>

View File

@ -1273,7 +1273,6 @@ class WC_AJAX {
}
if ( ! empty( $order_item_ids ) ) {
$order_notes = array();
foreach ( $order_item_ids as $item_id ) {
$item_id = absint( $item_id );
@ -1300,6 +1299,18 @@ class WC_AJAX {
$order->calculate_taxes( $calculate_tax_args );
$order->calculate_totals( false );
/**
* Fires after order items are removed.
*
* @since 5.2.0
*
* @param int $item_id WC item ID.
* @param WC_Order_Item|false $item As returned by $order->get_item( $item_id ).
* @param bool|array|WP_Error $changed_store Result of wc_maybe_adjust_line_item_product_stock().
* @param bool|WC_Order|WC_Order_Refund $order As returned by wc_get_order().
*/
do_action( 'woocommerce_ajax_order_items_removed', $item_id, $item, $changed_stock, $order );
// Get HTML to return.
ob_start();
include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';

View File

@ -26,6 +26,7 @@ class WC_Cache_Helper {
add_filter( 'nocache_headers', array( __CLASS__, 'additional_nocache_headers' ), 10 );
add_action( 'shutdown', array( __CLASS__, 'delete_transients_on_shutdown' ), 10 );
add_action( 'template_redirect', array( __CLASS__, 'geolocation_ajax_redirect' ) );
add_action( 'wc_ajax_update_order_review', array( __CLASS__, 'update_geolocation_hash' ), 5 );
add_action( 'admin_notices', array( __CLASS__, 'notices' ) );
add_action( 'delete_version_transients', array( __CLASS__, 'delete_version_transients' ), 10 );
add_action( 'wp', array( __CLASS__, 'prevent_caching' ) );
@ -190,6 +191,24 @@ class WC_Cache_Helper {
}
}
/**
* Updates the `woocommerce_geo_hash` cookie, which is used to help ensure we display
* the correct pricing etc to customers, according to their billing country.
*
* Note that:
*
* A) This only sets the cookie if the default customer address is set to "Geolocate (with
* Page Caching Support)".
*
* B) It is hooked into the `wc_ajax_update_order_review` action, which has the benefit of
* ensuring we update the cookie any time the billing country is changed.
*/
public static function update_geolocation_hash() {
if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) {
wc_setcookie( 'woocommerce_geo_hash', static::geolocation_ajax_get_location_hash(), time() + HOUR_IN_SECONDS );
}
}
/**
* Get transient version.
*

View File

@ -1120,6 +1120,20 @@ class WC_Cart extends WC_Legacy_Cart {
}
}
// Validate variation ID.
if (
0 < $variation_id && // Only check if there's any variation_id.
(
! $product_data->is_type( 'variation' ) || // Check if isn't a variation, it suppose to be a variation at this point.
$product_data->get_parent_id() !== $product_id // Check if belongs to the selected variable product.
)
) {
$product = wc_get_product( $product_id );
/* translators: 1: product link, 2: product name */
throw new Exception( sprintf( __( 'The selected product isn\'t a variation of %2$s, please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( $product->get_permalink() ), esc_html( $product->get_name() ) ) );
}
// Load cart item data - may be added by other plugins.
$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity );

View File

@ -398,7 +398,12 @@ class WC_Frontend_Scripts {
self::enqueue_script( 'wc-single-product' );
}
if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) {
// Only enqueue the geolocation script if the Default Current Address is set to "Geolocate
// (with Page Caching Support) and outside of the cart, checkout, account and customizer preview.
if (
'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' )
&& ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() )
) {
$ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent.
if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) {
@ -473,8 +478,6 @@ class WC_Frontend_Scripts {
$params = array(
'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
'home_url' => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility.
'is_available' => ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) ? '1' : '0',
'hash' => isset( $_GET['v'] ) ? wc_clean( wp_unslash( $_GET['v'] ) ) : '', // WPCS: input var ok, CSRF ok.
);
break;
case 'wc-single-product':

View File

@ -166,6 +166,9 @@ class WC_Tracker {
// Cart & checkout tech (blocks or shortcodes).
$data['cart_checkout'] = self::get_cart_checkout_info();
// WooCommerce Admin info.
$data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no';
return apply_filters( 'woocommerce_tracker_data', $data );
}

View File

@ -249,6 +249,18 @@ final class WooCommerce {
$this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' );
$this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' );
$this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION );
/** Define if we're checking against major, minor or no versions in the following places:
* - plugin screen in WP Admin (displaying extra warning when updating to new major versions)
* - System Status Report ('Installed version not tested with active version of WooCommerce' warning)
* - core update screen in WP Admin (displaying extra warning when updating to new major versions)
* - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins
* that don't declare compatibility, the auto-update is disabled)
*
* We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed.
* The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report.
*/
$this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' );
}
/**

View File

@ -272,9 +272,6 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$product->apply_changes();
// Any time we update the product, we should flush the term count cache.
$tools_controller = new WC_REST_System_Status_Tools_Controller();
$tools_controller->execute_tool( 'recount_terms' );
do_action( 'woocommerce_update_product', $product->get_id(), $product );
}

View File

@ -24,9 +24,6 @@ class WC_Twenty_Twenty_One {
remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );
add_action( 'woocommerce_before_main_content', array( __CLASS__, 'output_content_wrapper' ), 10 );
add_action( 'woocommerce_after_main_content', array( __CLASS__, 'output_content_wrapper_end' ), 10 );
// This theme doesn't have a traditional sidebar.
remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );
@ -34,7 +31,7 @@ class WC_Twenty_Twenty_One {
add_filter( 'woocommerce_enqueue_styles', array( __CLASS__, 'enqueue_styles' ) );
// Enqueue wp-admin compatibility styles.
add_action( 'admin_enqueue_scripts', array( __CLASS__ , 'enqueue_admin_styles' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_styles' ) );
// Register theme features.
add_theme_support( 'wc-product-gallery-zoom' );
@ -50,22 +47,6 @@ class WC_Twenty_Twenty_One {
}
/**
* Open the Twenty Twenty One wrapper.
*/
public static function output_content_wrapper() {
echo '<section id="primary" class="content-area">';
echo '<main id="main" class="site-main">';
}
/**
* Close the Twenty Twenty One wrapper.
*/
public static function output_content_wrapper_end() {
echo '</main>';
echo '</section>';
}
/**
* Enqueue CSS for this theme.
*

View File

@ -21,6 +21,7 @@ class WC_Extensions_Tracking {
add_action( 'woocommerce_helper_connected', array( $this, 'track_helper_connection_complete' ) );
add_action( 'woocommerce_helper_disconnected', array( $this, 'track_helper_disconnected' ) );
add_action( 'woocommerce_helper_subscriptions_refresh', array( $this, 'track_helper_subscriptions_refresh' ) );
add_action( 'woocommerce_addon_installed', array( $this, 'track_addon_install' ), 10, 2 );
}
/**
@ -76,4 +77,21 @@ class WC_Extensions_Tracking {
public function track_helper_subscriptions_refresh() {
WC_Tracks::record_event( 'extensions_subscriptions_update' );
}
/**
* Send a Tracks event when addon is installed via the Extensions page.
*
* @param string $addon_id Addon slug.
* @param string $section Extensions tab.
*/
public function track_addon_install( $addon_id, $section ) {
$properties = array(
'context' => 'extensions',
'section' => $section,
);
if ( 'woocommerce-payments' === $addon_id ) {
WC_Tracks::record_event( 'woocommerce_payments_install', $properties );
}
}
}

View File

@ -2255,7 +2255,11 @@ function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) {
$new_version = wc_clean( $plugin->new_version );
$plugin_updates = new WC_Plugin_Updates();
$untested_plugins = $plugin_updates->get_untested_plugins( $new_version, 'major' );
$version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
if ( ! is_string( $version_type ) ) {
$version_type = 'none';
}
$untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type );
if ( ! empty( $untested_plugins ) ) {
return false;
}

View File

@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
Requires at least: 5.4
Tested up to: 5.6
Requires PHP: 7.0
Stable tag: 4.9.2
Stable tag: 5.0.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -160,6 +160,6 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
= 5.1.0 2021-03-xx =
= 5.2.0 2021-04-xx =
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/changelog.txt).
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt).

View File

@ -2,4 +2,4 @@
All the code in this directory (and hence in the `Automattic\WooCommerce\Internal` namespace) is internal WooCommerce infrastructure code and not intended to be used by plugins. The important thing that this implies is that **backwards compatibility of the public surface for classes in this namespace is not guaranteed in future releases of WooCommerce**.
Therefore **plugin developers should never use classes in this namespace directly in their code**. See [the README file for the src folder](https://github.com/woocommerce/woocommerce/blob/master/src/README.md#the-internal-namespace) for more detailed guidance.
Therefore **plugin developers should never use classes in this namespace directly in their code**. See [the README file for the src folder](https://github.com/woocommerce/woocommerce/blob/trunk/src/README.md#the-internal-namespace) for more detailed guidance.

View File

@ -26,7 +26,7 @@
This directory is home to new WooCommerce class files under the `Automattic\WooCommerce` namespace using [PSR-4](https://www.php-fig.org/psr/psr-4/) file naming. This is to take full advantage of autoloading.
Ideally, all the new code for WooCommerce should consist of classes following the PSR-4 naming and living in this directory, and the code in [the `includes` directory](https://github.com/woocommerce/woocommerce/tree/master/includes/README.md) should receive the minimum amount of changes required for bug fixing. This will not always be possible but that should be the rule of thumb.
Ideally, all the new code for WooCommerce should consist of classes following the PSR-4 naming and living in this directory, and the code in [the `includes` directory](https://github.com/woocommerce/woocommerce/tree/trunk/includes/README.md) should receive the minimum amount of changes required for bug fixing. This will not always be possible but that should be the rule of thumb.
A [PSR-11](https://www.php-fig.org/psr/psr-11/) container is in place for registering and resolving the classes in this directory by using the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern. There are tools in place to interact with legacy code (and code outside the `src`directory in general) in a way that makes it easy to write unit tests.
@ -281,7 +281,7 @@ What this implies for you as developer depends on what type of contribution are
Here by "legacy code" we refer mainly to the old WooCommerce code in the `includes` directory, but the mechanisms described in this section are useful for dealing with any code outside the `src` directory.
The code in the `src` directory can for sure interact directly with legacy code. A function needs to be called? Call it. You need an instance of an object? Instantiate it. The problem is that this makes the code difficult to test: it's not easy to mock functions (unless you use [hacks](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/CodeHacking/README.md), or objects that are instantiated directly with `new` or whose instance is retrieved via a `TheClass::instance()` method).
The code in the `src` directory can for sure interact directly with legacy code. A function needs to be called? Call it. You need an instance of an object? Instantiate it. The problem is that this makes the code difficult to test: it's not easy to mock functions (unless you use [hacks](https://github.com/woocommerce/woocommerce/blob/trunk/tests/Tools/CodeHacking/README.md), or objects that are instantiated directly with `new` or whose instance is retrieved via a `TheClass::instance()` method).
But we want the WooCommerce code base (and especially the code in `src`) to be well covered by unit tests, and so there are mechanisms in place to interact with legacy code while keeping the code testable.
@ -355,7 +355,7 @@ $this->register_legacy_proxy_function_mocks(
Of course, for the cases where no mocks are defined `MockableLegacyProxy` works the same way as `LegacyProxy`.
Please see [the code of the MockableLegacyProxy class](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/DependencyManagement/MockableLegacyProxy.php) and [its unit tests](https://github.com/woocommerce/woocommerce/blob/master/tests/php/src/Proxies/MockableLegacyProxyTest.php) for more detailed usage instructions and examples.
Please see [the code of the MockableLegacyProxy class](https://github.com/woocommerce/woocommerce/blob/trunk/tests/Tools/DependencyManagement/MockableLegacyProxy.php) and [its unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/tests/php/src/Proxies/MockableLegacyProxyTest.php) for more detailed usage instructions and examples.
### But how does `get_instance_of` work?
@ -363,7 +363,7 @@ We use a container to resolve instances of classes in the `src` directory, but h
This is a mostly ad-hoc process. When a class has a special way to be instantiated or retrieved (e.g. a static `instance` method), then that is used; otherwise the method fallbacks to simply creating a new instance of the class using `new`.
This means that the `get_instance_of` method will most likely need to evolve over time to cover additional special cases. Take a look at the method code in [LegacyProxy](https://github.com/woocommerce/woocommerce/blob/master/src/Proxies/LegacyProxy.php) for details on how to properly make changes to the method.
This means that the `get_instance_of` method will most likely need to evolve over time to cover additional special cases. Take a look at the method code in [LegacyProxy](https://github.com/woocommerce/woocommerce/blob/trunk/src/Proxies/LegacyProxy.php) for details on how to properly make changes to the method.
### Creating specialized proxies

View File

@ -1,6 +1,6 @@
# WooCommerce Tests
This document discusses unit tests. See [the e2e README](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e) to learn how to setup testing environment for running e2e tests and run them.
This document discusses unit tests. See [the e2e README](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e) to learn how to setup testing environment for running e2e tests and run them.
## Table of contents
@ -108,7 +108,7 @@ Each test file should correspond to an associated source file and be named accor
* When testing functions: use one test file per functions group file, for example `wc-formatting-functions-test.php` for code in the `wc-formatting-functions.php` file.
See also [the guidelines for writing unit tests for `src` code](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#writing-unit-tests) and [the guidelines for `includes` code](https://github.com/woocommerce/woocommerce/tree/master/includes/README.md#writing-unit-tests).
See also [the guidelines for writing unit tests for `src` code](https://github.com/woocommerce/woocommerce/tree/trunk/src/README.md#writing-unit-tests) and [the guidelines for `includes` code](https://github.com/woocommerce/woocommerce/tree/trunk/includes/README.md#writing-unit-tests).
General guidelines for all the unit tests:

View File

@ -101,7 +101,7 @@ Puppeteer will still automatically download Chromium when needed.
- `cd` to the WooCommerce plugin folder
- `git checkout master` or checkout the branch where you need to run tests
- `git checkout trunk` or checkout the branch where you need to run tests
- Run `nvm use`
@ -267,7 +267,7 @@ In the WooCommerce Core repository the tests are in `tests/e2e/core-tests/specs/
The following packages are used in write tests:
- `@automattic/puppeteer-utils` - utilities and configuration for running puppeteer against WordPress. See details in the [package's repository](https://github.com/Automattic/puppeteer-utils).
- `@woocommerce/e2e-utils` - this package contains utilities to simplify writing e2e tests specific to WooCommmerce. See details in the [package's repository](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/utils).
- `@woocommerce/e2e-utils` - this package contains utilities to simplify writing e2e tests specific to WooCommmerce. See details in the [package's repository](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/utils).
### Creating test structure

View File

@ -1,5 +1,11 @@
# Unreleased
## Added
- Support for the external product type.
- Added support for grouped product type
- Support for coupons.
# 0.1.1
## Breaking Changes
@ -14,7 +20,7 @@
## Changes
- Added a tranformation layer between API responses and internal models
- Added a transformation layer between API responses and internal models
## Fixed

View File

@ -3,7 +3,7 @@
"version": "0.1.1",
"author": "Automattic",
"description": "A simple interface for interacting with a WooCommerce installation.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/api/README.md",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/api/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"

View File

@ -0,0 +1,243 @@
import { Model } from '../model';
import { HTTPClient } from '../../http';
import { couponRESTRepository } from '../../repositories';
import {
ModelRepositoryParams,
CreatesModels,
ListsModels,
ReadsModels,
UpdatesModels,
DeletesModels,
} from '../../framework';
import {
CouponUpdateParams,
} from './shared';
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type CouponRepositoryParams = ModelRepositoryParams< Coupon, never, never, CouponUpdateParams >;
/**
* An interface for creating coupons using the repository.
*
* @typedef CreatesCoupons
* @alias CreatesModels.<Coupon>
*/
export type CreatesCoupons = CreatesModels< CouponRepositoryParams >;
/**
* An interface for reading coupons using the repository.
*
* @typedef ReadsCoupons
* @alias ReadsModels.<Coupon>
*/
export type ReadsCoupons = ReadsModels< CouponRepositoryParams >;
/**
* An interface for updating coupons using the repository.
*
* @typedef UpdatesCoupons
* @alias UpdatesModels.<Coupon>
*/
export type UpdatesCoupons = UpdatesModels< CouponRepositoryParams >;
/**
* An interface for listing coupons using the repository.
*
* @typedef ListsCoupons
* @alias ListsModels.<Coupon>
*/
export type ListsCoupons = ListsModels< CouponRepositoryParams >;
/**
* An interface for deleting coupons using the repository.
*
* @typedef DeletesCoupons
* @alias DeletesModels.<Coupons>
*/
export type DeletesCoupons = DeletesModels< CouponRepositoryParams >;
/**
* The type of discount that is available for the coupon.
*/
type DiscountType = 'percent' | 'fixed_cart' | 'fixed_product' | string;
/**
* A coupon object.
*/
export class Coupon extends Model {
/**
* The coupon code.
*
* @type {string}
*/
public readonly code: string = '';
/**
* The amount of the discount, must always be numeric.
*
* @type {string}
*/
public readonly amount: string = '';
/**
* The date the coupon was created.
*
* @type {Date}
*/
public readonly dateCreated: Date = new Date();
/**
* The date the coupon was modified.
*
* @type {Date}
*/
public readonly dateModified: Date = new Date();
/**
* The discount type for the coupon.
*
* @type {string}
*/
public readonly discountType: string | DiscountType = '';
/**
* The description of the coupon.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The date the coupon expires.
*
* @type {Date}
*/
public readonly dateExpires: Date = new Date();
/**
* The number of times the coupon has already been used.
*
* @type {number}
*/
public readonly usageCount: Number = 0;
/**
* Flags if the coupon can only be used on its own and not combined with other coupons.
*
* @type {boolean}
*/
public readonly individualUse: boolean = false;
/**
* List of Product IDs that the coupon can be applied to.
*
* @type {ReadonlyArray.<number>}
*/
public readonly productIds: Array<number> = [];
/**
* List of Product IDs that the coupon cannot be applied to.
*
* @type {ReadonlyArray.<number>}
*/
public readonly excludedProductIds: Array<number> = [];
/**
* How many times the coupon can be used.
*
* @type {number}
*/
public readonly usageLimit: Number = -1;
/**
* How many times the coupon can be used per customer.
*
* @type {number}
*/
public readonly usageLimitPerUser: Number = -1;
/**
* Max number of items in the cart the coupon can be applied to.
*
* @type {number}
*/
public readonly limitUsageToXItems: Number = -1;
/**
* Flags if the free shipping option requires a coupon. This coupon will enable free shipping.
*
* @type {boolean}
*/
public readonly freeShipping: boolean = false;
/**
* List of Category IDs the coupon applies to.
*
* @type {ReadonlyArray.<number>}
*/
public readonly productCategories: Array<number> = [];
/**
* List of Category IDs the coupon does not apply to.
*
* @type {ReadonlyArray.<number>}
*/
public readonly excludedProductCategories: Array<number> = [];
/**
* Flags if the coupon applies to items on sale.
*
* @type {boolean}
*/
public readonly excludeSaleItems: boolean = false;
/**
* The minimum order amount that needs to be in the cart before the coupon applies.
*
* @type {string}
*/
public readonly minimumAmount: string = '';
/**
* The maximum order amount allowed when using the coupon.
*
* @type {string}
*/
public readonly maximumAmount: string = '';
/**
* List of email addresses that can use this coupon.
*
* @type {ReadonlyArray.<string>}
*/
public readonly emailRestrictions: Array<string> = [];
/**
* List of user IDs (or guest emails) that have used the coupon.
*
* @type {ReadonlyArray.<string>}
*/
public readonly usedBy: Array<string> = [];
/**
* Creates a new coupon instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties?: Partial< Coupon > ) {
super();
Object.assign( this, properties );
}
/**
* Returns the repository for interacting with this type of model.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof couponRESTRepository > {
return couponRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,2 @@
export * from './coupon';
export * from './shared';

View File

@ -0,0 +1 @@
export * from './update-params';

View File

@ -0,0 +1,7 @@
/**
* Coupon properties that can be updated
*/
export type CouponUpdateParams = 'code' | 'amount' | 'description' | 'discountType' | 'dateExpires' | 'individualUse'
| 'usageCount' | 'productIds' | 'excludedProductIds' | 'usageLimit' | 'usageLimitPerUser' | 'limitUsageToXItems'
| 'freeShipping' | 'productCategories' | 'excludedProductCategories' | 'excludeSaleItems' | 'minimumAmount'
| 'maximumAmount' | 'emailRestrictions';

View File

@ -2,3 +2,4 @@ export * from './products';
export * from './model';
export * from './settings';
export * from './shared-types';
export * from './coupons';

View File

@ -1,4 +1,5 @@
import { AbstractProductData } from './data';
import { ModelID } from '../../model';
import {
CatalogVisibility,
ProductTerm,
@ -10,6 +11,21 @@ import {
*/
export type ProductSearchParams = { search: string };
/**
* The base product URL.
*
* @return {string} RESTful Url.
*/
export const baseProductURL = () => '/wc/v3/products/';
/**
* A common product URL builder.
*
* @param {ModelID} id the id of the product.
* @return {string} RESTful Url.
*/
export const buildProductURL = ( id: ModelID ) => baseProductURL() + id;
/**
* The base for all product types.
*/

View File

@ -0,0 +1,22 @@
import { Model } from '../../model';
/**
* The base for external products.
*/
abstract class AbstractProductExternal extends Model {
/**
* The product's button text.
*
* @type {string}
*/
public readonly buttonText: string = ''
/**
* The product's external URL.
*
* @type {string}
*/
public readonly externalUrl: string = ''
}
export interface IProductExternal extends AbstractProductExternal {}

View File

@ -0,0 +1,15 @@
import { Model } from '../../model';
/**
* The base for cross sells.
*/
abstract class AbstractProductGrouped extends Model {
/**
* An array of grouped product ids.
*
* @type {ReadonlyArray.<number>}
*/
public readonly groupedProducts: Array<number> = [];
}
export interface IProductGrouped extends AbstractProductGrouped {}

View File

@ -2,7 +2,10 @@ export * from './common';
export * from './cross-sell';
export * from './data';
export * from './delivery';
export * from './external';
export * from './grouped';
export * from './inventory';
export * from './price';
export * from './sales-tax';
export * from './shipping';
export * from './upsell';

View File

@ -0,0 +1,57 @@
import { Model } from '../../model';
/**
* The base for price properties.
*/
abstract class AbstractProductPrice extends Model {
/**
* The current price of the product.
*
* @type {string}
*/
public readonly price: string = '';
/**
* The rendered HTML for the current price of the product.
*
* @type {string}
*/
public readonly priceHtml: string = '';
/**
* The regular price of the product when not discounted.
*
* @type {string}
*/
public readonly regularPrice: string = '';
/**
* Indicates whether or not the product is currently on sale.
*
* @type {boolean}
*/
public readonly onSale: boolean = false;
/**
* The price of the product when on sale.
*
* @type {string}
*/
public readonly salePrice: string = '';
/**
* The GMT datetime when the product should start to be on sale.
*
* @type {Date|null}
*/
public readonly saleStart: Date | null = null;
/**
* The GMT datetime when the product should no longer be on sale.
*
* @type {Date|null}
*/
public readonly saleEnd: Date | null = null;
}
export interface IProductPrice extends AbstractProductPrice {}

View File

@ -0,0 +1,139 @@
import {
AbstractProduct,
IProductCommon,
IProductExternal,
IProductPrice,
IProductSalesTax,
IProductUpSells,
ProductSearchParams,
} from './abstract';
import {
ProductCommonUpdateParams,
ProductExternalUpdateParams,
ProductPriceUpdateParams,
ProductSalesTaxUpdateParams,
ProductUpSellUpdateParams,
Taxability,
} from './shared';
import { HTTPClient } from '../../http';
import { externalProductRESTRepository } from '../../repositories';
import {
CreatesModels,
DeletesModels,
ListsModels,
ModelRepositoryParams,
ReadsModels,
UpdatesModels,
} from '../../framework';
/**
* The parameters that external products can update.
*/
type ExternalProductUpdateParams = ProductCommonUpdateParams
& ProductExternalUpdateParams
& ProductPriceUpdateParams
& ProductSalesTaxUpdateParams
& ProductUpSellUpdateParams;
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type ExternalProductRepositoryParams =
ModelRepositoryParams< ExternalProduct, never, ProductSearchParams, ExternalProductUpdateParams >;
/**
* An interface for listing external products using the repository.
*
* @typedef ListsExternalProducts
* @alias ListsModels.<ExternalProduct>
*/
export type ListsExternalProducts = ListsModels< ExternalProductRepositoryParams >;
/**
* An interface for external simple products using the repository.
*
* @typedef CreatesExternalProducts
* @alias CreatesModels.<ExternalProduct>
*/
export type CreatesExternalProducts = CreatesModels< ExternalProductRepositoryParams >;
/**
* An interface for reading external products using the repository.
*
* @typedef ReadsExternalProducts
* @alias ReadsModels.<ExternalProduct>
*/
export type ReadsExternalProducts = ReadsModels< ExternalProductRepositoryParams >;
/**
* An interface for updating external products using the repository.
*
* @typedef UpdatesExternalProducts
* @alias UpdatesModels.<ExternalProduct>
*/
export type UpdatesExternalProducts = UpdatesModels< ExternalProductRepositoryParams >;
/**
* An interface for deleting external products using the repository.
*
* @typedef DeletesExternalProducts
* @alias DeletesModels.<ExternalProduct>
*/
export type DeletesExternalProducts = DeletesModels< ExternalProductRepositoryParams >;
/**
* The base for the external product object.
*/
export class ExternalProduct extends AbstractProduct implements
IProductCommon,
IProductExternal,
IProductPrice,
IProductSalesTax,
IProductUpSells {
/**
* @see ./abstracts/external.ts
*/
public readonly buttonText: string = ''
public readonly externalUrl: string = ''
/**
* @see ./abstracts/price.ts
*/
public readonly price: string = '';
public readonly priceHtml: string = '';
public readonly regularPrice: string = '';
public readonly onSale: boolean = false;
public readonly salePrice: string = '';
public readonly saleStart: Date | null = null;
public readonly saleEnd: Date | null = null;
/**
* @see ./abstracts/upsell.ts
*/
public readonly upSellIds: Array<number> = [];
/**
* @see ./abstracts/sales-tax.ts
*/
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
public readonly taxClass: string = '';
/**
* Creates a new simple product instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties?: Partial< ExternalProduct > ) {
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.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof externalProductRESTRepository > {
return externalProductRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,112 @@
import {
AbstractProduct,
IProductCommon,
IProductGrouped,
IProductUpSells,
ProductSearchParams,
} from './abstract';
import {
ProductCommonUpdateParams,
ProductGroupedUpdateParams,
ProductUpSellUpdateParams,
} from './shared';
import { HTTPClient } from '../../http';
import { groupedProductRESTRepository } from '../../repositories';
import {
CreatesModels,
DeletesModels,
ListsModels,
ModelRepositoryParams,
ReadsModels,
UpdatesModels,
} from '../../framework';
/**
* The parameters that Grouped products can update.
*/
type GroupedProductUpdateParams = ProductCommonUpdateParams
& ProductGroupedUpdateParams
& ProductUpSellUpdateParams;
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type GroupedProductRepositoryParams =
ModelRepositoryParams< GroupedProduct, never, ProductSearchParams, GroupedProductUpdateParams >;
/**
* An interface for listing Grouped products using the repository.
*
* @typedef ListsGroupedProducts
* @alias ListsModels.<GroupedProduct>
*/
export type ListsGroupedProducts = ListsModels< GroupedProductRepositoryParams >;
/**
* An interface for creating Grouped products using the repository.
*
* @typedef CreatesGroupedProducts
* @alias CreatesModels.<GroupedProduct>
*/
export type CreatesGroupedProducts = CreatesModels< GroupedProductRepositoryParams >;
/**
* An interface for reading Grouped products using the repository.
*
* @typedef ReadsGroupedProducts
* @alias ReadsModels.<GroupedProduct>
*/
export type ReadsGroupedProducts = ReadsModels< GroupedProductRepositoryParams >;
/**
* An interface for updating Grouped products using the repository.
*
* @typedef UpdatesGroupedProducts
* @alias UpdatesModels.<GroupedProduct>
*/
export type UpdatesGroupedProducts = UpdatesModels< GroupedProductRepositoryParams >;
/**
* An interface for deleting Grouped products using the repository.
*
* @typedef DeletesGroupedProducts
* @alias DeletesModels.<GroupedProduct>
*/
export type DeletesGroupedProducts = DeletesModels< GroupedProductRepositoryParams >;
/**
* The base for the Grouped product object.
*/
export class GroupedProduct extends AbstractProduct implements
IProductCommon,
IProductGrouped,
IProductUpSells {
/**
* @see ./abstracts/grouped.ts
*/
public readonly groupedProducts: Array<number> = [];
/**
* @see ./abstracts/upsell.ts
*/
public readonly upSellIds: Array<number> = [];
/**
* Creates a new Grouped product instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties?: Partial< GroupedProduct > ) {
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.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof groupedProductRESTRepository > {
return groupedProductRESTRepository( httpClient );
}
}

View File

@ -1,5 +1,7 @@
export * from './abstract';
export * from './shared';
export * from './simple-product';
export * from './grouped-product';
export * from './external-product';
export * from './variation';
export * from './variable-product';

View File

@ -28,6 +28,12 @@ export type ProductCommonUpdateParams = 'name' | 'slug' | 'shortDescription'
*/
export type ProductCrossUpdateParams = 'crossSellIds';
/**
* Price properties.
*/
export type ProductPriceUpdateParams = 'price' | 'priceHtml' | 'regularPrice'
| 'salePrice' | 'saleStart' | 'saleEnd';
/**
* Upsells property.
*/
@ -36,12 +42,12 @@ export type ProductUpSellUpdateParams = 'upSellIds';
/**
* Properties exclusive to the External product type.
*/
export type ProductExternalTypeUpdateParams = 'buttonText' | 'externalUrl';
export type ProductExternalUpdateParams = 'buttonText' | 'externalUrl';
/**
* Properties exclusive to the Grouped product type.
*/
export type ProductGroupedTypeUpdateParams = 'groupedProducts';
export type ProductGroupedUpdateParams = 'groupedProducts';
/**
* Properties related to tracking inventory.

View File

@ -4,17 +4,19 @@ import {
IProductCrossSells,
IProductDelivery,
IProductInventory,
IProductPrice,
IProductSalesTax,
IProductShipping,
IProductUpSells,
ProductSearchParams,
} from './abstract';
import {
ProductCommonUpdateParams,
ProductCrossUpdateParams,
ProductDeliveryUpdateParams,
ProductInventoryUpdateParams,
ProductCommonUpdateParams,
ProductPriceUpdateParams,
ProductSalesTaxUpdateParams,
ProductCrossUpdateParams,
ProductShippingUpdateParams,
ProductUpSellUpdateParams,
ProductDownload,
@ -40,6 +42,7 @@ type SimpleProductUpdateParams = ProductDeliveryUpdateParams
& ProductCommonUpdateParams
& ProductCrossUpdateParams
& ProductInventoryUpdateParams
& ProductPriceUpdateParams
& ProductSalesTaxUpdateParams
& ProductShippingUpdateParams
& ProductUpSellUpdateParams;
@ -97,6 +100,7 @@ export class SimpleProduct extends AbstractProduct implements
IProductCrossSells,
IProductDelivery,
IProductInventory,
IProductPrice,
IProductSalesTax,
IProductShipping,
IProductUpSells {
@ -131,6 +135,17 @@ export class SimpleProduct extends AbstractProduct implements
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
/**
* @see ./abstracts/price.ts
*/
public readonly price: string = '';
public readonly priceHtml: string = '';
public readonly regularPrice: string = '';
public readonly onSale: boolean = false;
public readonly salePrice: string = '';
public readonly saleStart: Date | null = null;
public readonly saleEnd: Date | null = null;
/**
* @see ./abstracts/sales-tax.ts
*/

View File

@ -0,0 +1,55 @@
import { HTTPClient } from '../../../http';
import {
ModelRepository,
} from '../../../framework';
import {
ModelID,
Coupon,
CouponRepositoryParams,
ListsCoupons,
ReadsCoupons,
UpdatesCoupons,
CreatesCoupons,
DeletesCoupons,
} from '../../../models';
import {
restList,
restCreate,
restRead,
restUpdate,
restDelete,
} from '../shared';
import { createCouponTransformer } from './transformer';
/**
* 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 {
* CreatesCoupons|
* ListsCoupons|
* ReadsCoupons|
* UpdatesCoupons |
* DeletesCoupons
* } The created repository.
*/
export default function couponRESTRepository( httpClient: HTTPClient ): CreatesCoupons
& ListsCoupons
& ReadsCoupons
& UpdatesCoupons
& DeletesCoupons {
const buildURL = ( id: ModelID ) => '/wc/v3/coupons/' + id;
// Using `?force=true` permanently deletes the coupon
const buildDeleteUrl = ( id: ModelID ) => `/wc/v3/coupons/${ id }?force=true`;
const transformer = createCouponTransformer();
return new ModelRepository(
restList< CouponRepositoryParams >( () => '/wc/v3/coupons', Coupon, httpClient, transformer ),
restCreate< CouponRepositoryParams >( () => '/wc/v3/coupons', Coupon, httpClient, transformer ),
restRead< CouponRepositoryParams >( buildURL, Coupon, httpClient, transformer ),
restUpdate< CouponRepositoryParams >( buildURL, Coupon, httpClient, transformer ),
restDelete< CouponRepositoryParams >( buildDeleteUrl, httpClient ),
);
}

View File

@ -0,0 +1,3 @@
import couponRESTRepository from './coupon';
export { couponRESTRepository };

View File

@ -0,0 +1,64 @@
import {
IgnorePropertyTransformation,
KeyChangeTransformation,
ModelTransformer,
PropertyType,
PropertyTypeTransformation,
} from '../../../framework';
import { Coupon } from '../../../models';
/**
* Creates a transformer for a coupon object.
*
* @return {ModelTransformer} The created transformer.
*/
export function createCouponTransformer(): ModelTransformer< Coupon > {
return new ModelTransformer(
[
new IgnorePropertyTransformation( [ 'date_created', 'date_modified' ] ),
new PropertyTypeTransformation(
{
code: PropertyType.String,
amount: PropertyType.String,
dateCreated: PropertyType.Date,
dateModified: PropertyType.Date,
discountType: PropertyType.String,
dateExpires: PropertyType.Date,
usageCount: PropertyType.Integer,
individualUse: PropertyType.Boolean,
usageLimit: PropertyType.Integer,
usageLimitPerUser: PropertyType.Integer,
limitUsageToXItems: PropertyType.Integer,
freeShipping: PropertyType.Boolean,
excludeSaleItems: PropertyType.Boolean,
minimumAmount: PropertyType.String,
maximumAmount: PropertyType.String,
},
),
new KeyChangeTransformation< Coupon >(
{
dateCreated: 'date_created_gmt',
dateModified: 'date_modified_gmt',
discountType: 'discount_type',
dateExpires: 'date_expires',
usageCount: 'usage_count',
individualUse: 'individual_use',
productIds: 'product_ids',
excludedProductIds: 'excluded_product_ids',
usageLimit: 'usage_limit',
usageLimitPerUser: 'usage_limit_per_user',
limitUsageToXItems: 'limit_usage_to_x_items',
freeShipping: 'free_shipping',
productCategories: 'product_categories',
excludedProductCategories: 'excluded_product_categories',
excludeSaleItems: 'exclude_sale_items',
minimumAmount: 'minimum_amount',
maximumAmount: 'maximum_amount',
emailRestrictions: 'email_restrictions',
usedBy: 'used_by',
},
),
],
);
}

View File

@ -1,2 +1,3 @@
export * from './products';
export * from './settings';
export * from './coupons';

View File

@ -0,0 +1,66 @@
import { HTTPClient } from '../../../http';
import { ModelRepository } from '../../../framework';
import {
baseProductURL,
buildProductURL,
ExternalProduct,
CreatesExternalProducts,
DeletesExternalProducts,
ListsExternalProducts,
ReadsExternalProducts,
ExternalProductRepositoryParams,
UpdatesExternalProducts,
} from '../../../models';
import {
createProductTransformer,
createProductExternalTransformation,
createProductPriceTransformation,
createProductSalesTaxTransformation,
createProductUpSellsTransformation,
} from './shared';
import {
restCreate,
restDelete,
restList,
restRead,
restUpdate,
} from '../shared';
/**
* 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 {
* ListsExternalProducts|
* CreatesExternalProducts|
* ReadsExternalProducts|
* UpdatesExternalProducts|
* DeletesExternalProducts
* } The created repository.
*/
export function externalProductRESTRepository( httpClient: HTTPClient ): ListsExternalProducts
& CreatesExternalProducts
& ReadsExternalProducts
& UpdatesExternalProducts
& DeletesExternalProducts {
const external = createProductExternalTransformation();
const price = createProductPriceTransformation();
const salesTax = createProductSalesTaxTransformation();
const upsells = createProductUpSellsTransformation();
const transformations = [
...external,
...price,
...salesTax,
...upsells,
];
const transformer = createProductTransformer<ExternalProduct>( 'external', transformations );
return new ModelRepository(
restList< ExternalProductRepositoryParams >( baseProductURL, ExternalProduct, httpClient, transformer ),
restCreate< ExternalProductRepositoryParams >( baseProductURL, ExternalProduct, httpClient, transformer ),
restRead< ExternalProductRepositoryParams >( buildProductURL, ExternalProduct, httpClient, transformer ),
restUpdate< ExternalProductRepositoryParams >( buildProductURL, ExternalProduct, httpClient, transformer ),
restDelete< ExternalProductRepositoryParams >( buildProductURL, httpClient ),
);
}

View File

@ -0,0 +1,60 @@
import { HTTPClient } from '../../../http';
import { ModelRepository } from '../../../framework';
import {
GroupedProduct,
CreatesGroupedProducts,
DeletesGroupedProducts,
ListsGroupedProducts,
ReadsGroupedProducts,
GroupedProductRepositoryParams,
UpdatesGroupedProducts,
baseProductURL,
buildProductURL,
} from '../../../models';
import {
createProductTransformer,
createProductGroupedTransformation,
createProductUpSellsTransformation,
} from './shared';
import {
restCreate,
restDelete,
restList,
restRead,
restUpdate,
} from '../shared';
/**
* 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 {
* ListsGroupedProducts|
* CreatesGroupedProducts|
* ReadsGroupedProducts|
* UpdatesGroupedProducts|
* DeletesGroupedProducts
* } The created repository.
*/
export function groupedProductRESTRepository( httpClient: HTTPClient ): ListsGroupedProducts
& CreatesGroupedProducts
& ReadsGroupedProducts
& UpdatesGroupedProducts
& DeletesGroupedProducts {
const upsells = createProductUpSellsTransformation();
const grouped = createProductGroupedTransformation();
const transformations = [
...upsells,
...grouped,
];
const transformer = createProductTransformer<GroupedProduct>( 'grouped', transformations );
return new ModelRepository(
restList< GroupedProductRepositoryParams >( baseProductURL, GroupedProduct, httpClient, transformer ),
restCreate< GroupedProductRepositoryParams >( baseProductURL, GroupedProduct, httpClient, transformer ),
restRead< GroupedProductRepositoryParams >( buildProductURL, GroupedProduct, httpClient, transformer ),
restUpdate< GroupedProductRepositoryParams >( buildProductURL, GroupedProduct, httpClient, transformer ),
restDelete< GroupedProductRepositoryParams >( buildProductURL, httpClient ),
);
}

View File

@ -1,9 +1,13 @@
import { createProductTransformer } from './shared';
import { groupedProductRESTRepository } from './grouped-product';
import { simpleProductRESTRepository } from './simple-product';
import { externalProductRESTRepository } from './external-product';
import { variableProductRESTRepository } from './variable-product';
export {
createProductTransformer,
externalProductRESTRepository,
groupedProductRESTRepository,
simpleProductRESTRepository,
variableProductRESTRepository,
};

View File

@ -15,7 +15,10 @@ import {
AbstractProductData,
IProductCrossSells,
IProductDelivery,
IProductExternal,
IProductGrouped,
IProductInventory,
IProductPrice,
IProductSalesTax,
IProductShipping,
IProductUpSells,
@ -127,8 +130,6 @@ export function createProductDataTransformer< T extends AbstractProductData >(
[
'date_created',
'date_modified',
'date_on_sale_from',
'date_on_sale_to',
],
),
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
@ -146,6 +147,12 @@ export function createProductDataTransformer< T extends AbstractProductData >(
menuOrder: PropertyType.Integer,
permalink: PropertyType.String,
priceHtml: PropertyType.String,
isFeatured: PropertyType.Boolean,
allowReviews: PropertyType.Boolean,
averageRating: PropertyType.Integer,
numRatings: PropertyType.Integer,
totalSales: PropertyType.Integer,
relatedIds: PropertyType.Integer,
},
),
new KeyChangeTransformation< AbstractProduct >(
@ -154,15 +161,15 @@ export function createProductDataTransformer< T extends AbstractProductData >(
modified: 'date_modified_gmt',
postStatus: 'status',
isPurchasable: 'purchasable',
regularPrice: 'regular_price',
onSale: 'on_sale',
salePrice: 'sale_price',
saleStart: 'date_on_sale_from_gmt',
saleEnd: 'date_on_sale_to_gmt',
isFeatured: 'featured',
catalogVisibility: 'catalog_visibility',
allowReviews: 'reviews_allowed',
averageRating: 'average_rating',
numRatings: 'rating_count',
metaData: 'meta_data',
parentId: 'parent_id',
menuOrder: 'menu_order',
priceHtml: 'price_html',
relatedIds: 'related_ids',
links: '_links',
},
),
@ -217,6 +224,43 @@ export function createProductTransformer< T extends AbstractProduct >(
return createProductDataTransformer< T >( transformations );
}
/**
* Create a transformer for the product price properties.
*/
export function createProductPriceTransformation(): ModelTransformation[] {
const transformations = [
new IgnorePropertyTransformation(
[
'date_on_sale_from',
'date_on_sale_to',
],
),
new PropertyTypeTransformation(
{
onSale: PropertyType.Boolean,
saleStart: PropertyType.Date,
saleEnd: PropertyType.Date,
priceHtml: PropertyType.String,
},
),
new KeyChangeTransformation< IProductPrice >(
{
regularPrice: 'regular_price',
onSale: 'on_sale',
salePrice: 'sale_price',
saleStart: 'date_on_sale_from_gmt',
saleEnd: 'date_on_sale_to_gmt',
priceHtml: 'price_html',
},
),
];
return transformations;
}
/**
* Create a transformer for the product cross sells property.
*/
export function createProductCrossSellsTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
@ -234,6 +278,9 @@ export function createProductCrossSellsTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Create a transformer for the product upsells property.
*/
export function createProductUpSellsTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
@ -251,6 +298,29 @@ export function createProductUpSellsTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Transformer for the grouped products property.
*/
export function createProductGroupedTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
{
groupedProducts: PropertyType.Integer,
},
),
new KeyChangeTransformation< IProductGrouped >(
{
groupedProducts: 'grouped_products',
},
),
];
return transformations;
}
/**
* Create a transformer for product delivery properties.
*/
export function createProductDeliveryTransformation(): ModelTransformation[] {
const transformations = [
new ModelTransformerTransformation( 'downloads', ProductDownload, createProductDownloadTransformer() ),
@ -277,6 +347,9 @@ export function createProductDeliveryTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Create a transformer for product inventory properties.
*/
export function createProductInventoryTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
@ -306,6 +379,9 @@ export function createProductInventoryTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Create a transformer for product sales tax properties.
*/
export function createProductSalesTaxTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
@ -325,6 +401,9 @@ export function createProductSalesTaxTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Create a transformer for product shipping properties.
*/
export function createProductShippingTransformation(): ModelTransformation[] {
const transformations = [
new CustomTransformation(
@ -400,3 +479,24 @@ export function createProductVariableTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Transformer for the properties unique to the external product type.
*/
export function createProductExternalTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
{
buttonText: PropertyType.String,
externalUrl: PropertyType.String,
},
),
new KeyChangeTransformation< IProductExternal >(
{
buttonText: 'button_text',
externalUrl: 'external_url',
},
),
];
return transformations;
}

View File

@ -2,7 +2,8 @@ import { HTTPClient } from '../../../http';
import { ModelRepository } from '../../../framework';
import {
SimpleProduct,
ModelID,
baseProductURL,
buildProductURL,
CreatesSimpleProducts,
DeletesSimpleProducts,
ListsSimpleProducts,
@ -15,6 +16,7 @@ import {
createProductCrossSellsTransformation,
createProductDeliveryTransformation,
createProductInventoryTransformation,
createProductPriceTransformation,
createProductSalesTaxTransformation,
createProductShippingTransformation,
createProductUpSellsTransformation,
@ -44,11 +46,10 @@ export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimp
& ReadsSimpleProducts
& UpdatesSimpleProducts
& DeletesSimpleProducts {
const buildURL = ( id: ModelID ) => '/wc/v3/products/' + id;
const crossSells = createProductCrossSellsTransformation();
const delivery = createProductDeliveryTransformation();
const inventory = createProductInventoryTransformation();
const price = createProductPriceTransformation();
const salesTax = createProductSalesTaxTransformation();
const shipping = createProductShippingTransformation();
const upsells = createProductUpSellsTransformation();
@ -56,6 +57,7 @@ export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimp
...crossSells,
...delivery,
...inventory,
...price,
...salesTax,
...shipping,
...upsells,
@ -64,10 +66,10 @@ export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimp
const transformer = createProductTransformer<SimpleProduct>( 'simple', transformations );
return new ModelRepository(
restList< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
restCreate< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
restRead< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
restUpdate< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
restDelete< SimpleProductRepositoryParams >( buildURL, httpClient ),
restList< SimpleProductRepositoryParams >( baseProductURL, SimpleProduct, httpClient, transformer ),
restCreate< SimpleProductRepositoryParams >( baseProductURL, SimpleProduct, httpClient, transformer ),
restRead< SimpleProductRepositoryParams >( buildProductURL, SimpleProduct, httpClient, transformer ),
restUpdate< SimpleProductRepositoryParams >( buildProductURL, SimpleProduct, httpClient, transformer ),
restDelete< SimpleProductRepositoryParams >( buildProductURL, httpClient ),
);
}

View File

@ -17,6 +17,36 @@
},
"variable": {
"name": "Variable Product with Three Variations"
},
"grouped": {
"name": "Grouped Product with Three Children",
"groupedProducts": [
{
"name": "Base Unit",
"regularPrice": "29.99"
},
{
"name": "Add-on A",
"regularPrice": "11.95"
},
{
"name": "Add-on B",
"regularPrice": "18.97"
}
]
},
"external": {
"name": "External product",
"regularPrice": "24.99",
"buttonText": "Buy now",
"externalUrl": "https://wordpress.org/plugins/woocommerce"
}
},
"coupons": {
"percentage": {
"code": "20percent",
"discountType": "percent",
"amount": "20.00"
}
},
"addresses": {

View File

@ -16,7 +16,7 @@ This package contains the automated end-to-end tests for WooCommerce.
### Setting up the test environment
Follow [E2E setup instructions](https://github.com/woocommerce/woocommerce/blob/master/tests/e2e/README.md).
Follow [E2E setup instructions](https://github.com/woocommerce/woocommerce/blob/trunk/tests/e2e/README.md).
### Setting up core tests

View File

@ -2,7 +2,7 @@
"name": "@woocommerce/e2e-core-tests",
"version": "0.1.1",
"description": "End-To-End (E2E) tests for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/core-tests/README.md",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/core-tests/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
@ -14,6 +14,7 @@
"config": "3.3.3"
},
"peerDependencies": {
"@woocommerce/api": "^0.1.1",
"@woocommerce/e2e-utils": "^0.1.2"
},
"publishConfig": {

View File

@ -0,0 +1,85 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const { HTTPClientFactory, Coupon } = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* Create the default coupon and tests interactions with it via the API.
*/
const runCouponApiTest = () => {
describe('REST API > Coupon', () => {
let client;
let percentageCoupon;
let coupon;
let repository;
beforeAll(async () => {
percentageCoupon = config.get( 'coupons.percentage' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
client = HTTPClientFactory.build( url )
.withBasicAuth( admin.username, admin.password )
.withIndexPermalinks()
.create();
} );
it('can create a coupon', async () => {
repository = Coupon.restRepository( client );
// Check properties of the coupon in the create coupon response.
coupon = await repository.create( percentageCoupon );
expect( coupon ).toEqual( expect.objectContaining( percentageCoupon ) );
});
it('can retrieve a coupon', async () => {
const couponProperties = {
id: coupon.id,
code: percentageCoupon.code,
discount_type: percentageCoupon.discountType,
amount: percentageCoupon.amount,
};
// Read coupon directly from API to compare.
const response = await client.get( `/wc/v3/coupons/${coupon.id}` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( couponProperties ) );
});
it('can update a coupon', async () => {
const updatedCouponProperties = {
amount: '75.00',
discount_type: 'fixed_cart',
free_shipping: true,
};
await repository.update( coupon.id, updatedCouponProperties );
// Check the coupon response for the updated values.
const response = await client.get( `/wc/v3/coupons/${coupon.id}` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( updatedCouponProperties ) );
});
it('can delete a coupon', async () => {
// Delete the coupon
const deletedCoupon = await repository.delete( coupon.id );
// If the delete is successful, the response comes back truthy
expect( deletedCoupon ).toBeTruthy();
});
});
};
module.exports = runCouponApiTest;

View File

@ -0,0 +1,75 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const { HTTPClientFactory, ExternalProduct } = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* Create an external product and retrieve via the API.
*/
const runExternalProductAPITest = () => {
// @todo: add a call to ensure pretty permalinks are enabled once settings api is in use.
describe('REST API > External Product', () => {
let client;
let defaultExternalProduct;
let product;
let repository;
beforeAll(async () => {
defaultExternalProduct = config.get( 'products.external' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
client = HTTPClientFactory.build( url )
.withBasicAuth( admin.username, admin.password )
.withIndexPermalinks()
.create();
} );
it('can create an external product', async () => {
repository = ExternalProduct.restRepository( client );
// Check properties of product in the create product response.
product = await repository.create( defaultExternalProduct );
expect( product ).toEqual( expect.objectContaining( defaultExternalProduct ) );
});
it('can retrieve a raw external product', async () => {
const rawProperties = {
id: product.id,
button_text: defaultExternalProduct.buttonText,
external_url: defaultExternalProduct.externalUrl,
price: defaultExternalProduct.regularPrice,
};
// Read product directly from api.
const response = await client.get( `/wc/v3/products/${product.id}` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( rawProperties ) );
});
it('can retrieve a transformed external product', async () => {
const transformedProperties = {
...defaultExternalProduct,
id: product.id,
price: defaultExternalProduct.regularPrice,
};
// Read product via the repository.
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( transformedProperties ) );
});
});
};
module.exports = runExternalProductAPITest;

View File

@ -0,0 +1,82 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const { HTTPClientFactory, GroupedProduct, SimpleProduct } = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* Create an external product and retrieve via the API.
*/
const runGroupedProductAPITest = () => {
// @todo: add a call to ensure pretty permalinks are enabled once settings api is in use.
describe('REST API > Grouped Product', () => {
let client;
let defaultGroupedProduct;
let baseGroupedProduct;
let product;
let groupedProducts = [];
let repository;
beforeAll(async () => {
defaultGroupedProduct = config.get( 'products.grouped' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
client = HTTPClientFactory.build( url )
.withBasicAuth( admin.username, admin.password )
.withIndexPermalinks()
.create();
// Create the simple products to be grouped first.
repository = SimpleProduct.restRepository( client );
for ( let c = 0; c < defaultGroupedProduct.groupedProducts.length; c++ ) {
product = await repository.create( defaultGroupedProduct.groupedProducts[ c ] );
groupedProducts.push( product.id );
}
});
it('can create a grouped product', async () => {
baseGroupedProduct = {
...defaultGroupedProduct,
groupedProducts,
};
repository = GroupedProduct.restRepository( client );
// Check properties of product in the create product response.
product = await repository.create( baseGroupedProduct );
expect( product ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
it('can retrieve a raw external product', async () => {
let rawProperties = {
id: product.id,
grouped_products: baseGroupedProduct.groupedProducts,
...defaultGroupedProduct,
};
delete rawProperties['groupedProducts'];
// Read product directly from api.
const response = await client.get( `/wc/v3/products/${product.id}` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( rawProperties ) );
});
it('can retrieve a transformed external product', async () => {
// Read product via the repository.
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
});
};
module.exports = runGroupedProductAPITest;

View File

@ -20,6 +20,7 @@ const runVariableProductUpdateTest = require( './shopper/front-end-variable-prod
// Merchant tests
const runCreateCouponTest = require( './merchant/wp-admin-coupon-new.test' );
const runCreateOrderTest = require( './merchant/wp-admin-order-new.test' );
const runEditOrderTest = require( './merchant/wp-admin-order-edit.test' );
const { runAddSimpleProductTest, runAddVariableProductTest } = require( './merchant/wp-admin-product-new.test' );
const runUpdateGeneralSettingsTest = require( './merchant/wp-admin-settings-general.test' );
const runProductSettingsTest = require( './merchant/wp-admin-settings-product.test' );
@ -31,6 +32,11 @@ const runProductEditDetailsTest = require( './merchant/wp-admin-product-edit-det
const runProductSearchTest = require( './merchant/wp-admin-product-search.test' );
const runMerchantOrdersCustomerPaymentPage = require( './merchant/wp-admin-order-customer-payment-page.test' );
// REST API tests
const runExternalProductAPITest = require( './api/external-product.test' );
const runCouponApiTest = require( './api/coupon.test' );
const runGroupedProductAPITest = require( './api/grouped-product.test' );
const runSetupOnboardingTests = () => {
runActivationTest();
runOnboardingFlowTest();
@ -51,6 +57,7 @@ const runShopperTests = () => {
const runMerchantTests = () => {
runCreateCouponTest();
runCreateOrderTest();
runEditOrderTest();
runAddSimpleProductTest();
runAddVariableProductTest();
runUpdateGeneralSettingsTest();
@ -64,12 +71,20 @@ const runMerchantTests = () => {
runMerchantOrdersCustomerPaymentPage();
}
const runApiTests = () => {
runExternalProductAPITest();
runCouponApiTest();
}
module.exports = {
runActivationTest,
runOnboardingFlowTest,
runTaskListTest,
runInitialStoreSettingsTest,
runSetupOnboardingTests,
runExternalProductAPITest,
runGroupedProductAPITest,
runCouponApiTest,
runCartApplyCouponsTest,
runCartPageTest,
runCheckoutApplyCouponsTest,
@ -80,6 +95,7 @@ module.exports = {
runShopperTests,
runCreateCouponTest,
runCreateOrderTest,
runEditOrderTest,
runAddSimpleProductTest,
runAddVariableProductTest,
runUpdateGeneralSettingsTest,
@ -92,4 +108,5 @@ module.exports = {
runProductSearchTest,
runMerchantOrdersCustomerPaymentPage,
runMerchantTests,
runApiTests,
};

View File

@ -0,0 +1,85 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const {
merchant,
createSimpleOrder,
moveAllItemsToTrash
} = require( '@woocommerce/e2e-utils' );
let orderId;
const runEditOrderTest = () => {
describe('WooCommerce Orders > Edit order', () => {
beforeAll(async () => {
await merchant.login();
orderId = await createSimpleOrder('Processing');
});
afterAll( async () => {
// Make sure we're on the all orders view and cleanup the orders we created
await merchant.openAllOrdersView();
await moveAllItemsToTrash();
});
it('can view single order', async () => {
// Go to "orders" page
await merchant.openAllOrdersView();
// Make sure we're on the orders page
await expect(page.title()).resolves.toMatch('Orders');
//Open order we created
await merchant.goToOrder(orderId);
// Make sure we're on the order details page
await expect(page.title()).resolves.toMatch('Edit order');
});
it('can update order status', async () => {
//Open order we created
await merchant.goToOrder(orderId);
// Make sure we're still on the order details page
await expect(page.title()).resolves.toMatch('Edit order');
// Update order status to `Completed`
await merchant.updateOrderStatus(orderId, 'Completed');
// Verify order status changed note added
await expect( page ).toMatchElement( '#select2-order_status-container', { text: 'Completed' } );
await expect( page ).toMatchElement(
'#woocommerce-order-notes .note_content',
{
text: 'Order status changed from Processing to Completed.',
}
);
});
it('can update order details', async () => {
//Open order we created
await merchant.goToOrder(orderId);
// Make sure we're still on the order details page
await expect(page.title()).resolves.toMatch('Edit order');
// Update order details
await expect(page).toFill('input[name=order_date]', '2018-12-14');
// Wait for auto save
await page.waitFor( 2000 );
// Save the order changes
await expect( page ).toClick( 'button.save_order' );
await page.waitForSelector( '#message' );
// Verify
await expect( page ).toMatchElement( '#message', { text: 'Order updated.' } );
await expect( page ).toMatchElement( 'input[name=order_date]', { value: '2018-12-14' } );
});
});
}
module.exports = runEditOrderTest;

View File

@ -8,7 +8,7 @@ const {
} = require( '@woocommerce/e2e-utils' );
const runCreateOrderTest = () => {
describe('Add New Order Page', () => {
describe('WooCommerce Orders > Add new order', () => {
beforeAll(async () => {
await merchant.login();
});

View File

@ -121,9 +121,9 @@ Jest provides setup and teardown functions similar to PHPUnit. The default setup
Depending on the project and testing scenario, the built in testing environment container might not be the best solution for testing. This could be local testing where there is already a testing container, a repository that isn't a plugin or theme and there are multiple folders mapped into the container, or similar. The `e2e-environment` container supports using either the built in container or an external container. See the the appropriate readme for details:
- [Built In Container](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/env/builtin.md)
- [External Container](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/env/external.md)
- [Built In Container](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env/builtin.md)
- [External Container](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env/external.md)
## Additional information
Refer to [`tests/e2e/core-tests`](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/core-tests) for some test examples, and [`tests/e2e`](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e) for general information on e2e tests.
Refer to [`tests/e2e/core-tests`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/core-tests) for some test examples, and [`tests/e2e`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e) for general information on e2e tests.

View File

@ -16,7 +16,7 @@ wp post create --post_type=page --post_status=publish --post_title='Ready' --pos
### Project Initialization
Each project will have its own begin test state and initialization script. For example, a project might start testing expecting that the [sample products](https://github.com/woocommerce/woocommerce/tree/master/sample-data) have already been imported. Below is the WP CLI equivalent of the built in initialization script for WooCommerce Core E2E testing:
Each project will have its own begin test state and initialization script. For example, a project might start testing expecting that the [sample products](https://github.com/woocommerce/woocommerce/tree/trunk/sample-data) have already been imported. Below is the WP CLI equivalent of the built in initialization script for WooCommerce Core E2E testing:
```

View File

@ -16,7 +16,7 @@ wp post create --post_type=page --post_status=publish --post_title='Ready' --pos
### Project Initialization
Each project will have its own begin test state and initialization script. For example, a project might start testing expecting that the [sample products](https://github.com/woocommerce/woocommerce/tree/master/sample-data) have already been imported. Below is the WP CLI equivalent initialization script for WooCommerce Core E2E testing:
Each project will have its own begin test state and initialization script. For example, a project might start testing expecting that the [sample products](https://github.com/woocommerce/woocommerce/tree/trunk/sample-data) have already been imported. Below is the WP CLI equivalent initialization script for WooCommerce Core E2E testing:
```

View File

@ -10,7 +10,7 @@
"e2e",
"puppeteer"
],
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/env/README.md",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env/README.md",
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runApiTests } = require( '@woocommerce/e2e-core-tests' );
runApiTests();

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runCouponApiTest } = require( '@woocommerce/e2e-core-tests' );
runCouponApiTest();

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runExternalProductAPITest } = require( '@woocommerce/e2e-core-tests' );
runExternalProductAPITest();

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runGroupedProductAPITest } = require( '@woocommerce/e2e-core-tests' );
runGroupedProductAPITest();

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runEditOrderTest } = require( '@woocommerce/e2e-core-tests' );
runEditOrderTest();

View File

@ -2,7 +2,7 @@
"name": "@woocommerce/e2e-utils",
"version": "0.1.2",
"description": "End-To-End (E2E) test utils for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e-utils/README.md",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e-utils/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"

View File

@ -0,0 +1,120 @@
<?php
/**
* Class WC_Tests_Admin_Dashboard file.
*
* @package WooCommerce\Tests\Admin
*/
/**
* Tests for the WC_Admin_Report class.
*/
class WC_Tests_Admin_Dashboard extends WC_Unit_Test_Case {
/**
* Set up for tests.
*/
public function setUp() {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
// Mock http request to performance endpoint.
add_filter( 'rest_pre_dispatch', array( $this, 'mock_rest_responses' ), 10, 3 );
}
/**
* Tear down.
*/
public function tearDown() {
parent::tearDown();
remove_filter( 'rest_pre_dispatch', array( $this, 'mock_rest_responses' ), 10 );
}
/**
* Test: get_status_widget
*/
public function test_status_widget() {
wp_set_current_user( $this->user );
$order = WC_Helper_Order::create_order();
$order->set_status( 'completed' );
$order->save();
$this->expectOutputRegex( '/98,765\.00/' );
( new WC_Admin_Dashboard() )->status_widget();
$widget_output = $this->getActualOutput();
$this->assertRegExp( '/page\=wc-admin\&\#038\;path\=\%2Fanalytics\%2Frevenue/', $widget_output );
$this->assertRegExp( '/page\=wc-admin\&\#038\;filter\=single_product/', $widget_output );
$this->assertRegExp( '/page\=wc-admin\&\#038\;type\=lowstock/', $widget_output );
$this->assertRegExp( '/page\=wc-admin\&\#038\;type\=outofstock/', $widget_output );
}
/**
* Test: get_status_widget with woo admin disabled.
*/
public function test_status_widget_with_woo_admin_disabled() {
wp_set_current_user( $this->user );
$order = WC_Helper_Order::create_order();
$order->set_status( 'completed' );
$order->save();
add_filter( 'woocommerce_admin_disabled', '__return_true' );
$this->expectOutputRegex( '/50\.00 worth in the/' );
( new WC_Admin_Dashboard() )->status_widget();
$widget_output = $this->getActualOutput();
$this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=orders\&\#038\;range\=month/', $widget_output );
$this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=orders\&\#038\;report\=sales_by_product/', $widget_output );
$this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=stock\&\#038\;report\=low_in_stock/', $widget_output );
$this->assertRegExp( '/page\=wc-reports\&\#038\;tab\=stock\&\#038\;report\=out_of_stock/', $widget_output );
remove_filter( 'woocommerce_admin_disabled', '__return_true' );
}
/**
* Helper method to mock rest_do_request method.
*
* @param false $response Request arguments.
* @param WP_REST_Server $rest_server rest server class.
* @param WP_REST_Request $request incoming request.
*
* @return WP_REST_Response|false mocked response or false to let WP perform a regular request.
*/
public function mock_rest_responses( $response, $rest_server, $request ) {
if ( '/wc-analytics/reports/performance-indicators' === $request->get_route() ) {
$response = new WP_REST_Response(
array(
'status' => 200,
)
);
$response->set_data(
array(
array(
'chart' => 'net_revenue',
'value' => 98765.0,
),
)
);
} elseif ( '/wc-analytics/reports/revenue/stats' === $request->get_route() ) {
$response = new WP_REST_Response(
array(
'status' => 200,
)
);
$response->set_data(
array(
'intervals' => array(),
)
);
}
return $response;
}
}

View File

@ -46,7 +46,7 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
);
$notices = WC()->session->get( 'wc_notices', array() );
// Check that the second add to cart call increases the quantity of the existing cart-item.
// Check for cart contents.
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
@ -61,6 +61,41 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
$product->delete( true );
}
/**
* @testdox should throw a notice to the cart if using variation_id
* that doesn't belong to specified variable product.
*/
public function test_add_variation_to_the_cart_invalid_variation_id() {
WC()->cart->empty_cart();
WC()->session->set( 'wc_notices', null );
$variable_product = WC_Helper_Product::create_variation_product();
$single_product = WC_Helper_Product::create_simple_product();
// Add variation using parent id.
WC()->cart->add_to_cart(
$variable_product->get_id(),
1,
$single_product->get_id()
);
$notices = WC()->session->get( 'wc_notices', array() );
// Check for cart contents.
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
// Check that the notices contain an error message about invalid colour and number.
$this->assertArrayHasKey( 'error', $notices );
$this->assertCount( 1, $notices['error'] );
$expected = sprintf( sprintf( 'The selected product isn\'t a variation of %2$s, please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', esc_url( $variable_product->get_permalink() ), esc_html( $variable_product->get_name() ) ) );
$this->assertEquals( $expected, $notices['error'][0]['notice'] );
// Reset cart.
WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
$variable_product->delete( true );
}
/**
* Test show shipping.
*/

View File

@ -0,0 +1,67 @@
<?php
/**
* Unit tests for the WC_Tracker class.
*
* @package WooCommerce\Tests\WC_Tracker.
*/
/**
* Class WC_Tracker_Test
*/
class WC_Tracker_Test extends \WC_Unit_Test_Case {
/**
* Test the tracking of wc_admin being disabled via filter.
*/
public function test_wc_admin_disabled_get_tracking_data() {
$posted_data = null;
// Test the case for woocommerce_admin_disabled filter returning true.
add_filter(
'woocommerce_admin_disabled',
function( $default ) {
return true;
}
);
add_filter(
'pre_http_request',
function( $pre, $args, $url ) use ( &$posted_data ) {
$posted_data = $args;
return true;
},
3,
10
);
WC_Tracker::send_tracking_data( true );
$tracking_data = json_decode( $posted_data['body'], true );
// Test the default case of no filter for set for woocommerce_admin_disabled.
$this->assertArrayHasKey( 'wc_admin_disabled', $tracking_data );
$this->assertEquals( 'yes', $tracking_data['wc_admin_disabled'] );
}
/**
* Test the tracking of wc_admin being not disabled via filter.
*/
public function test_wc_admin_not_disabled_get_tracking_data() {
$posted_data = null;
// Bypass time delay so we can invoke send_tracking_data again.
update_option( 'woocommerce_tracker_last_send', strtotime( '-2 weeks' ) );
add_filter(
'pre_http_request',
function( $pre, $args, $url ) use ( &$posted_data ) {
$posted_data = $args;
return true;
},
3,
10
);
WC_Tracker::send_tracking_data( true );
$tracking_data = json_decode( $posted_data['body'], true );
// Test the default case of no filter for set for woocommerce_admin_disabled.
$this->assertArrayHasKey( 'wc_admin_disabled', $tracking_data );
$this->assertEquals( 'no', $tracking_data['wc_admin_disabled'] );
}
}

View File

@ -3,7 +3,7 @@
* Plugin Name: WooCommerce
* Plugin URI: https://woocommerce.com/
* Description: An eCommerce toolkit that helps you sell anything. Beautifully.
* Version: 5.1.0-dev
* Version: 5.2.0-dev
* Author: Automattic
* Author URI: https://woocommerce.com
* Text Domain: woocommerce