Merge branch 'trunk' into update/wccom-11343

This commit is contained in:
Remi Corson 2021-10-14 08:38:05 +02:00 committed by GitHub
commit 9aa7fd9dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3019 additions and 72 deletions

View File

@ -49,3 +49,53 @@ jobs:
npx wc-e2e test:e2e ./tests/e2e/specs/smoke-tests/update-woocommerce.js
npx wc-e2e test:e2e
npx wc-api-tests test api
test-wp-version:
name: Smoke test on L-${{ matrix.wp }} WordPress version
runs-on: ubuntu-18.04
strategy:
matrix:
wp: [ '1', '2' ]
steps:
- name: Create dirs.
run: |
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
- name: Checkout code.
uses: actions/checkout@v2
with:
path: package/woocommerce
- name: Run npm install.
working-directory: package/woocommerce
run: npm install
- name: Load docker images and start containers.
working-directory: package/woocommerce
env:
LATEST_WP_VERSION_MINUS: ${{ matrix.wp }}
run: npx wc-e2e docker:up
- name: Move current directory to code. We will install zip file in this dir later.
run: mv ./package/woocommerce/* ./code/woocommerce
- name: Download WooCommerce release zip
working-directory: tmp
run: |
ASSET_ID=$(jq ".release.assets[0].id" $GITHUB_EVENT_PATH)
curl https://api.github.com/repos/woocommerce/woocommerce/releases/assets/${ASSET_ID} -LJOH 'Accept: application/octet-stream'
unzip woocommerce.zip -d woocommerce
mv woocommerce/* ../package/woocommerce/
- name: Run tests command.
working-directory: code/woocommerce
env:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
run: npx wc-e2e test:e2e

View File

@ -162,16 +162,16 @@
},
{
"name": "league/mime-type-detection",
"version": "1.7.0",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
"reference": "b38b25d7b372e9fddb00335400467b223349fd7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/b38b25d7b372e9fddb00335400467b223349fd7e",
"reference": "b38b25d7b372e9fddb00335400467b223349fd7e",
"shasum": ""
},
"require": {
@ -202,7 +202,7 @@
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.8.0"
},
"funding": [
{
@ -214,7 +214,7 @@
"type": "tidelift"
}
],
"time": "2021-01-18T20:58:21+00:00"
"time": "2021-09-25T08:23:19+00:00"
},
{
"name": "psr/container",
@ -1153,5 +1153,5 @@
"platform-overrides": {
"php": "7.3"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -251,16 +251,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.6.0",
"version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625"
"reference": "f268ca40d54617c6e06757f83f699775c9b3ff2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625",
"reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/f268ca40d54617c6e06757f83f699775c9b3ff2e",
"reference": "f268ca40d54617c6e06757f83f699775c9b3ff2e",
"shasum": ""
},
"require": {
@ -303,7 +303,7 @@
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2021-04-09T00:54:41+00:00"
"time": "2021-10-11T04:00:11+00:00"
},
{
"name": "woocommerce/woocommerce-sniffs",
@ -411,5 +411,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -1697,5 +1697,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -624,5 +624,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

View File

@ -1,5 +1,78 @@
== Changelog ==
= 5.8.0 2021-10-12 =
**WooCommerce**
* Add - `modified_before` and `modified_after` filtering parameters to REST API for products, orders and coupons. #30585
* Add - `woocommerce_quantity_input_min_admin` and `woocommerce_quantity_input_step_admin` filters. #30705
* Dev - Action Scheduler updated to 3.3.0. #30719
* Dev - Add order argument to `woocommerce_order_actions` filter. #30475
* Fix - During product quick edit, the featured setting is sometimes not shown correctly as checked. #30639
* Fix - Offsets not calculated correctly sometimes on select2 dropdowns causing usability issues. #30690
* Fix - Select2 dropdown search input not getting focus when select2 dropdown element gets focused. #30626
* Tweak - Add individual item remove notices based on the context of the line item in the order. #30650
* Tweak - Change the shop page summary which was not relevant to the public. #30573
* Tweak - Deleted unneeded double spaces in text strings. #30487
* Tweak - Open Browse all extensions link in a new tab. #30640
** WooCommerce Admin - 2.7.1 & 2.7.2**
* Fix - Fix analytics crashing on daylight saving. #7763
* Fix - Allow super admins all capabilities within WooCommerce Admin. #7489
* Fix - Fix end date for last periods. #6584
* Fix - Fix up onboarding profiler not working when opted out of tracking. #7490
* Fix - Making Business Details sticky in onboarding wizard. #7426
* Fix - Missing RTL for onboarding styles. #7531
* Fix - Skip scheduling action if Action Scheduler tables have not been set up. #7521
* Fix - Update country region typeahead for better autofill support. #7497
* Fix - Use installable extensions for local state versus free extensions. #7585
* Fix - Fix fatal error and unrelated results in analytics. #7682
* Fix - Harden the reports directory. #7691
* Fix - Update task-item logic to only display content when expanded is true. #7611
* Add - Show Pinterest in installed marketing extensions (if installed). #7417
* Add - Added MailchimpScheduler that runs daily to subscribe store_email in the profile data. #7579
* Add - Added shipping plugin recommendations to settings page. #7446
* Add - Adding endpoint to snooze onboarding task. #7539
* Add - Adding undo snooze task endpoint. #7560
* Add - Add task dismissal endpoints. #7538
* Update - Add HK and SG countries to WC Pay intl support. #7558
* Update - Create task list REST API endpoint. #7512
* Update - Deleted OnboardingEmailMarketing note class. #7595
* Update - Removes the use of the depreciated woocommerce_shared_settings hook. #7480
* Update - Removes non WooCommerce Admin specific settings from the `wc_admin` namespace in the `wc/data` settings store (ex: countries). #7480
* Update - Updating eway logo in payment suggestions defaults. #7562
* Update - Update marketing task completion logic. #7586
* Dev - Add email address field to OBW. #7552
* Tweak - Add navigation items for the Marketplace menu. #7529
* Tweak - Change all analytics strings and labels to sentence case. #6501
* Tweak - Delete unneeded double spaces in text strings. #7502
* Tweak - Remove the preloaded onboarding options. #7338
* Tweak - Update analytics card header text styles. #6506
* Enhancement - Align Table fields with the fallback on isNumeric. #7431
**WooCommerce Blocks - 5.7.1 & 5.8.0 & 5.9.0 & 5.9.1**
* Add - Extensibility point for extensions to filter payment methods. #4668
* Add - "Filter Products by Stock" block. #4145
* Add - Introduced the `__experimental_woocommerce_blocks_checkout_update_order_from_request` hook to the Checkout Store API. #4610.
* Fix - Add label element to `<BlockTitle>` component. #4585
* Fix - Disable Cart, Checkout, All Products & filters blocks from the widgets screen.
* Fix - Infinite recursion when removing an attribute filter from the Active filters block. #4816
* Fix - Prevent Product Category List from displaying incorrectly when used on the shop page. #4587
* Fix - Product Search block displaying incorrectly. #4740
* Tweak - Add Extensibility info to Store API readme. #4605
* Tweak - Update documentation for the snackbarNoticeVisibility filter. #4508
* Tweak - Add documentation for `extensionCartUpdate` method - this allows extensions to update the client-side cart after it has been modified on the server. #4377
**Action Scheduler 3.3.0**
* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645
* Dev - Now supports queries that use multiple statuses. #649
* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723
* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519
* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645
= 5.7.1 2021-09-23 =
**WooCommerce**

View File

@ -21,7 +21,7 @@
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.3.0",
"woocommerce/woocommerce-admin": "2.7.1-rc.1",
"woocommerce/woocommerce-admin": "2.7.2",
"woocommerce/woocommerce-blocks": "5.9.1"
},
"require-dev": {

26
composer.lock generated
View File

@ -533,16 +533,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "2.7.1-rc.1",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "3e873f3a3733ef81fc3352a21dd152840550fffe"
"reference": "9ce54862556815e74cfe2476fae0eb30fcfd65d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/3e873f3a3733ef81fc3352a21dd152840550fffe",
"reference": "3e873f3a3733ef81fc3352a21dd152840550fffe",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/9ce54862556815e74cfe2476fae0eb30fcfd65d6",
"reference": "9ce54862556815e74cfe2476fae0eb30fcfd65d6",
"shasum": ""
},
"require": {
@ -597,9 +597,9 @@
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.7.1-rc.1"
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.7.2"
},
"time": "2021-09-23T22:13:54+00:00"
"time": "2021-10-11T21:11:02+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -2386,16 +2386,16 @@
},
{
"name": "yoast/phpunit-polyfills",
"version": "1.0.1",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/Yoast/PHPUnit-Polyfills.git",
"reference": "f014fb21c2b0038fd329515d59025af42fb98715"
"reference": "1a582ab1d91e86aa450340c4d35631a85314ff9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/f014fb21c2b0038fd329515d59025af42fb98715",
"reference": "f014fb21c2b0038fd329515d59025af42fb98715",
"url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/1a582ab1d91e86aa450340c4d35631a85314ff9f",
"reference": "1a582ab1d91e86aa450340c4d35631a85314ff9f",
"shasum": ""
},
"require": {
@ -2403,9 +2403,7 @@
"phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^0.5",
"php-parallel-lint/php-parallel-lint": "^1.3.0",
"yoast/yoastcs": "^2.1.0"
"yoast/yoastcs": "^2.2.0"
},
"type": "library",
"extra": {
@ -2445,7 +2443,7 @@
"issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues",
"source": "https://github.com/Yoast/PHPUnit-Polyfills"
},
"time": "2021-08-09T16:28:08+00:00"
"time": "2021-10-03T08:40:26+00:00"
}
],
"aliases": [],

View File

@ -7,6 +7,7 @@
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@ -81,10 +82,10 @@ class WC_Admin_Addons {
* @param string $term Search terms.
* @param string $country Store country.
*
* @return array of extensions
* @return object of extensions and promotions.
*/
public static function get_extension_data( $category, $term, $country ) {
$parameters = self::build_parameter_string( $category, $term, $country );
$parameters = self::build_parameter_string( $category, $term, $country );
$headers = array();
$auth = WC_Helper_Options::get( 'auth' );
@ -99,7 +100,7 @@ class WC_Admin_Addons {
);
if ( ! is_wp_error( $raw_extensions ) ) {
$addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) )->products;
$addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) );
}
return $addons;
}
@ -522,6 +523,37 @@ class WC_Admin_Addons {
<?php
}
/**
* Output the HTML for the promotion block.
*
* @param array $promotion Array of promotion block data.
* @return void
*/
public static function output_search_promotion_block( array $promotion ) {
?>
<div class="addons-wcs-banner-block">
<div class="addons-wcs-banner-block-image">
<img
class="addons-img"
src="<?php echo esc_url( $promotion['image'] ); ?>"
alt="<?php echo esc_attr( $promotion['image_alt'] ); ?>"
/>
</div>
<div class="addons-wcs-banner-block-content">
<h1><?php echo esc_html( $promotion['title'] ); ?></h1>
<p><?php echo esc_html( $promotion['description'] ); ?></p>
<?php
if ( ! empty( $promotion['actions'] ) ) {
foreach ( $promotion['actions'] as $action ) {
self::output_promotion_action( $action );
}
}
?>
</div>
</div>
<?php
}
/**
* Handles the output of a full-width block.
*
@ -563,7 +595,6 @@ class WC_Admin_Addons {
</div>
<div class="addons-promotion-block-buttons">
<?php
if ( $section['button_1'] ) {
self::output_button(
$section['button_1_href'],
@ -581,7 +612,6 @@ class WC_Admin_Addons {
$section['plugin']
);
}
?>
</div>
</div>
@ -680,6 +710,26 @@ class WC_Admin_Addons {
<?php
}
/**
* Output HTML for a promotion action.
*
* @param array $action Array of action properties.
* @return void
*/
public static function output_promotion_action( array $action ) {
if ( empty( $action ) ) {
return;
}
$style = ( ! empty( $action['primary'] ) && $action['primary'] ) ? 'addons-button-solid' : 'addons-button-outline-purple';
?>
<a
class="addons-button <?php echo esc_attr( $style ); ?>"
href="<?php echo esc_url( $action['url'] ); ?>">
<?php echo esc_html( $action['label'] ); ?>
</a>
<?php
}
/**
* Handles output of the addons page in admin.
@ -710,13 +760,33 @@ class WC_Admin_Addons {
$sections = self::get_sections();
$theme = wp_get_theme();
$current_section = isset( $_GET['section'] ) ? $section : '_featured';
$promotions = array();
$addons = array();
if ( '_featured' !== $current_section ) {
$category = $section ? $section : null;
$term = $search ? $search : null;
$country = WC()->countries->get_base_country();
$addons = self::get_extension_data( $category, $term, $country );
$category = $section ? $section : null;
$term = $search ? $search : null;
$country = WC()->countries->get_base_country();
$extension_data = self::get_extension_data( $category, $term, $country );
$addons = $extension_data->products;
$promotions = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array();
}
// We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions.
if ( ! WC()->is_wc_admin_active() ) {
$promotions = array();
}
// Check for existence of promotions and evaluate out if we should show them.
if ( ! empty( $promotions ) ) {
foreach ( $promotions as $promo_id => $promotion ) {
$evaluator = new PromotionRuleEngine\RuleEvaluator();
$passed = $evaluator->evaluate( $promotion->rules );
if ( ! $passed ) {
unset( $promotions[ $promo_id ] );
}
}
// Transform promotions to the correct format ready for output.
$promotions = self::format_promotions( $promotions );
}
/**
@ -811,4 +881,73 @@ class WC_Admin_Addons {
return " $admin_body_class woocommerce-page-wc-marketplace ";
}
/**
* Take an action object and return the URL based on properties of the action.
*
* @param object $action Action object.
* @return string URL.
*/
public static function get_action_url( $action ): string {
if ( ! isset( $action->url ) ) {
return '';
}
if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) {
return wc_admin_url( $action->url );
}
if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) {
if ( empty( $action->nonce ) ) {
return '';
}
return wp_nonce_url(
admin_url( $action->url ),
$action->nonce
);
}
return $action->url;
}
/**
* Format the promotion data ready for display, ie fetch locales and actions.
*
* @param array $promotions Array of promotoin objects.
* @return array Array of formatted promotions ready for output.
*/
public static function format_promotions( array $promotions ): array {
$formatted_promotions = array();
foreach ( $promotions as $promotion ) {
// Get the matching locale or fall back to en-US.
$locale = PromotionRuleEngine\SpecRunner::get_locale( $promotion->locales );
if ( null === $locale ) {
continue;
}
$promotion_actions = array();
if ( ! empty( $promotion->actions ) ) {
foreach ( $promotion->actions as $action ) {
$action_locale = PromotionRuleEngine\SpecRunner::get_action_locale( $action->locales );
$url = self::get_action_url( $action );
$promotion_actions[] = array(
'name' => $action->name,
'label' => $action_locale->label,
'url' => $url,
'primary' => isset( $action->is_primary ) ? $action->is_primary : false,
);
}
}
$formatted_promotions[] = array(
'title' => $locale->title,
'description' => $locale->description,
'image' => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image,
'image_alt' => $locale->image_alt,
'actions' => $promotion_actions,
);
}
return $formatted_promotions;
}
}

View File

@ -5,8 +5,11 @@
* @package WooCommerce\Admin
* @var string $view
* @var object $addons
* @var object $promotions
*/
use Automattic\WooCommerce\Admin\RemoteInboxNotifications as PromotionRuleEngine;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -62,7 +65,12 @@ $current_section_name = __( 'Browse Categories', 'woocommerce' );
<div class="wrap">
<div class="marketplace-content-wrapper">
<?php if ( ! empty( $search ) ) : ?>
<?php if ( count( $addons ) == 0 ) : ?>
<h1 class="search-form-title">
<?php esc_html_e( 'Sorry, could not find anything. Try searching again using a different term.', 'woocommerce' ); ?></p>
</h1>
<?php endif; ?>
<?php if ( ! empty( $search ) && count( $addons ) > 0 ) : ?>
<h1 class="search-form-title">
<?php // translators: search keyword. ?>
<?php printf( esc_html__( 'Search results for "%s"', 'woocommerce' ), esc_html( sanitize_text_field( wp_unslash( $search ) ) ) ); ?>
@ -77,16 +85,13 @@ $current_section_name = __( 'Browse Categories', 'woocommerce' );
</div>
<?php endif; ?>
<?php if ( '_featured' !== $current_section && $addons ) : ?>
<?php if ( 'shipping_methods' === $current_section ) : ?>
<div class="addons-shipping-methods">
<?php WC_Admin_Addons::output_wcs_banner_block(); ?>
</div>
<?php endif; ?>
<?php if ( 'payment-gateways' === $current_section ) : ?>
<div class="addons-shipping-methods">
<?php WC_Admin_Addons::output_wcpay_banner_block(); ?>
</div>
<?php endif; ?>
<?php
if ( ! empty( $promotions ) && WC()->is_wc_admin_active() ) {
foreach ( $promotions as $promotion ) {
WC_Admin_Addons::output_search_promotion_block( $promotion );
}
}
?>
<ul class="products">
<?php foreach ( $addons as $addon ) : ?>
<?php

View File

@ -125,7 +125,6 @@ class WC_AJAX {
'get_order_details',
'add_attribute',
'add_new_attribute',
'remove_variation',
'remove_variations',
'save_attributes',
'add_variation',

View File

@ -397,7 +397,16 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
protected function get_price_hash( &$product, $for_display = false ) {
global $wp_filter;
$price_hash = $for_display && wc_tax_enabled() ? array( get_option( 'woocommerce_tax_display_shop', 'excl' ), WC_Tax::get_rates() ) : array( false );
$price_hash = array( false );
if ( $for_display && wc_tax_enabled() ) {
$price_hash = array(
get_option( 'woocommerce_tax_display_shop', 'excl' ),
WC_Tax::get_rates(),
empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(),
);
}
$filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
foreach ( $filter_names as $filter_name ) {

View File

@ -1140,7 +1140,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => 'string',
'type' => array( 'string', 'null' ),
'format' => 'email',
'context' => array( 'view', 'edit' ),
),

View File

@ -915,6 +915,12 @@ add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts
function wc_cancel_unpaid_orders() {
$held_duration = get_option( 'woocommerce_hold_stock_minutes' );
// Re-schedule the event before cancelling orders
// this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry.
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
return;
}
@ -931,9 +937,6 @@ function wc_cancel_unpaid_orders() {
}
}
}
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' );

View File

@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
Requires at least: 5.6
Tested up to: 5.8
Requires PHP: 7.0
Stable tag: 5.7.1
Stable tag: 5.8.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
const { ordersApi } = require('./orders');
const { couponsApi } = require('./coupons');
const { productsApi } = require('./products');
module.exports = {
ordersApi,
couponsApi,
productsApi,
};

View File

@ -0,0 +1,62 @@
/**
* Internal dependencies
*/
const { getRequest, postRequest, putRequest, deleteRequest } = require('../utils/request');
/**
* WooCommerce Products endpoints.
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#products
*/
const productsApi = {
name: 'Products',
create: {
name: 'Create a product',
method: 'POST',
path: 'products',
responseCode: 201,
product: async ( productDetails ) => postRequest( 'products', productDetails ),
},
retrieve: {
name: 'Retrieve a product',
method: 'GET',
path: 'products/<id>',
responseCode: 200,
product: async ( productId ) => getRequest( `products/${productId}` ),
},
listAll: {
name: 'List all products',
method: 'GET',
path: 'products',
responseCode: 200,
products: async ( productsQuery = {} ) => getRequest( 'products', productsQuery ),
},
update: {
name: 'Update a product',
method: 'PUT',
path: 'products/<id>',
responseCode: 200,
product: async ( productId, productDetails ) => putRequest( `products/${productId}`, productDetails ),
},
delete: {
name: 'Delete a product',
method: 'DELETE',
path: 'products/<id>',
responseCode: 200,
payload: {
force: false
},
product: async ( productId, deletePermanently ) => deleteRequest( `products/${productId}`, deletePermanently ),
},
batch: {
name: 'Batch update products',
method: 'POST',
path: 'products/batch',
responseCode: 200,
products: async ( batchUpdatePayload ) => postRequest( `products/batch`, batchUpdatePayload ),
},
};
module.exports = {
productsApi,
};

View File

@ -0,0 +1,773 @@
/**
* Internal dependencies
*/
const { createSampleData, deleteSampleData } = require( '../../data/products' );
const { productsApi } = require('../../endpoints/products');
/**
* Tests for the WooCommerce Products API.
*
* @group api
* @group products
*
*/
describe( 'Products API tests', () => {
const PRODUCTS_COUNT = 20;
let sampleData;
beforeAll( async () => {
sampleData = await createSampleData();
}, 10000 );
afterAll( async () => {
await deleteSampleData( sampleData );
}, 10000 );
describe( 'List all products', () => {
it( 'defaults', async () => {
const result = await productsApi.listAll.products();
expect( result.statusCode ).toEqual( 200 );
expect( result.headers['x-wp-total'] ).toEqual( PRODUCTS_COUNT.toString() );
expect( result.headers['x-wp-totalpages'] ).toEqual( '2' );
} );
it( 'pagination', async () => {
const pageSize = 6;
const page1 = await productsApi.listAll.products( {
per_page: pageSize,
} );
const page2 = await productsApi.listAll.products( {
per_page: pageSize,
page: 2,
} );
expect( page1.statusCode ).toEqual( 200 );
expect( page2.statusCode ).toEqual( 200 );
// Verify total page count.
expect( page1.headers['x-wp-total'] ).toEqual( PRODUCTS_COUNT.toString() );
expect( page1.headers['x-wp-totalpages'] ).toEqual( '4' );
// Verify we get pageSize'd arrays.
expect( Array.isArray( page1.body ) ).toBe( true );
expect( Array.isArray( page2.body ) ).toBe( true );
expect( page1.body ).toHaveLength( pageSize );
expect( page2.body ).toHaveLength( pageSize );
// Ensure all of the product IDs are unique (no page overlap).
const allProductIds = page1.body.concat( page2.body ).reduce( ( acc, product ) => {
acc[ product.id ] = 1;
return acc;
}, {} );
expect( Object.keys( allProductIds ) ).toHaveLength( pageSize * 2 );
// Verify that offset takes precedent over page number.
const page2Offset = await productsApi.listAll.products( {
per_page: pageSize,
page: 2,
offset: pageSize + 1,
} );
// The offset pushes the result set 1 product past the start of page 2.
expect( page2Offset.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( { id: page2.body[0].id } )
] )
);
expect( page2Offset.body[0].id ).toEqual( page2.body[1].id );
// Verify the last page only has 2 products as we expect.
const lastPage = await productsApi.listAll.products( {
per_page: pageSize,
page: 4,
} );
expect( Array.isArray( lastPage.body ) ).toBe( true );
expect( lastPage.body ).toHaveLength( 2 );
// Verify a page outside the total page count is empty.
const page6 = await productsApi.listAll.products( {
per_page: pageSize,
page: 6,
} );
expect( Array.isArray( page6.body ) ).toBe( true );
expect( page6.body ).toHaveLength( 0 );
} );
it( 'search', async () => {
// Match in the short description.
const result1 = await productsApi.listAll.products( {
search: 'external'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'WordPress Pennant' );
// Match in the product name.
const result2 = await productsApi.listAll.products( {
search: 'pocket'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'Hoodie with Pocket' );
} );
it( 'inclusion / exclusion', async () => {
const allProducts = await productsApi.listAll.products( {
per_page: 20,
} );
expect( allProducts.statusCode ).toEqual( 200 );
const allProductIds = allProducts.body.map( product => product.id );
expect( allProductIds ).toHaveLength( PRODUCTS_COUNT );
const productsToFilter = [
allProductIds[2],
allProductIds[4],
allProductIds[7],
allProductIds[13],
];
const included = await productsApi.listAll.products( {
per_page: 20,
include: productsToFilter.join( ',' ),
} );
expect( included.statusCode ).toEqual( 200 );
expect( included.body ).toHaveLength( productsToFilter.length );
expect( included.body ).toEqual(
expect.arrayContaining(
productsToFilter.map( id => expect.objectContaining( { id } ) )
)
);
const excluded = await productsApi.listAll.products( {
per_page: 20,
exclude: productsToFilter.join( ',' ),
} );
expect( excluded.statusCode ).toEqual( 200 );
expect( excluded.body ).toHaveLength( PRODUCTS_COUNT - productsToFilter.length );
expect( excluded.body ).toEqual(
expect.not.arrayContaining(
productsToFilter.map( id => expect.objectContaining( { id } ) )
)
);
} );
it( 'slug', async () => {
// Match by slug.
const result1 = await productsApi.listAll.products( {
slug: 't-shirt-with-logo'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].slug ).toBe( 't-shirt-with-logo' );
// No matches
const result2 = await productsApi.listAll.products( {
slug: 'no-product-with-this-slug'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'sku', async () => {
// Match by SKU.
const result1 = await productsApi.listAll.products( {
sku: 'woo-sunglasses'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].sku ).toBe( 'woo-sunglasses' );
// No matches
const result2 = await productsApi.listAll.products( {
sku: 'no-product-with-this-sku'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'type', async () => {
const result1 = await productsApi.listAll.products( {
type: 'simple'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.headers['x-wp-total'] ).toEqual( '16' );
const result2 = await productsApi.listAll.products( {
type: 'external'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'WordPress Pennant' );
const result3 = await productsApi.listAll.products( {
type: 'variable'
} );
expect( result3.statusCode ).toEqual( 200 );
expect( result3.body ).toHaveLength( 2 );
const result4 = await productsApi.listAll.products( {
type: 'grouped'
} );
expect( result4.statusCode ).toEqual( 200 );
expect( result4.body ).toHaveLength( 1 );
expect( result4.body[0].name ).toBe( 'Logo Collection' );
} );
it( 'featured', async () => {
const featured = [
expect.objectContaining( { name: 'Hoodie with Zipper' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Sunglasses' } ),
expect.objectContaining( { name: 'Cap' } ),
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
];
const result1 = await productsApi.listAll.products( {
featured: true,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( featured.length );
expect( result1.body ).toEqual( expect.arrayContaining( featured ) );
const result2 = await productsApi.listAll.products( {
featured: false,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( featured ) );
} );
it( 'categories', async () => {
const accessory = [
expect.objectContaining( { name: 'Beanie' } ),
]
const hoodies = [
expect.objectContaining( { name: 'Hoodie with Zipper' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Hoodie with Logo' } ),
expect.objectContaining( { name: 'Hoodie' } ),
];
// Verify that subcategories are included.
const result1 = await productsApi.listAll.products( {
per_page: 20,
category: sampleData.categories.clothing.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toEqual( expect.arrayContaining( accessory ) );
expect( result1.body ).toEqual( expect.arrayContaining( hoodies ) );
// Verify sibling categories are not.
const result2 = await productsApi.listAll.products( {
category: sampleData.categories.hoodies.id,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( accessory ) );
expect( result2.body ).toEqual( expect.arrayContaining( hoodies ) );
} );
it( 'on sale', async () => {
const onSale = [
expect.objectContaining( { name: 'Beanie with Logo' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Single' } ),
expect.objectContaining( { name: 'Cap' } ),
expect.objectContaining( { name: 'Belt' } ),
expect.objectContaining( { name: 'Beanie' } ),
expect.objectContaining( { name: 'Hoodie' } ),
];
const result1 = await productsApi.listAll.products( {
on_sale: true,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( onSale.length );
expect( result1.body ).toEqual( expect.arrayContaining( onSale ) );
const result2 = await productsApi.listAll.products( {
on_sale: false,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( onSale ) );
} );
it( 'price', async () => {
const result1 = await productsApi.listAll.products( {
min_price: 21,
max_price: 28,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Long Sleeve Tee' );
expect( result1.body[0].price ).toBe( '25' );
const result2 = await productsApi.listAll.products( {
max_price: 5,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'Single' );
expect( result2.body[0].price ).toBe( '2' );
const result3 = await productsApi.listAll.products( {
min_price: 5,
order: 'asc',
orderby: 'price',
} );
expect( result3.statusCode ).toEqual( 200 );
expect( result3.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( { name: 'Single' } )
] )
);
} );
it( 'before / after', async () => {
const before = [
expect.objectContaining( { name: 'Album' } ),
expect.objectContaining( { name: 'Single' } ),
expect.objectContaining( { name: 'T-Shirt with Logo' } ),
expect.objectContaining( { name: 'Beanie with Logo' } ),
];
const after = [
expect.objectContaining( { name: 'Hoodie' } ),
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
expect.objectContaining( { name: 'Parent Product' } ),
expect.objectContaining( { name: 'Child Product' } ),
];
const result1 = await productsApi.listAll.products( {
before: '2021-09-05T15:50:19',
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( before.length );
expect( result1.body ).toEqual( expect.arrayContaining( before ) );
const result2 = await productsApi.listAll.products( {
after: '2021-09-18T15:50:18',
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( before ) );
expect( result2.body ).toHaveLength( after.length );
expect( result2.body ).toEqual( expect.arrayContaining( after ) );
} );
it( 'attributes', async () => {
const red = sampleData.attributes.colors.find( term => term.name === 'Red' );
const redProducts = [
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
expect.objectContaining( { name: 'Hoodie' } ),
expect.objectContaining( { name: 'Beanie' } ),
expect.objectContaining( { name: 'Beanie with Logo' } ),
];
const result = await productsApi.listAll.products( {
attribute: 'pa_color',
attribute_term: red.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( redProducts.length );
expect( result.body ).toEqual( expect.arrayContaining( redProducts ) );
} );
it( 'status', async () => {
const result1 = await productsApi.listAll.products( {
status: 'pending'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Polo' );
const result2 = await productsApi.listAll.products( {
status: 'draft'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'shipping class', async () => {
const result = await productsApi.listAll.products( {
shipping_class: sampleData.shippingClasses.freight.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'Long Sleeve Tee' );
} );
it( 'tax class', async () => {
const result = await productsApi.listAll.products( {
tax_class: 'reduced-rate',
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'Sunglasses' );
} );
it( 'stock status', async () => {
const result = await productsApi.listAll.products( {
stock_status: 'onbackorder',
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'T-Shirt' );
} );
it( 'tags', async () => {
const coolProducts = [
expect.objectContaining( { name: 'Sunglasses' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Beanie' } ),
];
const result = await productsApi.listAll.products( {
tag: sampleData.tags.cool.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( coolProducts.length );
expect( result.body ).toEqual( expect.arrayContaining( coolProducts ) );
} );
it( 'parent', async () => {
const result1 = await productsApi.listAll.products( {
parent: sampleData.hierarchicalProducts.parent.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Child Product' );
const result2 = await productsApi.listAll.products( {
parent_exclude: sampleData.hierarchicalProducts.parent.id,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( [
expect.objectContaining( { name: 'Child Product' } ),
] ) );
} );
describe( 'orderby', () => {
const productNamesAsc = [
'Album',
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Child Product',
'Hoodie',
'Hoodie with Logo',
'Hoodie with Pocket',
'Hoodie with Zipper',
'Logo Collection',
'Long Sleeve Tee',
'Parent Product',
'Polo',
'Single',
'Sunglasses',
'T-Shirt',
'T-Shirt with Logo',
'V-Neck T-Shirt',
'WordPress Pennant',
];
const productNamesDesc = [ ...productNamesAsc ].reverse();
const productNamesByRatingAsc = [
'Sunglasses',
'Cap',
'T-Shirt',
];
const productNamesByRatingDesc = [ ...productNamesByRatingAsc ].reverse();
const productNamesByPopularityDesc = [
'Beanie with Logo',
'Single',
'T-Shirt',
];
const productNamesByPopularityAsc = [ ...productNamesByPopularityDesc ].reverse();
it( 'default', async () => {
// Default = date desc.
const result = await productsApi.listAll.products();
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in descending order.
let lastDate = Date.now();
result.body.forEach( ( { date_created_gmt } ) => {
const created = Date.parse( date_created_gmt + '.000Z' );
expect( lastDate ).toBeGreaterThan( created );
lastDate = created;
} );
} );
it( 'date', async () => {
const result = await productsApi.listAll.products( {
order: 'asc',
orderby: 'date',
} );
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in ascending order.
let lastDate = 0;
result.body.forEach( ( { date_created_gmt } ) => {
const created = Date.parse( date_created_gmt + '.000Z' );
expect( created ).toBeGreaterThan( lastDate );
lastDate = created;
} );
} );
it( 'id', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'id',
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
let lastId = 0;
result1.body.forEach( ( { id } ) => {
expect( id ).toBeGreaterThan( lastId );
lastId = id;
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'id',
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
lastId = Number.MAX_SAFE_INTEGER;
result2.body.forEach( ( { id } ) => {
expect( lastId ).toBeGreaterThan( id );
lastId = id;
} );
} );
it( 'title', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'title',
per_page: productNamesAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesAsc[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'title',
per_page: productNamesDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesDesc[ idx ] );
} );
} );
// This case will remain skipped until orderby slug is fixed.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'slug', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'slug',
per_page: productNamesAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesAsc[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'slug',
per_page: productNamesDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesDesc[ idx ] );
} );
} );
it( 'price', async () => {
const productNamesMinPriceAsc = [
'Parent Product',
'Child Product',
'Single',
'WordPress Pennant',
'Album',
'V-Neck T-Shirt',
'Cap',
'Beanie with Logo',
'T-Shirt with Logo',
'Beanie',
'T-Shirt',
'Logo Collection',
'Polo',
'Long Sleeve Tee',
'Hoodie with Pocket',
'Hoodie',
'Hoodie with Zipper',
'Hoodie with Logo',
'Belt',
'Sunglasses',
];
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'price',
per_page: productNamesMinPriceAsc.length
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( productNamesMinPriceAsc.length );
// Verify all results are in ascending order.
// The query uses the min price calculated in the product meta lookup table,
// so we can't just check the price property of the response.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesMinPriceAsc[ idx ] );
} );
const productNamesMaxPriceDesc = [
'Sunglasses',
'Belt',
'Hoodie',
'Logo Collection',
'Hoodie with Logo',
'Hoodie with Zipper',
'Hoodie with Pocket',
'Long Sleeve Tee',
'V-Neck T-Shirt',
'Polo',
'T-Shirt',
'Beanie',
'T-Shirt with Logo',
'Beanie with Logo',
'Cap',
'Album',
'WordPress Pennant',
'Single',
'Child Product',
'Parent Product',
];
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'price',
per_page: productNamesMaxPriceDesc.length
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( productNamesMaxPriceDesc.length );
// Verify all results are in descending order.
// The query uses the max price calculated in the product meta lookup table,
// so we can't just check the price property of the response.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesMaxPriceDesc[ idx ] );
} );
} );
// This case will remain skipped until orderby include is fixed.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'include', async () => {
const includeIds = [
sampleData.groupedProducts[ 0 ].id,
sampleData.simpleProducts[ 3 ].id,
sampleData.hierarchicalProducts.parent.id,
];
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result1.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result2.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
} );
it( 'rating (desc)', async () => {
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'rating',
per_page: productNamesByRatingDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByRatingDesc[ idx ] );
} );
} );
// This case will remain skipped until ratings can be sorted ascending.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'rating (asc)', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'rating',
per_page: productNamesByRatingAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByRatingAsc[ idx ] );
} );
} );
it( 'popularity (desc)', async () => {
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'popularity',
per_page: productNamesByPopularityDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByPopularityDesc[ idx ] );
} );
} );
// This case will remain skipped until popularity can be sorted ascending.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'popularity (asc)', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'popularity',
per_page: productNamesByPopularityAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByPopularityAsc[ idx ] );
} );
} );
} );
} );
} );

View File

@ -765,6 +765,26 @@
"dev": true,
"requires": {
"axios": "^0.19.0"
},
"dependencies": {
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
}
}
}
},
"@types/node": {
@ -988,11 +1008,11 @@
"dev": true
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz",
"integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==",
"requires": {
"follow-redirects": "1.5.10"
"follow-redirects": "^1.14.0"
}
},
"babel-jest": {
@ -1492,6 +1512,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
@ -1889,12 +1910,9 @@
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
},
"for-in": {
"version": "1.0.2",
@ -3249,7 +3267,8 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
@ -4494,9 +4513,9 @@
"dev": true
},
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"to-fast-properties": {

View File

@ -34,7 +34,7 @@
"test": "jest"
},
"dependencies": {
"axios": "0.19.2",
"axios": "0.21.2",
"create-hmac": "1.1.7",
"oauth-1.0a": "2.2.6"
},

View File

@ -1,5 +1,9 @@
# Unreleased
## Added
- Added `LATEST_WP_VERSION_MINUS` that allows setting a number to subtract from the current WordPress version for the WordPress Docker image.
# 0.2.3
## Added

View File

@ -102,6 +102,18 @@ This value will override the default Jest timeout as well as pass the timeout to
For a list of the methods that the above timeout affects, please see the Puppeteer documentation for [`page.setDefaultTimeout()`](https://pptr.dev/#?product=Puppeteer&version=v10.2.0&show=api-pagesetdefaulttimeouttimeout) and [`page.setDefaultNavigationTimeout`](https://pptr.dev/#?product=Puppeteer&version=v10.2.0&show=api-pagesetdefaultnavigationtimeouttimeout) for more information.
### Test Against Previous WordPress Versions
You can use the `LATEST_WP_VERSION_MINUS` flag to determine how many versions back from the current WordPress version to use in the Docker environment. This is calculated from the current WordPress version minus the set value. For example, if `LATEST_WP_VERSION_MINUS` is set to 1, it will calculate the current WordPress version minus one, and use that for the WordPress Docker container.
For example, you could run the following command:
```bash
LATEST_WP_VERSION_MINUS=2 npx wc-e2e docker:up
```
In this example, if the current WordPress version is 6.0, this will go two versions back and use the WordPress 5.8 Docker image for the tests.
### Jest Puppeteer Config
The test sequencer uses the following default Puppeteer configuration:

View File

@ -13,6 +13,10 @@ if [[ $1 ]]; then
export WORDPRESS_VERSION="5.8.0"
fi
if [[ $LATEST_WP_VERSION_MINUS ]]; then
export WORDPRESS_VERSION=$(./bin/get-previous-version.js $WORDPRESS_VERSION $LATEST_WP_VERSION_MINUS 2> /dev/null)
fi
if ! [[ $TRAVIS_PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
TRAVIS_PHP_VERSION=$(./bin/get-latest-docker-tag.js php 7 2> /dev/null)
fi

View File

@ -2,6 +2,7 @@
const https = require( 'https' );
const semver = require( 'semver' );
const getLatestMinusVersion = require( './get-previous-version' );
/**
* Fetches the latest tag from a page using the Docker HTTP api.
@ -107,6 +108,10 @@ function findLatestVersion( image, nameSearch ) {
return fetchLatestTagFromPage( image, nameSearch, ++page ).then( paginationFn );
}
if ( image === 'wordpress' && process.env.LATEST_WP_VERSION_MINUS ) {
return getLatestMinusVersion( latestVersion.toString(), process.env.LATEST_WP_VERSION_MINUS );
}
return latestVersion.toString();
};

32
tests/e2e/env/bin/get-previous-version.js vendored Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
*
* @param {latestVersion} latestVersion
* @param {minus} minus
* @returns {String} the minused version.
*/
function getLatestMinusVersion( latestVersion, minus ) {
// Convert the 1 or 2 to a decimal we can use for the logic below.
let minusAmount = minus / 10;
// Check if we only have a major / minor (e.g. x.x) to append a patch version
if ( latestVersion.match( /\./g ).length < 2 ) {
latestVersion = latestVersion.concat( '.0' )
}
const baseVersion = latestVersion.replace( /.[^\.]$/, '' );
// Calculate the version we need and return.
console.info( String( baseVersion - minusAmount ) );
process.exit( 0 );
}
const latestVersion = process.argv[2];
const minus = process.argv[3];
if ( ! latestVersion || ! minus ) {
console.error( 'Usage: get-previous-version.js <latestVersion> <minus>' );
process.exit( 1 );
}
getLatestMinusVersion( latestVersion, minus );

View File

@ -0,0 +1,68 @@
<?php
/**
* Class WC_Product_Variable_Data_Store_CPT_Test
*/
class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
/**
* Helper filter to force prices inclusice of tax.
*/
public function __return_incl() {
return 'incl';
}
/**
* @testdox Variation price cache accounts for Customer VAT exemption.
*/
public function test_variation_price_cache_vat_exempt() {
// Set store to include tax in price display.
add_filter( 'wc_tax_enabled', '__return_true' );
add_filter( 'woocommerce_prices_include_tax', '__return_true' );
add_filter( 'pre_option_woocommerce_tax_display_shop', array( $this, '__return_incl' ) );
add_filter( 'pre_option_woocommerce_tax_display_cart', array( $this, '__return_incl' ) );
// Create tax rate.
$tax_id = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '10.0000',
'tax_rate_name' => 'VAT',
'tax_rate_priority' => '1',
'tax_rate_compound' => '0',
'tax_rate_shipping' => '1',
'tax_rate_order' => '1',
'tax_rate_class' => '',
)
);
// Create our variable product.
$product = WC_Helper_Product::create_variation_product();
// Verify that a VAT exempt customer gets prices with tax removed.
WC()->customer->set_is_vat_exempt( true );
$prices_no_tax = array( '9.09', '13.64', '14.55', '15.45', '16.36', '17.27' );
$variation_prices = $product->get_variation_prices( true );
$this->assertEquals( $prices_no_tax, array_values( $variation_prices['price'] ) );
// Verify that a normal customer gets prices with tax included.
// This indirectly proves that the customer's VAT exemption influences the cache key.
WC()->customer->set_is_vat_exempt( false );
$prices_with_tax = array( '10.00', '15.00', '16.00', '17.00', '18.00', '19.00' );
$variation_prices = $product->get_variation_prices( true );
$this->assertEquals( $prices_with_tax, array_values( $variation_prices['price'] ) );
// Clean up.
WC_Tax::_delete_tax_rate( $tax_id );
remove_filter( 'wc_tax_enabled', '__return_true' );
remove_filter( 'woocommerce_prices_include_tax', '__return_true' );
remove_filter( 'pre_option_woocommerce_tax_display_shop', array( $this, '__return_incl' ) );
remove_filter( 'pre_option_woocommerce_tax_display_cart', array( $this, '__return_incl' ) );
}
}