Merge branch 'master' into e2e-shopper-grouped-product

This commit is contained in:
Veljko V 2021-02-01 12:25:03 +01:00 committed by GitHub
commit 6d6554ccbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 800 additions and 389 deletions

32
.github/workflows/pr-build.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Build zip for PR
on:
pull_request
jobs:
build:
name: Build zip for PR
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build
id: build
uses: woocommerce/action-build@v2
- name: Upload PR zip
uses: actions/upload-artifact@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce.zip
path: ${{ steps.build.outputs.zip_path }}
retention-days: 7
- name: Add comment
uses: actions/github-script@v3
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 }})!'
})

70
.github/workflows/pr-unit-tests.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: Run unit tests on PR
on:
pull_request
jobs:
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
timeout-minutes: 15
runs-on: ubuntu-latest
strategy:
matrix:
php: [ '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ]
wp: [ "latest" ]
include:
- wp: nightly
php: '7.4'
- wp: '5.5'
php: 7.2
- wp: '5.4'
php: 7.2
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
extensions: mysql
coverage: none
- name: Tool versions
run: |
php --version
composer --version
- name: Get cached composer directories
uses: actions/cache@v2
with:
path: |
./packages
./vendor
key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }}
- name: Setup and install composer
run: composer install
- name: Add PHP8 Compatibility.
run: |
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
composer bin phpunit config --unset platform
composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
fi
- name: Init DB and WP
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
- name: Run tests
run: ./vendor/bin/phpunit -c ./phpunit.xml

View File

@ -36,9 +36,9 @@ jobs:
- npm install
- composer install --no-dev
script:
- travis_retry npm run build:assets
- travis_retry npm run docker:up
- travis_retry npm run test:e2e
- npm run build:assets
- npm run docker:up
- npm run test:e2e
after_script:
- npm run docker:down
- name: "WP Nightly"

View File

@ -126,12 +126,18 @@ a.button {
}
}
.woocommerce-breadcrumb {
margin-bottom: 5rem;
font-size: 0.88889em;
font-family: $headings;
.site-main {
.woocommerce-breadcrumb {
margin-bottom: var(--global--spacing-vertical);
font-size: 0.88889em;
font-family: $headings;
}
.woocommerce-products-header {
margin-top: var(--global--spacing-vertical);
}
}
.woocommerce-pagination {
font-family: $headings;
font-size: 0.88889em;
@ -350,6 +356,10 @@ a.button {
border-left: none;
border-right: none;
}
.product-thumbnail {
max-width: 120px;
}
}
}
@ -537,11 +547,16 @@ dl.variation,
display: none;
}
.entry-title {
margin: 0 0 2.5rem;
&::before {
margin-top: 0;
&.singular { // Needed for higher specificity to target the entry title font size
.entry-title {
font-size: var(--global--font-size-xl);
font-weight: normal;
margin: 0 0 2.5rem;
&::before {
margin-top: 0;
}
}
}
@ -829,7 +844,7 @@ a.reset_variations {
}
h2:first-of-type {
font-size: 3rem;
font-size: var(--global--font-size-lg);
margin: 0 0 2rem !important;
}
}
@ -1424,10 +1439,6 @@ a.reset_variations {
#main {
.entry-header {
padding: 3vw 0 1.5vw;
}
.woocommerce {
max-width: var(--responsive--alignwide-width);
margin: 0 auto;
@ -1860,6 +1871,10 @@ a.reset_variations {
}
}
}
tfoot {
text-align: left;
}
}
.woocommerce-order-received {
@ -2788,11 +2803,6 @@ a.reset_variations {
.woocommerce-account {
.entry-header {
padding-bottom: 20px !important;
}
.woocommerce-MyAccount-content {
p:first-of-type {

View File

@ -71,10 +71,6 @@
"stylecheck",
"tests"
],
"support": {
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
},
"time": "2020-06-25T14:57:39+00:00"
},
{
@ -133,10 +129,6 @@
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
},
"time": "2019-12-27T09:44:58+00:00"
},
{
@ -189,10 +181,6 @@
"polyfill",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
},
"time": "2019-11-04T15:17:54+00:00"
},
{
@ -243,10 +231,6 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
},
"time": "2019-08-28T14:22:28+00:00"
},
{
@ -343,10 +327,6 @@
"woocommerce",
"wordpress"
],
"support": {
"issues": "https://github.com/woocommerce/woocommerce-sniffs/issues",
"source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master"
},
"time": "2020-08-06T18:23:45+00:00"
},
{
@ -393,11 +373,6 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
},
"time": "2020-05-13T23:57:56+00:00"
}
],
@ -411,5 +386,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "1.1.0"
}

View File

@ -332,10 +332,6 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x"
},
"time": "2019-12-28T18:55:12+00:00"
},
{
@ -448,10 +444,6 @@
"spy",
"stub"
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
},
"time": "2020-03-05T15:02:03+00:00"
},
{
@ -612,10 +604,6 @@
"keywords": [
"template"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
"source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
},
"time": "2015-06-21T13:50:34+00:00"
},
{
@ -1236,10 +1224,6 @@
"keywords": [
"global state"
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
},
"time": "2017-04-27T15:39:26+00:00"
},
{
@ -1504,10 +1488,6 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
"source": "https://github.com/sebastianbergmann/version/tree/master"
},
"time": "2016-10-03T07:35:21+00:00"
},
{
@ -1627,10 +1607,6 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/master"
},
"time": "2019-06-13T22:48:21+00:00"
},
{
@ -1680,10 +1656,6 @@
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozart/assert/issues",
"source": "https://github.com/webmozart/assert/tree/master"
},
"time": "2020-07-08T17:02:28+00:00"
}
],
@ -1697,5 +1669,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "1.1.0"
}

View File

@ -133,24 +133,20 @@
"translations",
"unicode"
],
"support": {
"issues": "https://github.com/php-gettext/Languages/issues",
"source": "https://github.com/php-gettext/Languages/tree/2.6.0"
},
"time": "2019-11-13T10:30:21+00:00"
},
{
"name": "mck89/peast",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/mck89/peast.git",
"reference": "2a2bc6826114c46ff0bc1359208b7083a17f7a99"
"reference": "833be7a294627a8c5b1c482cbf489f73bf9b8086"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mck89/peast/zipball/2a2bc6826114c46ff0bc1359208b7083a17f7a99",
"reference": "2a2bc6826114c46ff0bc1359208b7083a17f7a99",
"url": "https://api.github.com/repos/mck89/peast/zipball/833be7a294627a8c5b1c482cbf489f73bf9b8086",
"reference": "833be7a294627a8c5b1c482cbf489f73bf9b8086",
"shasum": ""
},
"require": {
@ -162,7 +158,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11.0-dev"
"dev-master": "1.12.0-dev"
}
},
"autoload": {
@ -182,11 +178,7 @@
}
],
"description": "Peast is PHP library that generates AST for JavaScript code",
"support": {
"issues": "https://github.com/mck89/peast/issues",
"source": "https://github.com/mck89/peast/tree/v1.11.0"
},
"time": "2020-10-09T15:12:13+00:00"
"time": "2021-01-08T15:16:19+00:00"
},
{
"name": "mustache/mustache",
@ -232,10 +224,6 @@
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/master"
},
"time": "2019-11-23T21:40:31+00:00"
},
{
@ -285,10 +273,6 @@
"iri",
"sockets"
],
"support": {
"issues": "https://github.com/rmccue/Requests/issues",
"source": "https://github.com/rmccue/Requests/tree/master"
},
"time": "2016-10-13T00:11:37+00:00"
},
{
@ -398,10 +382,6 @@
],
"description": "Provides internationalization tools for WordPress projects.",
"homepage": "https://github.com/wp-cli/i18n-command",
"support": {
"issues": "https://github.com/wp-cli/i18n-command/issues",
"source": "https://github.com/wp-cli/i18n-command/tree/v2.2.6"
},
"time": "2020-12-07T19:28:27+00:00"
},
{
@ -450,9 +430,6 @@
],
"description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)",
"homepage": "https://github.com/mustangostang/spyc/",
"support": {
"source": "https://github.com/wp-cli/spyc/tree/autoload"
},
"time": "2017-04-25T11:26:20+00:00"
},
{
@ -503,10 +480,6 @@
"cli",
"console"
],
"support": {
"issues": "https://github.com/wp-cli/php-cli-tools/issues",
"source": "https://github.com/wp-cli/php-cli-tools/tree/master"
},
"time": "2018-09-04T13:28:00+00:00"
},
{
@ -569,11 +542,6 @@
"cli",
"wordpress"
],
"support": {
"docs": "https://make.wordpress.org/cli/handbook/",
"issues": "https://github.com/wp-cli/wp-cli/issues",
"source": "https://github.com/wp-cli/wp-cli"
},
"time": "2020-02-18T08:15:37+00:00"
}
],
@ -587,5 +555,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "1.1.0"
}

View File

@ -1,6 +1,14 @@
== Changelog ==
= 4.9.0 =
= 4.9.2 2021-01-25 =
* Tweak - Disable untested plugin's notice from System Status and Plugin's page. #28840
= 4.9.1 2021-01-19 =
* Fix - Reverts #28204 to ensure compatibility with extensions using legacy do_action calls. #28835
= 4.9.0 2021-01-12 =
**WooCommerce**
* Localization - Add 'Ladakh' to the list of Indian states. #28458
@ -28,6 +36,9 @@
* Fix - Better error messages when usage limit are reached. #28592
* Fix - Adjust stock items only for statuses which reduces the stock. #28620
* Fix - Named parameter to fix DB update routine on PHP8. #28537
* Fix - Add protection around func_get_args_call for backwards compatibility. #28677
* Fix - Restore stock_status in REST API V3 response. #28731
* Fix - Revert some of the changes related to perf enhancements (27735) as it was breaking stock_status filter. #28745
* Dev - Hook for intializing price slider in frontend. #28014
* Dev - Add support for running a custom initialization script for tests. #28041
* Dev - Use the coenjacobs/mozart package to renamespace vendor packages. #28147
@ -70,6 +81,7 @@
* Fix - Preventing desktop-sized navigation placeholder from appearing on mobile during load. #5616
* Fix - Completed tasks tracking causing infinite loop #5941
* Fix - Remove Navigation access #5940
* Fix - Compile the debug module so it can be used in older browsers like IE11. #5968
* Tweak - Fix inconsistent REST API parameter name for customer type filtering. #5823
* Tweak - Improve styles of the tax task. #5709
* Tweak - Do not show store setup link on the homescreen. #5801

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": "1.8.3",
"woocommerce/woocommerce-admin": "1.9.0",
"woocommerce/woocommerce-blocks": "4.0.0"
},
"require-dev": {

72
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": "758b097c13e89200b28bf3a3b5fc2752",
"content-hash": "5fac5fbdbcff552de109cd95d469e975",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -77,23 +77,20 @@
"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"
},
{
"name": "composer/installers",
"version": "v1.9.0",
"version": "v1.10.0",
"source": {
"type": "git",
"url": "https://github.com/composer/installers.git",
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca"
"reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"url": "https://api.github.com/repos/composer/installers/zipball/1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d",
"reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d",
"shasum": ""
},
"require": {
@ -104,17 +101,18 @@
"shama/baton": "*"
},
"require-dev": {
"composer/composer": "1.6.* || 2.0.*@dev",
"composer/semver": "1.0.* || 2.0.*@dev",
"phpunit/phpunit": "^4.8.36",
"sebastian/comparator": "^1.2.4",
"composer/composer": "1.6.* || ^2.0",
"composer/semver": "^1 || ^3",
"phpstan/phpstan": "^0.12.55",
"phpstan/phpstan-phpunit": "^0.12.16",
"symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.3"
},
"type": "composer-plugin",
"extra": {
"class": "Composer\\Installers\\Plugin",
"branch-alias": {
"dev-master": "1.0-dev"
"dev-main": "1.x-dev"
}
},
"autoload": {
@ -152,6 +150,7 @@
"Porto",
"RadPHP",
"SMF",
"Starbug",
"Thelia",
"Whmcs",
"WolfCMS",
@ -192,6 +191,7 @@
"phpbb",
"piwik",
"ppi",
"processwire",
"puppet",
"pxcms",
"reindex",
@ -207,21 +207,21 @@
"zend",
"zikula"
],
"support": {
"issues": "https://github.com/composer/installers/issues",
"source": "https://github.com/composer/installers/tree/v1.9.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-04-07T06:57:05+00:00"
"time": "2021-01-14T11:07:16+00:00"
},
{
"name": "maxmind-db/reader",
@ -281,10 +281,6 @@
"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"
},
{
@ -359,10 +355,6 @@
"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"
},
{
@ -412,10 +404,6 @@
"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"
},
{
@ -507,24 +495,20 @@
],
"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": "1.8.3",
"version": "1.9.0-rc.3",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "534442980c34369f711efdb8e85fdcb1363be712"
"reference": "8c5b20cb6347959daf5403ee30e47061f335240b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/534442980c34369f711efdb8e85fdcb1363be712",
"reference": "534442980c34369f711efdb8e85fdcb1363be712",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/8c5b20cb6347959daf5403ee30e47061f335240b",
"reference": "8c5b20cb6347959daf5403ee30e47061f335240b",
"shasum": ""
},
"require": {
@ -556,11 +540,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v1.8.3"
},
"time": "2021-01-06T00:00:42+00:00"
"time": "2021-01-22T10:23:29+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -659,10 +639,6 @@
"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"
}
],
@ -678,5 +654,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "1.1.0"
}

View File

@ -1270,6 +1270,8 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
protected function set_item_discount_amounts( $discounts ) {
$item_discounts = $discounts->get_discounts_by_item();
$tax_location = $this->get_tax_location();
$tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );
if ( $item_discounts ) {
foreach ( $item_discounts as $item_id => $amount ) {
@ -1277,7 +1279,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
// If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart.
if ( $this->get_prices_include_tax() && wc_tax_enabled() && 'taxable' === $item->get_tax_status() ) {
$taxes = WC_Tax::calc_tax( $amount, WC_Tax::get_rates( $item->get_tax_class() ), true );
$taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true );
// Use unrounded taxes so totals will be re-calculated accurately, like in cart.
$amount = $amount - array_sum( $taxes );
@ -1299,6 +1301,13 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' );
$all_discounts = $discounts->get_discounts();
$coupon_discounts = $discounts->get_discounts_by_coupon();
$tax_location = $this->get_tax_location();
$tax_location = array(
$tax_location['country'],
$tax_location['state'],
$tax_location['postcode'],
$tax_location['city'],
);
if ( $coupon_discounts ) {
foreach ( $coupon_discounts as $coupon_code => $amount ) {
@ -1321,7 +1330,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
continue;
}
$taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, WC_Tax::get_rates( $item->get_tax_class() ), $this->get_prices_include_tax() ) );
$taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), $this->get_prices_include_tax() ) );
if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) {
$taxes = wc_round_tax_total( $taxes );
}
@ -1514,6 +1523,21 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
return apply_filters( 'woocommerce_order_get_tax_location', $args, $this );
}
/**
* Get tax rates for an order. Use order's shipping or billing address, defaults to base location.
*
* @param string $tax_class Tax class to get rates for.
* @param array $location_args Location to compute rates for. Should be in form: array( country, state, postcode, city).
* @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`.
*
* @return mixed|void Tax rates.
*/
protected function get_tax_rates( $tax_class, $location_args = array(), $customer = null ) {
$tax_location = $this->get_tax_location( $location_args );
$tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] );
return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer );
}
/**
* Calculate taxes for all line items and shipping, and store the totals and tax rows.
*

View File

@ -64,6 +64,51 @@ class WC_Admin_Notices {
}
}
/**
* Parses query to create nonces when available.
*
* @param object $response The WP_REST_Response we're working with.
* @return object $response The prepared WP_REST_Response object.
*/
public static function prepare_note_with_nonce( $response ) {
if ( 'wc-update-db-reminder' !== $response->data['name'] || ! isset( $response->data['actions'] ) ) {
return $response;
}
foreach ( $response->data['actions'] as $action_key => $action ) {
$url_parts = ! empty( $action->query ) ? wp_parse_url( $action->query ) : '';
if ( ! isset( $url_parts['query'] ) ) {
continue;
}
wp_parse_str( $url_parts['query'], $params );
if ( array_key_exists( '_nonce_action', $params ) && array_key_exists( '_nonce_name', $params ) ) {
$org_params = $params;
// Check to make sure we're acting on the whitelisted nonce actions.
if ( 'wc_db_update' !== $params['_nonce_action'] && 'woocommerce_hide_notices_nonce' !== $params['_nonce_action'] ) {
continue;
}
unset( $org_params['_nonce_action'] );
unset( $org_params['_nonce_name'] );
$url = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path'];
$nonce = array( $params['_nonce_name'] => wp_create_nonce( $params['_nonce_action'] ) );
$merged_params = array_merge( $nonce, $org_params );
$parsed_query = add_query_arg( $merged_params, $url );
$response->data['actions'][ $action_key ]->query = $parsed_query;
$response->data['actions'][ $action_key ]->url = $parsed_query;
}
}
return $response;
}
/**
* Store notices to DB
*/

View File

@ -118,7 +118,9 @@ class WC_Meta_Box_Order_Actions {
WC()->payment_gateways();
WC()->shipping();
add_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' );
WC()->mailer()->emails['WC_Email_New_Order']->trigger( $order->get_id(), $order, true );
remove_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' );
do_action( 'woocommerce_after_resend_order_email', $order, 'new_order' );

View File

@ -110,10 +110,13 @@ class WC_Notes_Run_Db_Update {
*/
private static function update_needed_notice( $note_id = null ) {
$update_url = html_entity_decode(
wp_nonce_url(
add_query_arg( 'do_update_woocommerce', 'true', wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' ) ),
'wc_db_update',
'wc_db_update_nonce'
add_query_arg(
array(
'do_update_woocommerce' => 'true',
'_nonce_action' => 'wc_db_update',
'_nonce_name' => 'wc_db_update_nonce',
),
wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' )
)
);
@ -206,14 +209,13 @@ class WC_Notes_Run_Db_Update {
*/
private static function update_done_notice( $note_id ) {
$hide_notices_url = html_entity_decode( // to convert &s to normal &, otherwise produces invalid link.
wp_nonce_url(
add_query_arg(
'wc-hide-notice',
'update',
wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' )
add_query_arg(
array(
'wc-hide-notice' => 'update',
'_nonce_action' => 'woocommerce_hide_notices_nonce',
'_nonce_name' => '_wc_notice_nonce',
),
'woocommerce_hide_notices_nonce',
'_wc_notice_nonce'
wc_get_current_admin_url() ? remove_query_arg( 'do_update_woocommerce', wc_get_current_admin_url() ) : admin_url( 'admin.php?page=wc-settings' )
)
);

View File

@ -64,6 +64,10 @@ 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

@ -698,11 +698,11 @@ final class WC_Cart_Totals {
/**
* Subtotals are costs before discounts.
*
* To prevent rounding issues we need to work with the inclusive price where possible.
* otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would.
* To prevent rounding issues we need to work with the inclusive price where possible
* otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would
* be 8.325 leading to totals being 1p off.
*
* Pre tax coupons come off the price the customer thinks they are paying - tax is calculated.
* Pre tax coupons come off the price the customer thinks they are paying - tax is calculated
* afterwards.
*
* e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that.

View File

@ -210,7 +210,7 @@ class WC_Cart extends WC_Legacy_Cart {
}
/**
* Get subtotal.
* Get subtotal_tax.
*
* @since 3.2.0
* @return float

View File

@ -20,7 +20,7 @@ class WC_Payment_Tokens {
* Gets valid tokens from the database based on user defined criteria.
*
* @since 2.6.0
* @param array $args Query argyments {
* @param array $args Query arguments {
* Array of query parameters.
*
* @type string $token_id Token ID.

View File

@ -405,7 +405,7 @@ class WC_Tax {
$criteria_string = implode( ' AND ', $criteria );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$found_rates = $wpdb->get_results(
"
SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count
@ -479,8 +479,22 @@ class WC_Tax {
* @return array
*/
public static function get_rates( $tax_class = '', $customer = null ) {
$tax_class = sanitize_title( $tax_class );
$location = self::get_tax_location( $tax_class, $customer );
return self::get_rates_from_location( $tax_class, $location, $customer );
}
/**
* Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter.
*
* @param string $tax_class Tax class to get rates for.
* @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city).
* @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`.
*
* @return mixed|void Tax rates.
*/
public static function get_rates_from_location( $tax_class, $location, $customer = null ) {
$tax_class = sanitize_title( $tax_class );
$location = self::get_tax_location( $tax_class, $customer );
$matched_tax_rates = array();
if ( count( $location ) === 4 ) {

View File

@ -364,9 +364,11 @@ class WC_Tracker {
);
$first = time();
$processing_first = $first;
$first_time = $first;
$last = 0;
$processing_first = time();
$processing_last = 0;
$order_data = array();
$orders = wc_get_orders( $args );
$orders_count = count( $orders );
@ -445,10 +447,25 @@ class WC_Tracker {
$orders_count = count( $orders );
}
$order_data['first'] = gmdate( 'Y-m-d H:i:s', $first );
$order_data['last'] = gmdate( 'Y-m-d H:i:s', $last );
$order_data['processing_first'] = gmdate( 'Y-m-d H:i:s', $processing_first );
$order_data['processing_last'] = gmdate( 'Y-m-d H:i:s', $processing_last );
if ( $first !== $first_time ) {
$order_data['first'] = gmdate( 'Y-m-d H:i:s', $first );
}
if ( $processing_first !== $first_time ) {
$order_data['processing_first'] = gmdate( 'Y-m-d H:i:s', $processing_first );
}
if ( $last ) {
$order_data['last'] = gmdate( 'Y-m-d H:i:s', $last );
}
if ( $processing_last ) {
$order_data['processing_last'] = gmdate( 'Y-m-d H:i:s', $processing_last );
}
foreach ( $order_data as $key => $value ) {
$order_data[ $key ] = (string) $value;
}
return $order_data;
}

View File

@ -23,7 +23,7 @@ final class WooCommerce {
*
* @var string
*/
public $version = '5.0.0';
public $version = '5.1.0';
/**
* WooCommerce Schema version.
@ -203,6 +203,7 @@ final class WooCommerce {
add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 );
add_action( 'activated_plugin', array( $this, 'activated_plugin' ) );
add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) );
add_filter( 'woocommerce_rest_prepare_note', array( 'WC_Admin_Notices', 'prepare_note_with_nonce' ) );
// These classes set up hooks on instantiation.
wc_get_container()->get( DownloadPermissionsAdjuster::class );

View File

@ -818,7 +818,7 @@ class WC_Shop_Customizer {
'description' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ),
'section' => 'woocommerce_checkout',
'settings' => 'woocommerce_checkout_privacy_policy_text',
'active_callback' => 'wc_privacy_policy_page_id',
'active_callback' => array( $this, 'has_privacy_policy_page_id' ),
'type' => 'textarea',
)
);
@ -830,7 +830,7 @@ class WC_Shop_Customizer {
'description' => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ),
'section' => 'woocommerce_checkout',
'settings' => 'woocommerce_checkout_terms_and_conditions_checkbox_text',
'active_callback' => 'wc_terms_and_conditions_page_id',
'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ),
'type' => 'text',
)
);
@ -865,6 +865,24 @@ class WC_Shop_Customizer {
$options = array( 'hidden', 'optional', 'required' );
return in_array( $value, $options, true ) ? $value : '';
}
/**
* Whether or not a page has been chose for the privacy policy.
*
* @return bool
*/
public function has_privacy_policy_page_id() {
return wc_privacy_policy_page_id() > 0;
}
/**
* Whether or not a page has been chose for the terms and conditions.
*
* @return bool
*/
public function has_terms_and_conditions_page_id() {
return wc_terms_and_conditions_page_id() > 0;
}
}
new WC_Shop_Customizer();

View File

@ -906,7 +906,14 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
foreach ( $query->posts as $post ) {
$orders[] = wc_get_order( $post );
$order = wc_get_order( $post );
// If the order returns false, don't add it to the list.
if ( false === $order ) {
continue;
}
$orders[] = $order;
}
return $orders;

View File

@ -272,6 +272,9 @@ 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

@ -80,9 +80,8 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) :
*
* @param int $order_id The order ID.
* @param WC_Order|false $order Order object.
* @param bool $force_send Whether to force send the email.
*/
public function trigger( $order_id, $order = false, $force_send = false ) {
public function trigger( $order_id, $order = false ) {
$this->setup_locale();
if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
@ -97,7 +96,13 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) :
$email_already_sent = $order->get_meta( '_new_order_email_sent' );
}
if ( 'true' === $email_already_sent && ! $force_send ) {
/**
* Controls if new order emails can be resend multiple times.
*
* @since 5.0.0
* @param bool $allows Defaults to true.
*/
if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) {
return;
}

View File

@ -574,10 +574,33 @@ function wc_price( $price, $args = array() ) {
)
);
$original_price = $price;
// Convert to float to avoid issues on PHP 8.
$price = (float) $price;
$unformatted_price = $price;
$negative = $price < 0;
$price = apply_filters( 'raw_woocommerce_price', floatval( $negative ? $price * -1 : $price ) );
$price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] );
/**
* Filter raw price.
*
* @param float $raw_price Raw price.
* @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
*/
$price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price );
/**
* Filter formatted price.
*
* @param float $formatted_price Formatted price.
* @param float $price Unformatted price.
* @param int $decimals Number of decimals.
* @param string $decimal_separator Decimal separator.
* @param string $thousand_separator Thousand separator.
* @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
*/
$price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price );
if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) {
$price = wc_trim_zeros( $price );
@ -593,12 +616,13 @@ function wc_price( $price, $args = array() ) {
/**
* Filters the string of price markup.
*
* @param string $return Price HTML markup.
* @param string $price Formatted price.
* @param array $args Pass on the args.
* @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0.
* @param string $return Price HTML markup.
* @param string $price Formatted price.
* @param array $args Pass on the args.
* @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0.
* @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
*/
return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price );
return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price );
}
/**

View File

@ -651,7 +651,7 @@ function wc_get_product_id_by_sku( $sku ) {
}
/**
* Get attibutes/data for an individual variation from the database and maintain it's integrity.
* Get attributes/data for an individual variation from the database and maintain it's integrity.
*
* @since 2.4.0
* @param int $variation_id Variation ID.

View File

@ -352,7 +352,7 @@ function wc_no_js() {
var c = document.body.className;
c = c.replace(/woocommerce-no-js/, 'woocommerce-js');
document.body.className = c;
})()
})();
</script>
<?php
}
@ -3568,7 +3568,12 @@ function wc_empty_cart_message() {
*/
function wc_page_noindex() {
if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) {
wp_no_robots();
// Adds support for WP 5.7.
if ( function_exists( 'wp_robots_no_robots' ) ) {
wp_robots_no_robots();
} else {
wp_no_robots();
}
}
}
add_action( 'wp_head', 'wc_page_noindex' );

View File

@ -1,7 +1,7 @@
{
"name": "woocommerce",
"title": "WooCommerce",
"version": "5.0.0",
"version": "5.1.0",
"homepage": "https://woocommerce.com/",
"repository": {
"type": "git",

View File

@ -1,10 +1,10 @@
=== WooCommerce ===
Contributors: automattic, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho
Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce
Requires at least: 5.3
Requires at least: 5.4
Tested up to: 5.6
Requires PHP: 7.0
Stable tag: 4.8.0
Stable tag: 4.9.2
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -160,11 +160,6 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
= 4.9.0 - 2021-01-xx =
= 5.1.0 2021-03-xx =
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/changelog.txt).
== Upgrade Notice ==
= 4.0 =
4.0 is a major update. Make a full site backup, update your theme and extensions, and [review update best practices](https://docs.woocommerce.com/document/how-to-update-your-site) before upgrading.

View File

@ -83,6 +83,9 @@ class DownloadPermissionsAdjuster {
*/
public function adjust_download_permissions( int $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
$children_ids = $product->get_children();
if ( ! $children_ids ) {

View File

@ -30,7 +30,7 @@ $wrapper_classes = apply_filters(
'woocommerce_single_product_image_gallery_classes',
array(
'woocommerce-product-gallery',
'woocommerce-product-gallery--' . ( $product->get_image_id() ? 'with-images' : 'without-images' ),
'woocommerce-product-gallery--' . ( $post_thumbnail_id ? 'with-images' : 'without-images' ),
'woocommerce-product-gallery--columns-' . absint( $columns ),
'images',
)
@ -39,7 +39,7 @@ $wrapper_classes = apply_filters(
<div class="<?php echo esc_attr( implode( ' ', array_map( 'sanitize_html_class', $wrapper_classes ) ) ); ?>" data-columns="<?php echo esc_attr( $columns ); ?>" style="opacity: 0; transition: opacity .25s ease-in-out;">
<figure class="woocommerce-product-gallery__wrapper">
<?php
if ( $product->get_image_id() ) {
if ( $post_thumbnail_id ) {
$html = wc_get_gallery_image_html( $post_thumbnail_id, true );
} else {
$html = '<div class="woocommerce-product-gallery__image--placeholder">';

View File

@ -1,5 +1,7 @@
# Unreleased
# 0.1.1
## Breaking Changes
- The `HTTPClientFactory` API was changed to make it easier to configure instances with
@ -14,6 +16,11 @@
- Added a tranformation layer between API responses and internal models
## Fixed
- issues that caused the factory creation to fail for SimpleProduct types
- a bug with OAuth signature generation when using query parameters
# 0.1.0
- Initial/beta release

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/api",
"version": "0.1.0",
"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",

View File

@ -1,6 +1,10 @@
# Unreleased
# 0.1.1
## Added
- Registered Shopper Checkout tests
- Merchant Order Status Filter tests
- Merchant Order Refund tests
- Merchant Apply Coupon tests
@ -8,6 +12,9 @@
- Shopper Checkout Apply Coupon
- Shopper Cart Apply Coupon
## Fixed
- Flaky Create Product, Coupon, and Order tests

View File

@ -37,7 +37,7 @@ The functions to access the core tests are:
### Activation and setup
- `runSetupOnboardingTests` - Run all setup and onboarding tests
- `runActivationTest` - Merchant can activate WooCommerce
- `runActivationTest` - Merchant can activate WooCommerce
- `runOnboardingFlowTest` - Merchant can complete onboarding flow
- `runTaskListTest` - Merchant can complete onboarding task list
- `runInitialStoreSettingsTest` - Merchant can complete initial settings
@ -45,7 +45,7 @@ The functions to access the core tests are:
### Merchant
- `runMerchantTests` - Run all merchant tests
- `runCreateCouponTest` - Merchant can create coupon
- `runCreateCouponTest` - Merchant can create coupon
- `runCreateOrderTest` - Merchant can create order
- `runAddSimpleProductTest` - Merchant can create a simple product
- `runAddVariableProductTest` - Merchant can create a variable product
@ -59,10 +59,10 @@ The functions to access the core tests are:
### Shopper
- `runShopperTests` - Run all shopper tests
- `runCartPageTest` - Shopper can view and update cart
- `runCartPageTest` - Shopper can view and update cart
- `runCheckoutPageTest` - Shopper can complete checkout
- `runMyAccountPageTest` - Shopper can access my account page
- `runSingleProductPageTest` - Shopper can view single product page
- `runSingleProductPageTest` - Shopper can view single product page
## Contributing a new test
@ -92,7 +92,7 @@ const runExampleTestName = () => {
module.exports = runExampleTestName;
```
- Add your test to `tests/e2e/core-tests/specs/index.js`
- Add your test to `tests/e2e/core-tests/specs/index.js`
```js
const runExampleTestName = require( './grouping/example-test-name.test' );
// ...

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-core-tests",
"version": "0.1.0",
"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",
"repository": {
@ -14,7 +14,7 @@
"config": "3.3.3"
},
"peerDependencies": {
"@woocommerce/e2e-utils": "^0.1.1"
"@woocommerce/e2e-utils": "^0.1.2"
},
"publishConfig": {
"access": "public"

View File

@ -9,6 +9,7 @@ const { runOnboardingFlowTest, runTaskListTest } = require( './activate-and-setu
const runInitialStoreSettingsTest = require( './activate-and-setup/setup.test' );
// Shopper tests
const runCartApplyCouponsTest = require( './shopper/front-end-cart-coupons.test');
const runCartPageTest = require( './shopper/front-end-cart.test' );
const runCheckoutApplyCouponsTest = require( './shopper/front-end-checkout-coupons.test');
const runCheckoutPageTest = require( './shopper/front-end-checkout.test' );
@ -34,6 +35,7 @@ const runSetupOnboardingTests = () => {
};
const runShopperTests = () => {
runCartApplyCouponsTest();
runCartPageTest();
runCheckoutApplyCouponsTest();
runCheckoutPageTest();
@ -60,6 +62,7 @@ module.exports = {
runTaskListTest,
runInitialStoreSettingsTest,
runSetupOnboardingTests,
runCartApplyCouponsTest,
runCartPageTest,
runCheckoutApplyCouponsTest,
runCheckoutPageTest,

View File

@ -0,0 +1,108 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests, jest/expect-expect */
/**
* Internal dependencies
*/
const {
shopper,
merchant,
createCoupon,
createSimpleProduct,
uiUnblocked
} = require( '@woocommerce/e2e-utils' );
/**
* External dependencies
*/
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const runCartApplyCouponsTest = () => {
describe('Cart applying coupons', () => {
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
beforeAll(async () => {
await merchant.login();
await createSimpleProduct();
couponFixedCart = await createCoupon();
couponPercentage = await createCoupon('50', 'Percentage discount');
couponFixedProduct = await createCoupon('5', 'Fixed product discount');
await merchant.logout();
});
it('allows customer to apply coupons in the cart', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage('Simple product');
await shopper.goToCart();
await shopper.productIsInCart('Simple product');
// Apply Fixed cart discount coupon
await expect(page).toFill('#coupon_code', couponFixedCart);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Wait for page to expand total calculations to avoid flakyness
await page.waitForSelector('.order-total');
// Verify discount applied and order total
await page.waitForSelector('.cart-discount .amount', {text: '$5.00'});
await page.waitForSelector('.order-total .amount', {text: '$4.99'});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
// Apply Percentage discount coupon
await expect(page).toFill('#coupon_code', couponPercentage);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.cart-discount .amount', {text: '$4.99'});
await page.waitForSelector('.order-total .amount', {text: '$5.00'});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
// Apply Fixed product discount coupon
await expect(page).toFill('#coupon_code', couponFixedProduct);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.cart-discount .amount', {text: '$5.00'});
await page.waitForSelector('.order-total .amount', {text: '$4.99'});
// Try to apply the same coupon
await expect(page).toFill('#coupon_code', couponFixedProduct);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-error', { text: 'Coupon code already applied!' });
// Try to apply multiple coupons
await expect(page).toFill('#coupon_code', couponFixedCart);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.order-total .amount', {text: '$0.00'});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
// Verify the total amount after all coupons removal
await page.waitForSelector('.order-total .amount', {text: '$9.99'});
});
});
};
module.exports = runCartApplyCouponsTest;

View File

@ -7,7 +7,8 @@ const {
merchant,
createCoupon,
createSimpleProduct,
uiUnblocked
uiUnblocked,
clearAndFillInput,
} = require( '@woocommerce/e2e-utils' );
/**
@ -19,11 +20,36 @@ const {
beforeAll,
} = require( '@jest/globals' );
/**
* Apply a coupon code to the cart.
*
* @param couponCode string
* @returns {Promise<void>}
*/
const applyCouponToCart = async ( couponCode ) => {
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
};
/**
* Remove one coupon from the cart.
*
* @returns {Promise<void>}
*/
const removeCouponFromCart = async () => {
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
}
const runCheckoutApplyCouponsTest = () => {
describe('Checkout applying coupons', () => {
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
describe('Checkout coupons', () => {
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
beforeAll(async () => {
await merchant.login();
await createSimpleProduct();
@ -31,86 +57,63 @@ const runCheckoutApplyCouponsTest = () => {
couponPercentage = await createCoupon('50', 'Percentage discount');
couponFixedProduct = await createCoupon('5', 'Fixed product discount');
await merchant.logout();
});
it('allows customer to apply coupons in the checkout', async () => {
await shopper.goToShop();
await shopper.addToCartFromShopPage('Simple product');
await uiUnblocked();
await shopper.goToCheckout();
});
// Apply Fixed cart discount coupon
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await expect(page).toFill('#coupon_code', couponFixedCart);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
it('allows customer to apply fixed cart coupon', async () => {
await applyCouponToCart( couponFixedCart );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Wait for page to expand total calculations to avoid flakyness
await page.waitForSelector('.order-total');
// Verify discount applied and order total
await page.waitForSelector('.cart-discount .amount', {text: '$5.00'});
await page.waitForSelector('.order-total .amount', {text: '$4.99'});
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
it('allows customer to apply percentage coupon', async () => {
await applyCouponToCart( couponPercentage );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Apply Percentage discount coupon
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await expect(page).toFill('#coupon_code', couponPercentage);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.cart-discount .amount', {text: '$4.99'});
await page.waitForSelector('.order-total .amount', {text: '$5.00'});
// Verify discount applied and order total
await expect(page).toMatchElement('.cart-discount .amount', {text: '$4.99'});
await expect(page).toMatchElement('.order-total .amount', {text: '$5.00'});
await removeCouponFromCart();
});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
it('allows customer to apply fixed product coupon', async () => {
await applyCouponToCart( couponFixedProduct );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
await removeCouponFromCart();
});
// Apply Fixed product discount coupon
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await expect(page).toFill('#coupon_code', couponFixedProduct);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.cart-discount .amount', {text: '$5.00'});
await page.waitForSelector('.order-total .amount', {text: '$4.99'});
it('prevents customer applying same coupon twice', async () => {
await applyCouponToCart( couponFixedCart );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await applyCouponToCart( couponFixedCart );
// Verify only one discount applied
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
await expect(page).toMatchElement('.order-total .amount', {text: '$4.99'});
});
// Try to apply the same coupon
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await expect(page).toFill('#coupon_code', couponFixedProduct);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-error', { text: 'Coupon code already applied!' });
it('allows customer to apply multiple coupons', async () => {
await applyCouponToCart( couponFixedProduct );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await expect(page).toMatchElement('.order-total .amount', {text: '$0.00'});
});
// Try to apply multiple coupons
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await uiUnblocked();
await expect(page).toFill('#coupon_code', couponFixedCart);
await expect(page).toClick('button', {text: 'Apply coupon'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await page.waitForSelector('.order-total .amount', {text: '$0.00'});
// Remove coupon
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
await expect(page).toClick('.woocommerce-remove-coupon', {text: '[Remove]'});
await uiUnblocked();
await page.waitForSelector('.woocommerce-message', {text: 'Coupon has been removed.'});
// Verify the total amount after all coupons removal
await page.waitForSelector('.order-total .amount', {text: '$9.99'});
it('restores cart total when coupons are removed', async () => {
await removeCouponFromCart();
await removeCouponFromCart();
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
});
});
};

View File

@ -20,7 +20,8 @@ const threeProductPrice = singleProductPrice * 3;
const fourProductPrice = singleProductPrice * 4;
const fiveProductPrice = singleProductPrice * 5;
let orderId;
let guestOrderId;
let customerOrderId;
const runCheckoutPageTest = () => {
describe('Checkout page', () => {
@ -136,34 +137,36 @@ const runCheckoutPageTest = () => {
// Get order ID from the order received html element on the page
let orderReceivedHtmlElement = await page.$('.woocommerce-order-overview__order.order');
let orderReceivedText = await page.evaluate(element => element.textContent, orderReceivedHtmlElement);
return orderId = orderReceivedText.split(/(\s+)/)[6].toString();
return guestOrderId = orderReceivedText.split(/(\s+)/)[6].toString();
});
it('allows existing customer to place order', async () => {
await shopper.login();
await shopper.goToShop();
await shopper.addToCartFromShopPage(simpleProductName);
await shopper.goToCheckout();
await shopper.productIsInCheckout(simpleProductName, `1`, singleProductPrice, singleProductPrice);
await shopper.fillBillingDetails(config.get('addresses.customer.billing'));
await uiUnblocked();
await expect(page).toClick('.wc_payment_method label', {text: 'Cash on delivery'});
await expect(page).toMatchElement('.payment_method_cod', {text: 'Pay with cash upon delivery.'});
await uiUnblocked();
await shopper.placeOrder();
await expect(page).toMatch('Order received');
// Get order ID from the order received html element on the page
let orderReceivedHtmlElement = await page.$('.woocommerce-order-overview__order.order');
let orderReceivedText = await page.evaluate(element => element.textContent, orderReceivedHtmlElement);
return customerOrderId = orderReceivedText.split(/(\s+)/)[6].toString();
});
it('store owner can confirm the order was received', async () => {
await merchant.login();
await merchant.openAllOrdersView();
// Click on the order which was placed in the previous step
await Promise.all([
page.click('#post-' + orderId),
page.waitForNavigation({waitUntil: 'networkidle0'}),
]);
// Verify that the order page is indeed of the order that was placed
// Verify order number
await expect(page).toMatchElement('.woocommerce-order-data__heading', {text: 'Order #' + orderId + ' details'});
// Verify product name
await expect(page).toMatchElement('.wc-order-item-name', {text: simpleProductName});
// Verify product cost
await expect(page).toMatchElement('.woocommerce-Price-amount.amount', {text: singleProductPrice});
// Verify product quantity
await expect(page).toMatchElement('.quantity', {text: '5'});
// Verify total order amount without shipping
await expect(page).toMatchElement('.line_cost', {text: fiveProductPrice});
await merchant.verifyOrder(guestOrderId, simpleProductName, singleProductPrice, 5, fiveProductPrice);
await merchant.verifyOrder(customerOrderId, simpleProductName, singleProductPrice, 1, singleProductPrice, true);
});
});
};

View File

@ -1,5 +1,11 @@
# Unreleased
# 0.2.0
## Fixed
- Return jest exit code from `npx wc-e2e test:e2e*`
## Added
- support for custom container name
@ -9,7 +15,7 @@
## Fixed
- Remove redundant `puppeteer` dependency
- Support for admin user configuration from default.json
- Support for admin user configuration from `default.json`
# 0.1.6

View File

@ -25,6 +25,9 @@ fi
# Store original path
OLDPATH=$(pwd)
# Return value for CI test runs
TESTRESULT=0
# Use the script symlink to find and change directory to the root of the package
SCRIPTPATH=$(dirname "$0")
REALPATH=$(readlink $0)
@ -46,12 +49,15 @@ case $1 in
;;
'test:e2e')
./bin/wait-for-build.sh && ./bin/e2e-test-integration.js $2
TESTRESULT=$?
;;
'test:e2e-dev')
./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev $2
TESTRESULT=$?
;;
'test:e2e-debug')
./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev --debug $2
TESTRESULT=$?
;;
*)
usage
@ -60,3 +66,5 @@ esac
# Restore working path
cd $OLDPATH
exit $TESTRESULT

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-environment",
"version": "0.1.6",
"version": "0.2.0",
"description": "WooCommerce End to End Testing Environment Configuration.",
"author": "Automattic",
"license": "GPL-3.0-or-later",

View File

@ -18,6 +18,21 @@ import {
* @type {Array}
*/
const pageEvents = [];
/**
* Set of logged messages that will only be logged once.
*
* @type {Object<string,object>}
*/
const loggedMessages = {
proxy: {
logged: false,
text: 'Failed to load resource: net::ERR_PROXY_CONNECTION_FAILED',
},
http404: {
logged: false,
text: 'the server responded with a status of 404',
},
};
/**
* Set of console logging types observed to protect against unexpected yet
* handled (i.e. not catastrophic) errors or warnings. Each key corresponds
@ -140,16 +155,21 @@ function observeConsoleLogging() {
return;
}
// As of WordPress 5.3.2 in Chrome 79, navigating to the block editor
// (Posts > Add New) will display a console warning about
// non - unique IDs.
// See: https://core.trac.wordpress.org/ticket/23165
if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) {
return;
}
const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ];
// Limit warnings on missing resources.
let previouslyLogged = false;
Object.keys( loggedMessages ).forEach( function( key ) {
if ( text.includes( loggedMessages[ key ].text ) ) {
if ( loggedMessages[ key ].logged ) {
previouslyLogged = true;
}
loggedMessages[ key ].logged = true;
}
} );
if ( previouslyLogged ) {
return;
}
// As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of
// type JSHandle for error logging, instead of the expected string.
//

View File

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

View File

@ -1,5 +1,7 @@
# Unreleased
# 0.1.2
## Fixed
- Missing `config` package dependency

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-utils",
"version": "0.1.1",
"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",
"repository": {

View File

@ -6,7 +6,7 @@
* Internal dependencies
*/
import { merchant } from './flows';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset, selectOptionInSelect2 } from './page-utils';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick, selectOptionInSelect2 } from './page-utils';
import factories from './factories';
const config = require( 'config' );
@ -25,6 +25,20 @@ const verifyAndPublish = async () => {
await expect( page ).toMatchElement( '.updated.notice', { text: 'Product published.' } );
};
/**
* Wait for primary button to be enabled and click.
*
* @param waitForNetworkIdle - Wait for network idle after click
* @returns {Promise<void>}
*/
const waitAndClickPrimary = async ( waitForNetworkIdle = true ) => {
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await page.click( 'button.is-primary' );
if ( waitForNetworkIdle ) {
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
}
};
/**
* Complete onboarding wizard.
*/
@ -88,15 +102,7 @@ const completeOnboardingWizard = async () => {
await expect( page ).toFill( '.components-text-control__input', config.get( 'onboardingwizard.industry' ) );
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await Promise.all( [
// Click on "Continue" button to move to the next step
page.click( 'button.is-primary' ),
// Wait for "What type of products will be listed?" section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
await waitAndClickPrimary();
// Product types section
@ -110,15 +116,7 @@ const completeOnboardingWizard = async () => {
}
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await Promise.all( [
// Click on "Continue" button to move to the next step
page.click( 'button.is-primary' ),
// Wait for "Tell us about your business" section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
await waitAndClickPrimary();
// Business Details section
@ -136,48 +134,15 @@ const completeOnboardingWizard = async () => {
await page.waitForSelector( '.woocommerce-select-control__control' );
await expect( page ).toClick( '.woocommerce-select-control__option', { text: config.get( 'onboardingwizard.sellingelsewhere' ) } );
// Query for the extensions toggles
const extensionsToggles = await page.$$( '.components-form-toggle__input' );
expect( extensionsToggles ).toHaveLength( 4 );
// Disable download of the onboarding suggested extensions
for ( let i = 0; i < extensionsToggles.length; i++ ) {
await extensionsToggles[i].click();
}
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await waitAndClickPrimary( false );
await Promise.all( [
// Click on "Continue" button to move to the next step
page.click( 'button.is-primary' ),
// Wait for "Theme" section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Skip installing extensions
await evalAndClick( '.components-checkbox-control__input' );
await waitAndClickPrimary();
// Theme section
// Wait for "Continue with my active theme" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );
await Promise.all( [
// Click on "Continue with my active theme" button to move to the next step
page.click( 'button.is-primary' ),
// Wait for "Enhance your store with WooCommerce Services" section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Benefits section
// Wait for Benefits section to appear
await page.waitForSelector( '.woocommerce-profile-wizard__benefits' );
// Wait for "No thanks" button to become active
await page.waitForSelector( 'button.is-secondary:not(:disabled)' );
// Click on "No thanks" button to move to the next step
await page.click( 'button.is-secondary' );
await waitAndClickPrimary();
// End of onboarding wizard

View File

@ -131,6 +131,31 @@ const merchant = {
await page.waitForSelector( '#message' );
await expect( page ).toMatchElement( '#message', { text: 'Order updated.' } );
},
verifyOrder: async (orderId, productName, productPrice, quantity, orderTotal, ensureCustomerRegistered = false) => {
await merchant.goToOrder(orderId);
// Verify that the order page is indeed of the order that was placed
// Verify order number
await expect(page).toMatchElement('.woocommerce-order-data__heading', {text: 'Order #' + orderId + ' details'});
// Verify product name
await expect(page).toMatchElement('.wc-order-item-name', {text: productName});
// Verify product cost
await expect(page).toMatchElement('.woocommerce-Price-amount.amount', {text: productPrice});
// Verify product quantity
await expect(page).toMatchElement('.quantity', {text: quantity.toString()});
// Verify total order amount without shipping
await expect(page).toMatchElement('.line_cost', {text: orderTotal});
if ( ensureCustomerRegistered ) {
// Verify customer profile link is present to verify order was placed by a registered customer, not a guest
await expect( page ).toMatchElement( 'label[for="customer_user"] a[href*=user-edit]', { text: 'Profile' } );
}
},
};
module.exports = merchant;

View File

@ -73,4 +73,68 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
$this->assertEquals( 1248.96, $order->get_total() );
}
/**
* Test that coupon taxes are not affected by logged in admin user.
*/
public function test_apply_coupon_for_correct_location_taxes() {
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_tax_based_on', 'billing' );
update_option( 'woocommerce_calc_taxes', 'yes' );
$password = wp_generate_password( 8, false, false );
$admin_id = wp_insert_user(
array(
'user_login' => "test_admin$password",
'user_pass' => $password,
'user_email' => "admin$password@example.com",
'role' => 'administrator',
)
);
update_user_meta( $admin_id, 'billing_country', 'MV' ); // Different than customer's address and base location.
wp_set_current_user( $admin_id );
WC()->customer = null;
WC()->initialize_cart();
update_option( 'woocommerce_default_country', 'IN:AP' );
$tax_rate = array(
'tax_rate_country' => 'IN',
'tax_rate_state' => '',
'tax_rate' => '25.0000',
'tax_rate_name' => 'tax',
'tax_rate_order' => '1',
'tax_rate_class' => '',
);
WC_Tax::_insert_tax_rate( $tax_rate );
$product = WC_Helper_Product::create_simple_product();
$product->set_regular_price( 100 );
$product->save();
$order = wc_create_order();
$order->set_billing_country( 'IN' );
$order->add_product( $product, 1 );
$order->save();
$order->calculate_totals();
$this->assertEquals( 100, $order->get_total() );
$this->assertEquals( 80, $order->get_subtotal() );
$this->assertEquals( 20, $order->get_total_tax() );
$coupon = new WC_Coupon();
$coupon->set_code( '10off' );
$coupon->set_discount_type( 'percent' );
$coupon->set_amount( 10 );
$coupon->save();
$order->apply_coupon( '10off' );
$this->assertEquals( 8, $order->get_discount_total() );
$this->assertEquals( 90, $order->get_total() );
$this->assertEquals( 18, $order->get_total_tax() );
$this->assertEquals( 2, $order->get_discount_tax() );
}
}

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.0.0-dev
* Version: 5.1.0-dev
* Author: Automattic
* Author URI: https://woocommerce.com
* Text Domain: woocommerce