diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
new file mode 100644
index 00000000000..445317ac548
--- /dev/null
+++ b/.github/workflows/pr-build.yml
@@ -0,0 +1,21 @@
+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
diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml
new file mode 100644
index 00000000000..6563779641f
--- /dev/null
+++ b/.github/workflows/pr-unit-tests.yml
@@ -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
diff --git a/.travis.yml b/.travis.yml
index 3bac1469de0..e637ca63572 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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"
diff --git a/assets/css/_variables.scss b/assets/css/_variables.scss
index e2c3d1b29b1..494001ffd85 100644
--- a/assets/css/_variables.scss
+++ b/assets/css/_variables.scss
@@ -8,7 +8,7 @@ $red: #a00 !default;
$orange: #ffba00 !default;
$blue: #2ea2cc !default;
-$primary: #a46497 !default; // Primary color for buttons (alt)
+$primary: #a46497 !default; // Primary color for buttons (alt)
$primarytext: desaturate(lighten($primary, 50%), 18%) !default; // Text on primary color bg
$secondary: desaturate(lighten($primary, 40%), 21%) !default; // Secondary buttons
@@ -17,5 +17,22 @@ $secondarytext: desaturate(darken($secondary, 60%), 21%) !default; // Text
$highlight: adjust-hue($primary, 150deg) !default; // Prices, In stock labels, sales flash
$highlightext: desaturate(lighten($highlight, 50%), 18%) !default; // Text on highlight color bg
-$contentbg: #fff !default; // Content BG - Tabs (active state)
-$subtext: #767676 !default; // small, breadcrumbs etc
+$contentbg: #fff !default; // Content BG - Tabs (active state)
+$subtext: #767676 !default; // small, breadcrumbs etc
+
+// export vars as CSS vars
+:root {
+ --woocommerce: $woocommerce;
+ --wc-green: $green;
+ --wc-red: $red;
+ --wc-orange: $orange;
+ --wc-blue: $blue;
+ --wc-primary: $primary;
+ --wc-primary-text: $primarytext;
+ --wc-secondary: $secondary;
+ --wc-secondary-text: $secondarytext;
+ --wc-highlight: $highlight;
+ --wc-highligh-text: $highlightext;
+ --wc-content-bg: $contentbg;
+ --wc-subtext: $subtext;
+}
diff --git a/assets/css/twenty-twenty-one.scss b/assets/css/twenty-twenty-one.scss
index c8fbba84047..ad542d5e7fd 100644
--- a/assets/css/twenty-twenty-one.scss
+++ b/assets/css/twenty-twenty-one.scss
@@ -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 {
diff --git a/bin/composer/phpcs/composer.lock b/bin/composer/phpcs/composer.lock
index 363928fcb9a..5a70d30ff7e 100644
--- a/bin/composer/phpcs/composer.lock
+++ b/bin/composer/phpcs/composer.lock
@@ -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"
}
diff --git a/bin/composer/phpunit/composer.lock b/bin/composer/phpunit/composer.lock
index 0f86a7f43fe..fe06e30dd0d 100644
--- a/bin/composer/phpunit/composer.lock
+++ b/bin/composer/phpunit/composer.lock
@@ -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"
}
diff --git a/bin/composer/wp/composer.lock b/bin/composer/wp/composer.lock
index ed08b92c203..24fdbbf28cb 100644
--- a/bin/composer/wp/composer.lock
+++ b/bin/composer/wp/composer.lock
@@ -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"
}
diff --git a/changelog.txt b/changelog.txt
index 44e4695e3dd..5bb4adee9e1 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -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
diff --git a/composer.json b/composer.json
index 130063f3882..65792a01784 100644
--- a/composer.json
+++ b/composer.json
@@ -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-rc.3",
"woocommerce/woocommerce-blocks": "4.0.0"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 254877de3a7..1a0ffd4d455 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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"
}
diff --git a/i18n/states.php b/i18n/states.php
index cda1e51e4aa..6065e0771f8 100644
--- a/i18n/states.php
+++ b/i18n/states.php
@@ -306,6 +306,40 @@ return array(
'CZ' => array(),
'DE' => array(),
'DK' => array(),
+ 'DO' => array( // Dominican Republic.
+ 'DO-01' => __( 'Distrito Nacional', 'woocommerce' ),
+ 'DO-02' => __( 'Azua', 'woocommerce' ),
+ 'DO-03' => __( 'Baoruco', 'woocommerce' ),
+ 'DO-04' => __( 'Barahona', 'woocommerce' ),
+ 'DO-05' => __( 'Dajabón', 'woocommerce' ),
+ 'DO-06' => __( 'Duarte', 'woocommerce' ),
+ 'DO-07' => __( 'Elías Piña', 'woocommerce' ),
+ 'DO-08' => __( 'El Seibo', 'woocommerce' ),
+ 'DO-09' => __( 'Espaillat', 'woocommerce' ),
+ 'DO-10' => __( 'Independencia', 'woocommerce' ),
+ 'DO-11' => __( 'La Altagracia', 'woocommerce' ),
+ 'DO-12' => __( 'La Romana', 'woocommerce' ),
+ 'DO-13' => __( 'La Vega', 'woocommerce' ),
+ 'DO-14' => __( 'María Trinidad Sánchez', 'woocommerce' ),
+ 'DO-15' => __( 'Monte Cristi', 'woocommerce' ),
+ 'DO-16' => __( 'Pedernales', 'woocommerce' ),
+ 'DO-17' => __( 'Peravia', 'woocommerce' ),
+ 'DO-18' => __( 'Puerto Plata', 'woocommerce' ),
+ 'DO-19' => __( 'Hermanas Mirabal', 'woocommerce' ),
+ 'DO-20' => __( 'Samaná', 'woocommerce' ),
+ 'DO-21' => __( 'San Cristóbal', 'woocommerce' ),
+ 'DO-22' => __( 'San Juan', 'woocommerce' ),
+ 'DO-23' => __( 'San Pedro de Macorís', 'woocommerce' ),
+ 'DO-24' => __( 'Sánchez Ramírez', 'woocommerce' ),
+ 'DO-25' => __( 'Santiago', 'woocommerce' ),
+ 'DO-26' => __( 'Santiago Rodríguez', 'woocommerce' ),
+ 'DO-27' => __( 'Valverde', 'woocommerce' ),
+ 'DO-28' => __( 'Monseñor Nouel', 'woocommerce' ),
+ 'DO-29' => __( 'Monte Plata', 'woocommerce' ),
+ 'DO-30' => __( 'Hato Mayor', 'woocommerce' ),
+ 'DO-31' => __( 'San José de Ocoa', 'woocommerce' ),
+ 'DO-32' => __( 'Santo Domingo', 'woocommerce' ),
+ ),
'DZ' => array(
'DZ-01' => __( 'Adrar', 'woocommerce' ),
'DZ-02' => __( 'Chlef', 'woocommerce' ),
@@ -442,6 +476,7 @@ return array(
),
'FI' => array(),
'FR' => array(),
+ 'GF' => array(),
'GH' => array( // Ghanaian Regions.
'AF' => __( 'Ahafo', 'woocommerce' ),
'AH' => __( 'Ashanti', 'woocommerce' ),
@@ -477,7 +512,30 @@ return array(
'L' => __( 'South Aegean', 'woocommerce' ),
'M' => __( 'Crete', 'woocommerce' ),
),
- 'GF' => array(),
+ 'GT' => array( // Guatemalan states.
+ 'AV' => __( 'Alta Verapaz', 'woocommerce' ),
+ 'BV' => __( 'Baja Verapaz', 'woocommerce' ),
+ 'CM' => __( 'Chimaltenango', 'woocommerce' ),
+ 'CQ' => __( 'Chiquimula', 'woocommerce' ),
+ 'PR' => __( 'El Progreso', 'woocommerce' ),
+ 'ES' => __( 'Escuintla', 'woocommerce' ),
+ 'GU' => __( 'Guatemala', 'woocommerce' ),
+ 'HU' => __( 'Huehuetenango', 'woocommerce' ),
+ 'IZ' => __( 'Izabal', 'woocommerce' ),
+ 'JA' => __( 'Jalapa', 'woocommerce' ),
+ 'JU' => __( 'Jutiapa', 'woocommerce' ),
+ 'PE' => __( 'Petén', 'woocommerce' ),
+ 'QZ' => __( 'Quetzaltenango', 'woocommerce' ),
+ 'QC' => __( 'Quiché', 'woocommerce' ),
+ 'RE' => __( 'Retalhuleu', 'woocommerce' ),
+ 'SA' => __( 'Sacatepéquez', 'woocommerce' ),
+ 'SM' => __( 'San Marcos', 'woocommerce' ),
+ 'SR' => __( 'Santa Rosa', 'woocommerce' ),
+ 'SO' => __( 'Sololá', 'woocommerce' ),
+ 'SU' => __( 'Suchitepéquez', 'woocommerce' ),
+ 'TO' => __( 'Totonicapán', 'woocommerce' ),
+ 'ZA' => __( 'Zacapa', 'woocommerce' )
+ ),
'HK' => array( // Hong Kong states.
'HONG KONG' => __( 'Hong Kong Island', 'woocommerce' ),
'KOWLOON' => __( 'Kowloon', 'woocommerce' ),
@@ -1304,7 +1362,6 @@ return array(
'VS' => __( 'Vaslui', 'woocommerce' ),
'VN' => __( 'Vrancea', 'woocommerce' ),
),
- 'RS' => array(),
'SG' => array(),
'SK' => array(),
'SI' => array(),
diff --git a/includes/admin/class-wc-admin-notices.php b/includes/admin/class-wc-admin-notices.php
index 70c76ee6120..fa9716cc498 100644
--- a/includes/admin/class-wc-admin-notices.php
+++ b/includes/admin/class-wc-admin-notices.php
@@ -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
*/
diff --git a/includes/admin/class-wc-admin-status.php b/includes/admin/class-wc-admin-status.php
index 1742531c8c3..0092b8b4383 100644
--- a/includes/admin/class-wc-admin-status.php
+++ b/includes/admin/class-wc-admin-status.php
@@ -377,7 +377,7 @@ class WC_Admin_Status {
private static function output_plugins_info( $plugins, $untested_plugins ) {
$wc_version = Constants::get_constant( 'WC_VERSION' );
- if ( 'major' === WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE ) {
+ if ( 'major' === Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
// Since we're only testing against major, we don't need to show minor and patch version.
$wc_version = $wc_version[0] . '.0';
}
diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php b/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php
index 6e68d641740..092493b4a29 100644
--- a/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php
+++ b/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php
@@ -118,7 +118,9 @@ class WC_Meta_Box_Order_Actions {
WC()->payment_gateways();
WC()->shipping();
- WC()->mailer()->emails['WC_Email_New_Order']->trigger( $order->get_id(), $order );
+ 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' );
diff --git a/includes/admin/notes/class-wc-notes-run-db-update.php b/includes/admin/notes/class-wc-notes-run-db-update.php
index c723d231d06..1fad100ff9e 100644
--- a/includes/admin/notes/class-wc-notes-run-db-update.php
+++ b/includes/admin/notes/class-wc-notes-run-db-update.php
@@ -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' )
)
);
diff --git a/includes/admin/plugin-updates/class-wc-plugin-updates.php b/includes/admin/plugin-updates/class-wc-plugin-updates.php
index 50ca0fc799d..bb8c40af460 100644
--- a/includes/admin/plugin-updates/class-wc-plugin-updates.php
+++ b/includes/admin/plugin-updates/class-wc-plugin-updates.php
@@ -150,10 +150,15 @@ class WC_Plugin_Updates {
* with the $new_version.
*
* @param string $new_version WooCommerce version to test against.
- * @param string $release 'major' or 'minor'.
+ * @param string $release 'major', 'minor', or 'none'.
* @return array of plugin info arrays
*/
public function get_untested_plugins( $new_version, $release ) {
+ // Since 5.0 all versions are backwards compatible.
+ if ( 'none' === $release ) {
+ return array();
+ }
+
$extensions = array_merge( $this->get_plugins_with_header( self::VERSION_TESTED_HEADER ), $this->get_plugins_for_woocommerce() );
$untested = array();
$new_version_parts = explode( '.', $new_version );
diff --git a/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php b/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php
index ad24e66848e..5920fb52616 100644
--- a/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php
+++ b/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php
@@ -42,9 +42,14 @@ class WC_Plugins_Screen_Updates extends WC_Plugin_Updates {
* @param stdClass $response Plugin update response.
*/
public function in_plugin_update_message( $args, $response ) {
+ $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
+ if ( ! is_string( $version_type ) ) {
+ $version_type = 'none';
+ }
+
$this->new_version = $response->new_version;
$this->upgrade_notice = $this->get_upgrade_notice( $response->new_version );
- $this->major_untested_plugins = $this->get_untested_plugins( $response->new_version, 'major' );
+ $this->major_untested_plugins = $this->get_untested_plugins( $response->new_version, $version_type );
$current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) );
$new_version_parts = explode( '.', $this->new_version );
diff --git a/includes/admin/plugin-updates/views/html-notice-untested-extensions-modal.php b/includes/admin/plugin-updates/views/html-notice-untested-extensions-modal.php
index 23c1fe21a19..bb1c58468b1 100644
--- a/includes/admin/plugin-updates/views/html-notice-untested-extensions-modal.php
+++ b/includes/admin/plugin-updates/views/html-notice-untested-extensions-modal.php
@@ -18,7 +18,7 @@ $untested_plugins_msg = sprintf(
?>
-
+
@@ -41,7 +41,7 @@ $untested_plugins_msg = sprintf(
-
+
diff --git a/includes/admin/settings/class-wc-settings-emails.php b/includes/admin/settings/class-wc-settings-emails.php
index 9b4c524ebc5..a3da4c3f514 100644
--- a/includes/admin/settings/class-wc-settings-emails.php
+++ b/includes/admin/settings/class-wc-settings-emails.php
@@ -191,6 +191,22 @@ class WC_Settings_Emails extends WC_Settings_Page {
'id' => 'email_template_options',
),
+ array(
+ 'title' => __( 'Store management insights', 'woocommerce' ),
+ 'type' => 'title',
+ 'id' => 'email_merchant_notes',
+ ),
+
+ array(
+ 'title' => __( 'Enable email insights', 'woocommerce' ),
+ 'desc' => __( 'Receive email notifications with additional guidance to complete the basic store setup and helpful insights', 'woocommerce' ),
+ 'id' => 'woocommerce_merchant_email_notifications',
+ 'type' => 'checkbox',
+ 'checkboxgroup' => 'start',
+ 'default' => 'yes',
+ 'autoload' => false,
+ ),
+
)
);
diff --git a/includes/admin/views/html-admin-page-reports.php b/includes/admin/views/html-admin-page-reports.php
index a5ef713ee1a..8eb38322a06 100644
--- a/includes/admin/views/html-admin-page-reports.php
+++ b/includes/admin/views/html-admin-page-reports.php
@@ -9,6 +9,16 @@ if ( ! defined( 'ABSPATH' ) ) {
?>
+
+
+
+ WooCommerce Analytics or learn more about the new experience in the WooCommerce Analytics documentation .', 'woocommerce' ), esc_url( wc_admin_url( '&path=/analytics/overview' ) ) ) );
+ ?>
+
+
+
$report_group ) {
diff --git a/includes/admin/views/html-admin-page-status-report.php b/includes/admin/views/html-admin-page-status-report.php
index 5b3028a11c5..fa7b1cfbe81 100644
--- a/includes/admin/views/html-admin-page-status-report.php
+++ b/includes/admin/views/html-admin-page-status-report.php
@@ -11,7 +11,8 @@ global $wpdb;
if ( ! defined( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
// Define if we're checking against major or minor versions.
- define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'major' );
+ // Since 5.0 all versions are backwards compatible, so there's no more check.
+ define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' );
}
$report = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
@@ -844,10 +845,11 @@ if ( 0 < count( $dropins_mu_plugins['mu_plugins'] ) ) :
echo ' ' . wp_kses_post( sprintf( __( 'Page visibility should be public ', 'woocommerce' ), 'https://wordpress.org/support/article/content-visibility/' ) ) . ' ';
$found_error = true;
} else {
- // Shortcode check.
- if ( $_page['shortcode_required'] ) {
- if ( ! $_page['shortcode_present'] ) {
- echo ' ' . sprintf( esc_html__( 'Page does not contain the shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) . ' ';
+ // Shortcode and block check.
+ if ( $_page['shortcode_required'] || $_page['block_required'] ) {
+ if ( ! $_page['shortcode_present'] && ! $_page['block_present'] ) {
+ /* Translators: %1$s: shortcode text, %2$s: block slug. */
+ echo ' ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . ' '; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
}
diff --git a/includes/admin/views/html-report-by-date.php b/includes/admin/views/html-report-by-date.php
index 860226a9ccc..12a1ca6d416 100644
--- a/includes/admin/views/html-report-by-date.php
+++ b/includes/admin/views/html-report-by-date.php
@@ -8,9 +8,7 @@
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
-
?>
-
diff --git a/includes/blocks/class-wc-blocks-utils.php b/includes/blocks/class-wc-blocks-utils.php
new file mode 100644
index 00000000000..4ecb4c212e5
--- /dev/null
+++ b/includes/blocks/class-wc-blocks-utils.php
@@ -0,0 +1,84 @@
+post_content );
+ if ( ! $blocks ) {
+ return array();
+ }
+
+ return $blocks;
+ }
+
+ /**
+ * Get all instances of the specified block on a specific woo page
+ * (e.g. `cart` or `checkout` page).
+ *
+ * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`.
+ * @param string $woo_page_name The woo page to search, e.g. `cart`.
+ * @return array Array of blocks as returned by parse_blocks().
+ */
+ public static function get_blocks_from_page( $block_name, $woo_page_name ) {
+ $page_blocks = self::get_all_blocks_from_page( $woo_page_name );
+
+ // Get any instances of the specified block.
+ return array_values(
+ array_filter(
+ $page_blocks,
+ function ( $block ) use ( $block_name ) {
+ return ( $block_name === $block['blockName'] );
+ }
+ )
+ );
+ }
+
+ /**
+ * Check if a given page contains a particular block.
+ *
+ * @param int|WP_Post $page Page post ID or post object.
+ * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`.
+ * @return bool Boolean value if the page contains the block or not. Null in case the page does not exist.
+ */
+ public static function has_block_in_page( $page, $block_name ) {
+ $page_to_check = get_post( $page );
+ if ( null === $page_to_check ) {
+ return false;
+ }
+
+ $blocks = parse_blocks( $page_to_check->post_content );
+ foreach ( $blocks as $block ) {
+ if ( $block_name === $block['blockName'] ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php
index 84a04b2b80f..facee43a729 100644
--- a/includes/class-wc-cart-totals.php
+++ b/includes/class-wc-cart-totals.php
@@ -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.
diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php
index d84da807701..b5683f69bc1 100644
--- a/includes/class-wc-cart.php
+++ b/includes/class-wc-cart.php
@@ -210,7 +210,7 @@ class WC_Cart extends WC_Legacy_Cart {
}
/**
- * Get subtotal.
+ * Get subtotal_tax.
*
* @since 3.2.0
* @return float
diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php
index 3199d6773ba..a36be39d9c1 100644
--- a/includes/class-wc-comments.php
+++ b/includes/class-wc-comments.php
@@ -340,6 +340,49 @@ class WC_Comments {
return $average;
}
+ /**
+ * Utility function for getting review counts for multiple products in one query. This is not cached.
+ *
+ * @since 5.0.0
+ *
+ * @param array $product_ids Array of product IDs.
+ *
+ * @return array
+ */
+ public static function get_review_counts_for_product_ids( $product_ids ) {
+ global $wpdb;
+
+ if ( empty( $product_ids ) ) {
+ return array();
+ }
+
+ $product_id_string_placeholder = substr( str_repeat( ',%s', count( $product_ids ) ), 1 );
+
+ $review_counts = $wpdb->get_results(
+ // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in IN query.
+ $wpdb->prepare(
+ "
+ SELECT comment_post_ID as product_id, COUNT( comment_post_ID ) as review_count
+ FROM $wpdb->comments
+ WHERE
+ comment_parent = 0
+ AND comment_post_ID IN ( $product_id_string_placeholder )
+ AND comment_approved = '1'
+ AND comment_type in ( 'review', '', 'comment' )
+ GROUP BY product_id
+ ",
+ $product_ids
+ ),
+ // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared.
+ ARRAY_A
+ );
+
+ // Convert to key value pairs.
+ $counts = array_replace( array_fill_keys( $product_ids, 0 ), array_column( $review_counts, 'review_count', 'product_id' ) );
+
+ return $counts;
+ }
+
/**
* Get product review count for a product (not replies). Please note this is not cached.
*
@@ -348,22 +391,9 @@ class WC_Comments {
* @return int
*/
public static function get_review_count_for_product( &$product ) {
- global $wpdb;
+ $counts = self::get_review_counts_for_product_ids( array( $product->get_id() ) );
- $count = $wpdb->get_var(
- $wpdb->prepare(
- "
- SELECT COUNT(*) FROM $wpdb->comments
- WHERE comment_parent = 0
- AND comment_post_ID = %d
- AND comment_approved = '1'
- AND comment_type in ( 'review', '', 'comment' )
- ",
- $product->get_id()
- )
- );
-
- return $count;
+ return $counts[ $product->get_id() ];
}
/**
diff --git a/includes/class-wc-emails.php b/includes/class-wc-emails.php
index 338c8caff18..e4aeb5c185d 100644
--- a/includes/class-wc-emails.php
+++ b/includes/class-wc-emails.php
@@ -190,7 +190,7 @@ class WC_Emails {
$this->init();
// Email Header, Footer and content hooks.
- add_action( 'woocommerce_email_header', array( $this, 'email_header' ), 10, 2 );
+ add_action( 'woocommerce_email_header', array( $this, 'email_header' ) );
add_action( 'woocommerce_email_footer', array( $this, 'email_footer' ) );
add_action( 'woocommerce_email_order_details', array( $this, 'order_downloads' ), 10, 4 );
add_action( 'woocommerce_email_order_details', array( $this, 'order_details' ), 10, 4 );
@@ -263,26 +263,17 @@ class WC_Emails {
/**
* Get the email header.
*
- * @param mixed $email_heading Heading for the email.
- * @param WC_Email $email Email object for the email.
+ * @param mixed $email_heading Heading for the email.
*/
- public function email_header( $email_heading, $email ) {
- wc_get_template(
- 'emails/email-header.php',
- array(
- 'email_heading' => $email_heading,
- 'email' => $email,
- )
- );
+ public function email_header( $email_heading ) {
+ wc_get_template( 'emails/email-header.php', array( 'email_heading' => $email_heading ) );
}
/**
* Get the email footer.
- *
- * @param WC_Email $email Email object for the email.
*/
- public function email_footer( $email ) {
- wc_get_template( 'emails/email-footer.php', array( 'email' => $email ) );
+ public function email_footer() {
+ wc_get_template( 'emails/email-footer.php' );
}
/**
diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php
index f79bfdafda7..c4eddfa86c2 100644
--- a/includes/class-wc-install.php
+++ b/includes/class-wc-install.php
@@ -157,6 +157,10 @@ class WC_Install {
'wc_update_450_sanitize_coupons_code',
'wc_update_450_db_version',
),
+ '5.0.0' => array(
+ 'wc_update_500_fix_product_review_count',
+ 'wc_update_500_db_version',
+ ),
);
/**
diff --git a/includes/class-wc-payment-tokens.php b/includes/class-wc-payment-tokens.php
index 85625d02acc..2185895f0df 100644
--- a/includes/class-wc-payment-tokens.php
+++ b/includes/class-wc-payment-tokens.php
@@ -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.
diff --git a/includes/class-wc-post-data.php b/includes/class-wc-post-data.php
index 5d91fd6edcc..5ea84ff351f 100644
--- a/includes/class-wc-post-data.php
+++ b/includes/class-wc-post-data.php
@@ -132,7 +132,7 @@ class WC_Post_Data {
/**
* Filter to prevent variations from being deleted while switching from a variable product type to a variable product type.
*
- * @since 4.9.0
+ * @since 5.0.0
*
* @param bool A boolean value of true will delete the variations.
* @param WC_Product $product Product data.
diff --git a/includes/class-wc-tracker.php b/includes/class-wc-tracker.php
index 27a36961c7f..1576cc1b23d 100644
--- a/includes/class-wc-tracker.php
+++ b/includes/class-wc-tracker.php
@@ -352,30 +352,105 @@ class WC_Tracker {
}
/**
- * Get order counts
- *
- * @return array
- */
- private static function get_order_counts() {
- $order_count = array();
- $order_count_data = wp_count_posts( 'shop_order' );
- foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
- $order_count[ $status_slug ] = $order_count_data->{ $status_slug };
- }
- return $order_count;
- }
-
- /**
- * Combine all order data.
+ * Get all order data.
*
* @return array
*/
private static function get_orders() {
- $order_dates = self::get_order_dates();
- $order_counts = self::get_order_counts();
- $order_totals = self::get_order_totals();
+ $args = array(
+ 'type' => array( 'shop_order', 'shop_order_refund' ),
+ 'limit' => get_option( 'posts_per_page' ),
+ 'paged' => 1,
+ );
- return array_merge( $order_dates, $order_counts, $order_totals );
+ $first = time();
+ $last = 0;
+ $processing_first = time();
+ $processing_last = 0;
+
+ $orders = wc_get_orders( $args );
+ $orders_count = count( $orders );
+
+ while ( $orders_count ) {
+
+ foreach ( $orders as $order ) {
+
+ $date_created = (int) $order->get_date_created()->getTimestamp();
+ $type = $order->get_type();
+ $status = $order->get_status();
+
+ if ( 'shop_order' == $type ) {
+
+ // Find the first and last order dates for completed and processing statuses.
+ if ( 'completed' == $status && $date_created < $first ) {
+ $first = $date_created;
+ }
+ if ( 'completed' == $status && $date_created > $last ) {
+ $last = $date_created;
+ }
+ if ( 'processing' == $status && $date_created < $processing_first ) {
+ $processing_first = $date_created;
+ }
+ if ( 'processing' == $status && $date_created > $processing_last ) {
+ $processing_last = $date_created;
+ }
+
+ // Get order counts by status.
+ $status = 'wc-' . $status;
+
+ if ( ! isset( $order_data[ $status ] ) ) {
+ $order_data[ $status ] = 1;
+ } else {
+ $order_data[ $status ] += 1;
+ }
+
+ // Count number of orders by gateway used.
+ $gateway = $order->get_payment_method();
+
+ if ( ! empty( $gateway ) && in_array( $status, array( 'wc-completed', 'wc-refunded', 'wc-processing' ) ) ) {
+ $gateway = 'gateway_' . $gateway;
+
+ if ( ! isset( $order_data[ $gateway ] ) ) {
+ $order_data[ $gateway ] = 1;
+ } else {
+ $order_data[ $gateway ] += 1;
+ }
+ }
+ } else {
+ // If it is a refunded order (shop_order_refunnd type), add the prefix as this prefix gets
+ // added midway in the if clause.
+ $status = 'wc-' . $status;
+ }
+
+ // Calculate the gross total for 'completed' and 'processing' orders.
+ $total = $order->get_total();
+
+ if ( in_array( $status, array( 'wc-completed', 'wc-refunded' ) ) ) {
+ if ( ! isset( $order_data['gross'] ) ) {
+ $order_data['gross'] = $total;
+ } else {
+ $order_data['gross'] += $total;
+ }
+ } elseif ( 'wc-processing' == $status ) {
+ if ( ! isset( $order_data['processing_gross'] ) ) {
+ $order_data['processing_gross'] = $total;
+ } else {
+ $order_data['processing_gross'] += $total;
+ }
+ }
+ }
+ $args['paged']++;
+
+ $orders = wc_get_orders( $args );
+ $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 );
+
+ return $order_data;
}
/**
@@ -543,94 +618,12 @@ class WC_Tracker {
/**
* Get order totals
*
+ * @deprecated 5.1.0 Logic moved to get_orders.
* @return array
*/
public static function get_order_totals() {
- global $wpdb;
-
- $gross_total = $wpdb->get_var(
- "
- SELECT
- SUM( order_meta.meta_value ) AS 'gross_total'
- FROM {$wpdb->prefix}posts AS orders
- LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
- WHERE order_meta.meta_key = '_order_total'
- AND orders.post_status in ( 'wc-completed', 'wc-refunded' )
- GROUP BY order_meta.meta_key
- "
- );
-
- if ( is_null( $gross_total ) ) {
- $gross_total = 0;
- }
-
- $processing_gross_total = $wpdb->get_var(
- "
- SELECT
- SUM( order_meta.meta_value ) AS 'gross_total'
- FROM {$wpdb->prefix}posts AS orders
- LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID
- WHERE order_meta.meta_key = '_order_total'
- AND orders.post_status = 'wc-processing'
- GROUP BY order_meta.meta_key
- "
- );
-
- if ( is_null( $processing_gross_total ) ) {
- $processing_gross_total = 0;
- }
-
- return array(
- 'gross' => $gross_total,
- 'processing_gross' => $processing_gross_total,
- );
- }
-
- /**
- * Get last order date
- *
- * @return string
- */
- private static function get_order_dates() {
- global $wpdb;
-
- $min_max = $wpdb->get_row(
- "
- SELECT
- MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last'
- FROM {$wpdb->prefix}posts
- WHERE post_type = 'shop_order'
- AND post_status = 'wc-completed'
- ",
- ARRAY_A
- );
-
- if ( is_null( $min_max ) ) {
- $min_max = array(
- 'first' => '-',
- 'last' => '-',
- );
- }
-
- $processing_min_max = $wpdb->get_row(
- "
- SELECT
- MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last'
- FROM {$wpdb->prefix}posts
- WHERE post_type = 'shop_order'
- AND post_status = 'wc-processing'
- ",
- ARRAY_A
- );
-
- if ( is_null( $processing_min_max ) ) {
- $processing_min_max = array(
- 'processing_first' => '-',
- 'processing_last' => '-',
- );
- }
-
- return array_merge( $min_max, $processing_min_max );
+ wc_deprecated_function( 'WC_Tracker::get_order_totals', '5.1.0', '' );
+ return self::get_orders();
}
/**
@@ -660,49 +653,6 @@ class WC_Tracker {
return ( '0' !== $result ) ? 'Yes' : 'No';
}
- /**
- * Get blocks from a woocommerce page.
- *
- * @param string $woo_page_name A woocommerce page e.g. `checkout` or `cart`.
- * @return array Array of blocks as returned by parse_blocks().
- */
- private static function get_all_blocks_from_page( $woo_page_name ) {
- $page_id = wc_get_page_id( $woo_page_name );
-
- $page = get_post( $page_id );
- if ( ! $page ) {
- return array();
- }
-
- $blocks = parse_blocks( $page->post_content );
- if ( ! $blocks ) {
- return array();
- }
-
- return $blocks;
- }
-
- /**
- * Get all instances of the specified block on a specific woo page
- * (e.g. `cart` or `checkout` page).
- *
- * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`.
- * @param string $woo_page_name The woo page to search, e.g. `cart`.
- * @return array Array of blocks as returned by parse_blocks().
- */
- private static function get_blocks_from_page( $block_name, $woo_page_name ) {
- $page_blocks = self::get_all_blocks_from_page( $woo_page_name );
-
- // Get any instances of the specified block.
- return array_values(
- array_filter(
- $page_blocks,
- function ( $block ) use ( $block_name ) {
- return ( $block_name === $block['blockName'] );
- }
- )
- );
- }
/**
* Get tracker data for a specific block type on a woocommerce page.
@@ -714,7 +664,7 @@ class WC_Tracker {
* - block_attributes
*/
public static function get_block_tracker_data( $block_name, $woo_page_name ) {
- $blocks = self::get_blocks_from_page( $block_name, $woo_page_name );
+ $blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name );
$block_present = false;
$attributes = array();
diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php
index d7ecad18160..ec163d8efcf 100644
--- a/includes/class-woocommerce.php
+++ b/includes/class-woocommerce.php
@@ -8,6 +8,7 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
@@ -22,7 +23,7 @@ final class WooCommerce {
*
* @var string
*/
- public $version = '5.0.0';
+ public $version = '5.1.0';
/**
* WooCommerce Schema version.
@@ -202,6 +203,10 @@ 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 );
}
/**
@@ -421,6 +426,7 @@ final class WooCommerce {
include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php';
include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php';
include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php';
+ include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php';
/**
* Data stores - used to store and retrieve CRUD object data from the database.
@@ -901,7 +907,7 @@ final class WooCommerce {
'https://wordpress.org/plugins/woocommerce/',
'https://github.com/woocommerce/woocommerce/releases'
);
- printf( '
', $message_one, $message_two ); /* WPCS: xss ok. */
+ printf( '
', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
diff --git a/includes/data-stores/class-wc-customer-download-data-store.php b/includes/data-stores/class-wc-customer-download-data-store.php
index 379e5fa1d28..f715fc6d3cb 100644
--- a/includes/data-stores/class-wc-customer-download-data-store.php
+++ b/includes/data-stores/class-wc-customer-download-data-store.php
@@ -16,6 +16,38 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store_Interface {
+ /**
+ * Names of the database fields for the download permissions table.
+ */
+ const DOWNLOAD_PERMISSION_DB_FIELDS = array(
+ 'download_id',
+ 'product_id',
+ 'user_id',
+ 'user_email',
+ 'order_id',
+ 'order_key',
+ 'downloads_remaining',
+ 'access_granted',
+ 'download_count',
+ 'access_expires',
+ );
+
+ /**
+ * Create download permission for a user, from an array of data.
+ *
+ * @param array $data Data to create the permission for.
+ * @returns int The database id of the created permission, or false if the permission creation failed.
+ */
+ public function create_from_data( $data ) {
+ $data = array_intersect_key( $data, array_flip( self::DOWNLOAD_PERMISSION_DB_FIELDS ) );
+
+ $id = $this->insert_new_download_permission( $data );
+
+ do_action( 'woocommerce_grant_product_download_access', $data );
+
+ return $id;
+ }
+
/**
* Create download permission for a user.
*
@@ -29,18 +61,41 @@ class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store
$download->set_access_granted( time() );
}
- $data = array(
- 'download_id' => $download->get_download_id( 'edit' ),
- 'product_id' => $download->get_product_id( 'edit' ),
- 'user_id' => $download->get_user_id( 'edit' ),
- 'user_email' => $download->get_user_email( 'edit' ),
- 'order_id' => $download->get_order_id( 'edit' ),
- 'order_key' => $download->get_order_key( 'edit' ),
- 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ),
- 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
- 'download_count' => $download->get_download_count( 'edit' ),
- 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
- );
+ $data = array();
+ foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) {
+ $value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' );
+ $data[ $db_field_name ] = $value;
+ }
+
+ $inserted_id = $this->insert_new_download_permission( $data );
+ if ( $inserted_id ) {
+ $download->set_id( $inserted_id );
+ $download->apply_changes();
+ }
+
+ do_action( 'woocommerce_grant_product_download_access', $data );
+ }
+
+ /**
+ * Create download permission for a user, from an array of data.
+ * Assumes that all the keys in the passed data are valid.
+ *
+ * @param array $data Data to create the permission for.
+ * @return int The database id of the created permission, or false if the permission creation failed.
+ */
+ private function insert_new_download_permission( $data ) {
+ global $wpdb;
+
+ // Always set a access granted date.
+ if ( ! isset( $data['access_granted'] ) ) {
+ $data['access_granted'] = time();
+ }
+
+ $data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] );
+
+ if ( isset( $data['access_expires'] ) ) {
+ $data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] );
+ }
$format = array(
'%s',
@@ -61,12 +116,29 @@ class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store
apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data )
);
- if ( $result ) {
- $download->set_id( $wpdb->insert_id );
- $download->apply_changes();
+ return $result ? $wpdb->insert_id : false;
+ }
+
+ /**
+ * Adjust a date value to be inserted in the database.
+ *
+ * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes.
+ * @return string The date converted to 'Y-m-d' format.
+ * @throws Exception The passed value can't be converted to a date.
+ */
+ private function adjust_date_for_db( $date ) {
+ if ( 'WC_DateTime' === get_class( $date ) ) {
+ $date = $date->getTimestamp();
}
- do_action( 'woocommerce_grant_product_download_access', $data );
+ $adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
+
+ if ( $adjusted_date ) {
+ return $adjusted_date;
+ }
+
+ $msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) );
+ throw new Exception( $msg );
}
/**
@@ -128,8 +200,10 @@ class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store
'order_id' => $download->get_order_id( 'edit' ),
'order_key' => $download->get_order_key( 'edit' ),
'downloads_remaining' => $download->get_downloads_remaining( 'edit' ),
+ // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ),
'download_count' => $download->get_download_count( 'edit' ),
+ // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null,
);
@@ -412,7 +486,7 @@ class WC_Customer_Download_Data_Store implements WC_Customer_Download_Data_Store
)
ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;",
$customer_id,
- date( 'Y-m-d', current_time( 'timestamp' ) )
+ date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
)
);
}
diff --git a/includes/data-stores/class-wc-order-data-store-cpt.php b/includes/data-stores/class-wc-order-data-store-cpt.php
index 6fff3cd51c4..0a80dddb540 100644
--- a/includes/data-stores/class-wc-order-data-store-cpt.php
+++ b/includes/data-stores/class-wc-order-data-store-cpt.php
@@ -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;
diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php
index 47a8fde4b7c..ed626f9a9b3 100644
--- a/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -6,6 +6,7 @@
*/
use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\Utilities\NumberUtil;
if ( ! defined( 'ABSPATH' ) ) {
@@ -265,6 +266,10 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$this->handle_updated_props( $product );
$this->clear_caches( $product );
+ wc_get_container()
+ ->get( DownloadPermissionsAdjuster::class )
+ ->maybe_schedule_adjust_download_permissions( $product );
+
$product->apply_changes();
do_action( 'woocommerce_update_product', $product->get_id(), $product );
diff --git a/includes/data-stores/class-wc-shipping-zone-data-store.php b/includes/data-stores/class-wc-shipping-zone-data-store.php
index 453ec877f15..fe9c54d2159 100644
--- a/includes/data-stores/class-wc-shipping-zone-data-store.php
+++ b/includes/data-stores/class-wc-shipping-zone-data-store.php
@@ -74,33 +74,41 @@ class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Shippin
public function read( &$zone ) {
global $wpdb;
- $zone_data = false;
-
- if ( 0 !== $zone->get_id() || '0' !== $zone->get_id() ) {
- $zone_data = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1",
- $zone->get_id()
- )
- );
- }
-
+ // Zone 0 is used as a default if no other zones fit.
if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) {
$this->read_zone_locations( $zone );
$zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) );
$zone->read_meta_data();
$zone->set_object_read( true );
+
+ /**
+ * Indicate that the WooCommerce shipping zone has been loaded.
+ *
+ * @param WC_Shipping_Zone $zone The shipping zone that has been loaded.
+ */
do_action( 'woocommerce_shipping_zone_loaded', $zone );
- } elseif ( $zone_data ) {
- $zone->set_zone_name( $zone_data->zone_name );
- $zone->set_zone_order( $zone_data->zone_order );
- $this->read_zone_locations( $zone );
- $zone->read_meta_data();
- $zone->set_object_read( true );
- do_action( 'woocommerce_shipping_zone_loaded', $zone );
- } else {
+ return;
+ }
+
+ $zone_data = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1",
+ $zone->get_id()
+ )
+ );
+
+ if ( ! $zone_data ) {
throw new Exception( __( 'Invalid data store.', 'woocommerce' ) );
}
+
+ $zone->set_zone_name( $zone_data->zone_name );
+ $zone->set_zone_order( $zone_data->zone_order );
+ $this->read_zone_locations( $zone );
+ $zone->read_meta_data();
+ $zone->set_object_read( true );
+
+ /** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */
+ do_action( 'woocommerce_shipping_zone_loaded', $zone );
}
/**
diff --git a/includes/emails/class-wc-email-new-order.php b/includes/emails/class-wc-email-new-order.php
index 0c063d61e76..9cc757495e3 100644
--- a/includes/emails/class-wc-email-new-order.php
+++ b/includes/emails/class-wc-email-new-order.php
@@ -92,10 +92,25 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) :
$this->object = $order;
$this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() );
$this->placeholders['{order_number}'] = $this->object->get_order_number();
+
+ $email_already_sent = $order->get_meta( '_new_order_email_sent' );
+ }
+
+ /**
+ * 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;
}
if ( $this->is_enabled() && $this->get_recipient() ) {
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
+
+ $order->update_meta_data( '_new_order_email_sent', 'true' );
+ $order->save();
}
$this->restore_locale();
diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
index e2d6569140b..a420be32a5f 100644
--- a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
+++ b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
@@ -1162,7 +1162,7 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
/**
* Returns a mini-report on WC pages and if they are configured correctly:
- * Present, visible, and including the correct shortcode.
+ * Present, visible, and including the correct shortcode or block.
*
* @return array
*/
@@ -1172,22 +1172,27 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
_x( 'Shop base', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_shop_page_id',
'shortcode' => '',
+ 'block' => '',
),
_x( 'Cart', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_cart_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']',
+ 'block' => 'woocommerce/cart',
),
_x( 'Checkout', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_checkout_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']',
+ 'block' => 'woocommerce/checkout',
),
_x( 'My account', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_myaccount_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']',
+ 'block' => '',
),
_x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_terms_page_id',
'shortcode' => '',
+ 'block' => '',
),
);
@@ -1199,6 +1204,8 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
$page_visible = false;
$shortcode_present = false;
$shortcode_required = false;
+ $block_present = false;
+ $block_required = false;
// Page checks.
if ( $page_id ) {
@@ -1220,6 +1227,12 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
}
}
+ // Block checks.
+ if ( $values['block'] && get_post( $page_id ) ) {
+ $block_required = true;
+ $block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] );
+ }
+
// Wrap up our findings into an output array.
$pages_output[] = array(
'page_name' => $page_name,
@@ -1228,8 +1241,11 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
'page_exists' => $page_exists,
'page_visible' => $page_visible,
'shortcode' => $values['shortcode'],
+ 'block' => $values['block'],
'shortcode_required' => $shortcode_required,
'shortcode_present' => $shortcode_present,
+ 'block_present' => $block_present,
+ 'block_required' => $block_required,
);
}
diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php
index 94331f2ecc5..916ecbcd6b8 100644
--- a/includes/wc-formatting-functions.php
+++ b/includes/wc-formatting-functions.php
@@ -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 );
}
/**
diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php
index 27c2ed9ff62..8e473271159 100644
--- a/includes/wc-product-functions.php
+++ b/includes/wc-product-functions.php
@@ -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.
diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php
index 3cb0ff50245..9f37d99641e 100644
--- a/includes/wc-template-functions.php
+++ b/includes/wc-template-functions.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' );
diff --git a/includes/wc-update-functions.php b/includes/wc-update-functions.php
index 2afa3e0e7bd..439095d5482 100644
--- a/includes/wc-update-functions.php
+++ b/includes/wc-update-functions.php
@@ -2219,3 +2219,67 @@ function wc_update_450_sanitize_coupons_code() {
delete_option( 'woocommerce_update_450_last_coupon_id' );
return false;
}
+
+/**
+ * Fixes product review count that might have been incorrect.
+ *
+ * See @link https://github.com/woocommerce/woocommerce/issues/27688.
+ */
+function wc_update_500_fix_product_review_count() {
+ global $wpdb;
+
+ $product_id = 0;
+ $last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' );
+
+ $products_data = $wpdb->get_results(
+ $wpdb->prepare(
+ "
+ SELECT post_id, meta_value
+ FROM $wpdb->postmeta
+ JOIN $wpdb->posts
+ ON $wpdb->postmeta.post_id = $wpdb->posts.ID
+ WHERE
+ post_type = 'product'
+ AND post_status = 'publish'
+ AND post_id > %d
+ AND meta_key = '_wc_review_count'
+ ORDER BY post_id ASC
+ LIMIT 10
+ ",
+ $last_product_id
+ ),
+ ARRAY_A
+ );
+
+ if ( empty( $products_data ) ) {
+ delete_option( 'woocommerce_update_500_last_product_id' );
+ return false;
+ }
+
+ $product_ids_to_check = array_column( $products_data, 'post_id' );
+ $actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check );
+
+ foreach ( $products_data as $product_data ) {
+ $product_id = intval( $product_data['post_id'] );
+ $current_review_count = intval( $product_data['meta_value'] );
+
+ if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) {
+ WC_Comments::clear_transients( $product_id );
+ }
+ }
+
+ // Start the run again.
+ if ( $product_id ) {
+ return update_option( 'woocommerce_update_500_last_product_id', $product_id );
+ }
+
+ delete_option( 'woocommerce_update_500_last_product_id' );
+ return false;
+}
+
+/**
+ * Update DB version to 5.0.0.
+ */
+function wc_update_500_db_version() {
+ WC_Install::update_db_version( '5.0.0' );
+}
diff --git a/package.json b/package.json
index c001a8ca0b3..e17367d37b8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "woocommerce",
"title": "WooCommerce",
- "version": "5.0.0",
+ "version": "5.1.0",
"homepage": "https://woocommerce.com/",
"repository": {
"type": "git",
diff --git a/readme.txt b/readme.txt
index 62267eeb801..1015d0eebc9 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
Requires at least: 5.3
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.
diff --git a/src/Container.php b/src/Container.php
index 4291867682b..517239afeb7 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -5,8 +5,9 @@
namespace Automattic\WooCommerce;
-use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
+use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
+use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
/**
* PSR11 compliant dependency injection container for WooCommerce.
@@ -33,6 +34,7 @@ final class Container implements \Psr\Container\ContainerInterface {
*/
private $service_providers = array(
ProxiesServiceProvider::class,
+ DownloadPermissionsAdjusterServiceProvider::class,
);
/**
diff --git a/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php
new file mode 100644
index 00000000000..126e689f783
--- /dev/null
+++ b/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php
@@ -0,0 +1,31 @@
+share( DownloadPermissionsAdjuster::class );
+ }
+}
diff --git a/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php
index b790397d35c..4e2823d5e05 100644
--- a/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php
+++ b/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php
@@ -1,6 +1,6 @@
downloads_data_store = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Data_Store::class, 'customer-download' );
+ add_action( 'adjust_download_permissions', array( $this, 'adjust_download_permissions' ), 10, 1 );
+ }
+
+ /**
+ * Schedule a download permissions adjustment for a product if necessary.
+ * This should be executed whenever a product is saved.
+ *
+ * @param \WC_Product $product The product to schedule a download permission adjustments for.
+ */
+ public function maybe_schedule_adjust_download_permissions( \WC_Product $product ) {
+ $children_ids = $product->get_children();
+ if ( ! $children_ids ) {
+ return;
+ }
+
+ $scheduled_action_args = array( $product->get_id() );
+
+ $already_scheduled_actions =
+ WC()->call_function(
+ 'as_get_scheduled_actions',
+ array(
+ 'hook' => 'adjust_download_permissions',
+ 'args' => $scheduled_action_args,
+ 'status' => \ActionScheduler_Store::STATUS_PENDING,
+ ),
+ 'ids'
+ );
+
+ if ( empty( $already_scheduled_actions ) ) {
+ WC()->call_function(
+ 'as_schedule_single_action',
+ WC()->call_function( 'time' ) + 1,
+ 'adjust_download_permissions',
+ $scheduled_action_args
+ );
+ }
+ }
+
+ /**
+ * Create additional download permissions for variations if necessary.
+ *
+ * When a simple downloadable product is converted to a variable product,
+ * existing download permissions are still present in the database but they don't apply anymore.
+ * This method creates additional download permissions for the variations based on
+ * the old existing ones for the main product.
+ *
+ * The procedure is as follows. For each existing download permission for the parent product,
+ * check if there's any variation offering the same file for download (the file URL, not name, is checked).
+ * If that is found, check if an equivalent permission exists (equivalent means for the same file and with
+ * the same order id and customer id). If no equivalent permission exists, create it.
+ *
+ * @param int $product_id The id of the product to check permissions for.
+ */
+ public function adjust_download_permissions( int $product_id ) {
+ $product = wc_get_product( $product_id );
+
+ $children_ids = $product->get_children();
+ if ( ! $children_ids ) {
+ return;
+ }
+
+ $parent_downloads = $this->get_download_files_and_permissions( $product );
+ if ( ! $parent_downloads ) {
+ return;
+ }
+
+ $children_with_downloads = array();
+ foreach ( $children_ids as $child_id ) {
+ $child = wc_get_product( $child_id );
+ $children_with_downloads[ $child_id ] = $this->get_download_files_and_permissions( $child );
+ }
+
+ foreach ( $parent_downloads['permission_data_by_file_order_user'] as $parent_file_order_and_user => $parent_download_data ) {
+ foreach ( $children_with_downloads as $child_id => $child_download_data ) {
+ $file_url = $parent_download_data['file'];
+
+ $must_create_permission =
+ // The variation offers the same file as the parent for download...
+ in_array( $file_url, array_keys( $child_download_data['download_ids_by_file_url'] ), true ) &&
+ // ...but no equivalent download permission (same file URL, order id and user id) exists.
+ ! array_key_exists( $parent_file_order_and_user, $child_download_data['permission_data_by_file_order_user'] );
+
+ if ( $must_create_permission ) {
+ // The new child download permission is a copy of the parent's,
+ // but with the product and download ids changed to match those of the variation.
+ $new_download_data = $parent_download_data['data'];
+ $new_download_data['product_id'] = $child_id;
+ $new_download_data['download_id'] = $child_download_data['download_ids_by_file_url'][ $file_url ];
+ $this->downloads_data_store->create_from_data( $new_download_data );
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the existing downloadable files and download permissions for a given product.
+ * The returned value is an array with two keys:
+ *
+ * - download_ids_by_file_url: an associative array of file url => download_id.
+ * - permission_data_by_file_order_user: an associative array where key is "file_url:customer_id:order_id" and value is the full permission data set.
+ *
+ * @param \WC_Product $product The product to get the downloadable files and permissions for.
+ * @return array[] Information about the downloadable files and permissions for the product.
+ */
+ private function get_download_files_and_permissions( \WC_Product $product ) {
+ $result = array(
+ 'permission_data_by_file_order_user' => array(),
+ 'download_ids_by_file_url' => array(),
+ );
+ $downloads = $product->get_downloads();
+ foreach ( $downloads as $download ) {
+ $result['download_ids_by_file_url'][ $download->get_file() ] = $download->get_id();
+ }
+
+ $permissions = $this->downloads_data_store->get_downloads( array( 'product_id' => $product->get_id() ) );
+ foreach ( $permissions as $permission ) {
+ $permission_data = (array) $permission->data;
+ if ( array_key_exists( $permission_data['download_id'], $downloads ) ) {
+ $file = $downloads[ $permission_data['download_id'] ]->get_file();
+ $data = array(
+ 'file' => $file,
+ 'data' => (array) $permission->data,
+ );
+ $result['permission_data_by_file_order_user'][ "${file}:${permission_data['user_id']}:${permission_data['order_id']}" ] = $data;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Packages.php b/src/Packages.php
index 6b1208963e9..1d017c70a2d 100644
--- a/src/Packages.php
+++ b/src/Packages.php
@@ -72,10 +72,18 @@ class Packages {
// Proxies "activated_plugin" hook for embedded packages listen on WC plugin activation
// https://github.com/woocommerce/woocommerce/issues/28697.
if ( is_admin() ) {
- $woocommerce_activated_plugin = get_transient( 'woocommerce_activated_plugin' );
- if ( $woocommerce_activated_plugin ) {
+ $activated_plugin = get_transient( 'woocommerce_activated_plugin' );
+ if ( $activated_plugin ) {
delete_transient( 'woocommerce_activated_plugin' );
- do_action( 'woocommerce_activated_plugin', $woocommerce_activated_plugin );
+
+ /**
+ * WooCommerce is activated hook.
+ *
+ * @since 5.0.0
+ * @param bool $activated_plugin Activated plugin path,
+ * generally woocommerce/woocommerce.php.
+ */
+ do_action( 'woocommerce_activated_plugin', $activated_plugin );
}
}
}
diff --git a/src/Proxies/LegacyProxy.php b/src/Proxies/LegacyProxy.php
index fff4ee62986..b4bce3e1bba 100644
--- a/src/Proxies/LegacyProxy.php
+++ b/src/Proxies/LegacyProxy.php
@@ -52,6 +52,11 @@ class LegacyProxy {
return $class_name::instance( ...$args );
}
+ // If the class has a "load" method, use it.
+ if ( method_exists( $class_name, 'load' ) ) {
+ return $class_name::load( ...$args );
+ }
+
// Fallback to simply creating a new instance of the class.
return new $class_name( ...$args );
}
diff --git a/src/Vendor/.gitignore b/src/Vendor/.gitignore
index ef7b2901fa6..316569909e1 100644
--- a/src/Vendor/.gitignore
+++ b/src/Vendor/.gitignore
@@ -1,5 +1,5 @@
# Prevent anyone from accidentally adding code to these directories.
-# This will break any PRs that do, revealing ths mistake they made.
-README.md
+# This will break any PRs that do, revealing the mistake they made.
+*
!.gitignore
!README.md
diff --git a/templates/single-product/product-image.php b/templates/single-product/product-image.php
index 0399028c8d8..d892b4b69b0 100644
--- a/templates/single-product/product-image.php
+++ b/templates/single-product/product-image.php
@@ -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(
get_image_id() ) {
+ if ( $post_thumbnail_id ) {
$html = wc_get_gallery_image_html( $post_thumbnail_id, true );
} else {
$html = '';
diff --git a/tests/e2e/api/CHANGELOG.md b/tests/e2e/api/CHANGELOG.md
index 99f2fd40686..ced421cefaa 100644
--- a/tests/e2e/api/CHANGELOG.md
+++ b/tests/e2e/api/CHANGELOG.md
@@ -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
diff --git a/tests/e2e/api/package.json b/tests/e2e/api/package.json
index 4b8dbc990aa..76ce5abae51 100644
--- a/tests/e2e/api/package.json
+++ b/tests/e2e/api/package.json
@@ -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",
diff --git a/tests/e2e/config/jest.setup.js b/tests/e2e/config/jest.setup.js
index b786d7aa9d7..2f2f5140a87 100644
--- a/tests/e2e/config/jest.setup.js
+++ b/tests/e2e/config/jest.setup.js
@@ -1,18 +1,19 @@
const {
- switchUserToAdmin,
visitAdminPage,
switchUserToTest,
clearLocalStorage,
setBrowserViewport
} = require( "@wordpress/e2e-test-utils" );
+const { merchant } = require( '@woocommerce/e2e-utils' );
+
/**
* Navigates to the post listing screen and bulk-trashes any posts which exist.
*
* @return {Promise} Promise resolving once posts have been trashed.
*/
async function trashExistingPosts() {
- await switchUserToAdmin();
+ await merchant.login();
// Visit `/wp-admin/edit.php` so we can see a list of posts and delete them.
await visitAdminPage( 'edit.php' );
@@ -41,7 +42,7 @@ async function trashExistingPosts() {
* @return {Promise} Promise resolving once products have been trashed.
*/
async function trashExistingProducts() {
- await switchUserToAdmin();
+ await merchant.login();
// Visit `/wp-admin/edit.php?post_type=product` so we can see a list of products and delete them.
await visitAdminPage( 'edit.php', 'post_type=product' );
diff --git a/tests/e2e/core-tests/CHANGELOG.md b/tests/e2e/core-tests/CHANGELOG.md
index 4e247139e08..9c64777498d 100644
--- a/tests/e2e/core-tests/CHANGELOG.md
+++ b/tests/e2e/core-tests/CHANGELOG.md
@@ -1,12 +1,21 @@
# Unreleased
+# 0.1.1
+
## Added
+
+- Registered Shopper Checkout tests
- Merchant Order Status Filter tests
- Merchant Order Refund tests
- Merchant Apply Coupon tests
- Added new config variable for Simple Product price to `tests/e2e/env/config/default.json`. Defaults to 9.99
- Shopper My Account Pay Order
+- Shopper Checkout Apply Coupon
+
+
+- Shopper Cart Apply Coupon
+
## Fixed
- Flaky Create Product, Coupon, and Order tests
diff --git a/tests/e2e/core-tests/README.md b/tests/e2e/core-tests/README.md
index 3d32fdf33f9..a75a039f18c 100644
--- a/tests/e2e/core-tests/README.md
+++ b/tests/e2e/core-tests/README.md
@@ -37,32 +37,32 @@ The functions to access the core tests are:
### Activation and setup
- `runSetupOnboardingTests` - Run all setup and onboarding tests
-- `runActivationTest` - Merchant can activate WooCommerce
-- `runOnboardingFlowTest` - Merchant can complete onboarding flow
-- `runTaskListTest` - Merchant can complete onboarding task list
-- `runInitialStoreSettingsTest` - Merchant can complete initial settings
+ - `runActivationTest` - Merchant can activate WooCommerce
+ - `runOnboardingFlowTest` - Merchant can complete onboarding flow
+ - `runTaskListTest` - Merchant can complete onboarding task list
+ - `runInitialStoreSettingsTest` - Merchant can complete initial settings
### Merchant
- `runMerchantTests` - Run all merchant tests
-- `runCreateCouponTest` - Merchant can create coupon
-- `runCreateOrderTest` - Merchant can create order
-- `runAddSimpleProductTest` - Merchant can create a simple product
-- `runAddVariableProductTest` - Merchant can create a variable product
-- `runUpdateGeneralSettingsTest` - Merchant can update general settings
-- `runProductSettingsTest` - Merchant can update product settings
-- `runTaxSettingsTest` - Merchant can update tax settings
-- `runOrderStatusFilterTest` - Merchant can filter orders by order status
-- `runOrderRefundTest` - Merchant can refund an order
-- `runOrderApplyCouponTest` - Merchant can apply a coupon to an order
+ - `runCreateCouponTest` - Merchant can create coupon
+ - `runCreateOrderTest` - Merchant can create order
+ - `runAddSimpleProductTest` - Merchant can create a simple product
+ - `runAddVariableProductTest` - Merchant can create a variable product
+ - `runUpdateGeneralSettingsTest` - Merchant can update general settings
+ - `runProductSettingsTest` - Merchant can update product settings
+ - `runTaxSettingsTest` - Merchant can update tax settings
+ - `runOrderStatusFilterTest` - Merchant can filter orders by order status
+ - `runOrderRefundTest` - Merchant can refund an order
+ - `runOrderApplyCouponTest` - Merchant can apply a coupon to an order
### Shopper
- `runShopperTests` - Run all shopper tests
-- `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
+ - `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
## 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' );
// ...
diff --git a/tests/e2e/core-tests/package.json b/tests/e2e/core-tests/package.json
index d742c0613f2..463f960069c 100644
--- a/tests/e2e/core-tests/package.json
+++ b/tests/e2e/core-tests/package.json
@@ -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"
diff --git a/tests/e2e/core-tests/specs/index.js b/tests/e2e/core-tests/specs/index.js
index 8496219af50..8968b87a15f 100644
--- a/tests/e2e/core-tests/specs/index.js
+++ b/tests/e2e/core-tests/specs/index.js
@@ -9,7 +9,9 @@ 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' );
const runMyAccountPayOrderTest = require( './shopper/front-end-my-account-pay-order.test' );
const runMyAccountPageTest = require( './shopper/front-end-my-account.test' );
@@ -34,7 +36,9 @@ const runSetupOnboardingTests = () => {
};
const runShopperTests = () => {
+ runCartApplyCouponsTest();
runCartPageTest();
+ runCheckoutApplyCouponsTest();
runCheckoutPageTest();
runMyAccountPayOrderTest();
runMyAccountPageTest();
@@ -60,7 +64,9 @@ module.exports = {
runTaskListTest,
runInitialStoreSettingsTest,
runSetupOnboardingTests,
+ runCartApplyCouponsTest,
runCartPageTest,
+ runCheckoutApplyCouponsTest,
runCheckoutPageTest,
runMyAccountPayOrderTest,
runMyAccountPageTest,
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-apply-coupon.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-apply-coupon.test.js
index 01635b1fc34..dc29a02a7b7 100644
--- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-apply-coupon.test.js
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-apply-coupon.test.js
@@ -10,6 +10,7 @@ const {
createCoupon,
uiUnblocked,
addProductToOrder,
+ evalAndClick,
} = require( '@woocommerce/e2e-utils' );
const config = require( 'config' );
@@ -55,7 +56,7 @@ const runOrderApplyCouponTest = () => {
// Verify the coupon list is showing
await page.waitForSelector('.wc-used-coupons');
await expect(page).toMatchElement('.wc_coupon_list', { text: 'Coupon(s)' });
- await expect(page).toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode });
+ await expect(page).toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode.toLowerCase() });
// Check that the coupon has been applied
await expect(page).toMatchElement('.wc-order-item-discount', { text: '5.00' });
@@ -65,16 +66,13 @@ const runOrderApplyCouponTest = () => {
it('can remove a coupon', async () => {
// Make sure we have a coupon on the page to use
await page.waitForSelector('.wc-used-coupons');
- await expect(page).toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode });
-
- // We need to use this here as `expect(page).toClick()` was unable to find the element
- // See: https://github.com/puppeteer/puppeteer/issues/1769#issuecomment-637645219
- page.$eval('a.remove-coupon', elem => elem.click());
+ await expect(page).toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode.toLowerCase() });
+ await evalAndClick( 'a.remove-coupon' );
await uiUnblocked();
// Verify the coupon pricing has been removed
- await expect(page).not.toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode });
+ await expect(page).not.toMatchElement('.wc_coupon_list li.code.editable', { text: couponCode.toLowerCase() });
await expect(page).not.toMatchElement('.wc-order-item-discount', { text: '5.00' });
await expect(page).not.toMatchElement('.line-cost .view .woocommerce-Price-amount', { text: discountedPrice });
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-refund.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-refund.test.js
index 2936c280ab8..7c4a53668bf 100644
--- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-refund.test.js
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-refund.test.js
@@ -11,6 +11,7 @@ const {
verifyValueOfInputField,
uiUnblocked,
addProductToOrder,
+ evalAndClick,
} = require( '@woocommerce/e2e-utils' );
const config = require( 'config' );
@@ -75,10 +76,7 @@ const runRefundOrderTest = () => {
});
it('can delete an issued refund', async () => {
- // We need to use this here as `expect(page).toClick()` was unable to find the element
- // See: https://github.com/puppeteer/puppeteer/issues/1769#issuecomment-637645219
- page.$eval('a.delete_refund', elem => elem.click());
-
+ await evalAndClick( 'a.delete_refund' );
await uiUnblocked();
// Verify the refunded row item is no longer showing
diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-product-new.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-product-new.test.js
index 9d8a416b032..5155fbae0b6 100644
--- a/tests/e2e/core-tests/specs/merchant/wp-admin-product-new.test.js
+++ b/tests/e2e/core-tests/specs/merchant/wp-admin-product-new.test.js
@@ -6,6 +6,7 @@ const {
merchant,
clickTab,
uiUnblocked,
+ evalAndClick,
setCheckbox,
} = require( '@woocommerce/e2e-utils' );
const {
@@ -120,9 +121,7 @@ const runAddVariableProductTest = () => {
// headless: false doesn't require this
const firstDialog = await expect(page).toDisplayDialog(async () => {
- // Using this technique since toClick() isn't working.
- // See: https://github.com/GoogleChrome/puppeteer/issues/1805#issuecomment-464802876
- page.$eval('a.do_variation_action', elem => elem.click());
+ await evalAndClick( 'a.do_variation_action' );
});
await expect(firstDialog.message()).toMatch('Are you sure you want to link all variations?');
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js b/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js
new file mode 100644
index 00000000000..288c11eea48
--- /dev/null
+++ b/tests/e2e/core-tests/specs/shopper/front-end-cart-coupons.test.js
@@ -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;
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js b/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js
new file mode 100644
index 00000000000..446c3af5c6a
--- /dev/null
+++ b/tests/e2e/core-tests/specs/shopper/front-end-checkout-coupons.test.js
@@ -0,0 +1,118 @@
+/* eslint-disable jest/no-export, jest/no-disabled-tests, jest/expect-expect, jest/no-standalone-expect */
+/**
+ * Internal dependencies
+ */
+const {
+ shopper,
+ merchant,
+ createCoupon,
+ createSimpleProduct,
+ uiUnblocked
+} = require( '@woocommerce/e2e-utils' );
+
+/**
+ * External dependencies
+ */
+const {
+ it,
+ describe,
+ beforeAll,
+} = require( '@jest/globals' );
+
+const runCheckoutApplyCouponsTest = () => {
+ describe('Checkout 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 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.'});
+
+ // 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).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'});
+
+ // 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).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'});
+
+ // 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!' });
+
+ // 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'});
+ });
+ });
+};
+
+module.exports = runCheckoutApplyCouponsTest;
diff --git a/tests/e2e/core-tests/specs/shopper/front-end-checkout.test.js b/tests/e2e/core-tests/specs/shopper/front-end-checkout.test.js
index 2928b6e38ec..17e84c339c7 100644
--- a/tests/e2e/core-tests/specs/shopper/front-end-checkout.test.js
+++ b/tests/e2e/core-tests/specs/shopper/front-end-checkout.test.js
@@ -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);
});
});
};
diff --git a/tests/e2e/env/.env b/tests/e2e/env/.env
index 2c355997315..703f339c0ea 100644
--- a/tests/e2e/env/.env
+++ b/tests/e2e/env/.env
@@ -9,6 +9,3 @@ WORDPRESS_DEBUG=1
# WordPress CLI environment
WORDPRESS_HOST=wordpress-www:80
WORDPRESS_TITLE=WooCommerce Core E2E Test Suite
-WORDPRESS_LOGIN=admin
-WORDPRESS_PASSWORD=password
-WORDPRESS_EMAIL=admin@woocommercecoree2etestsuite.com
diff --git a/tests/e2e/env/CHANGELOG.md b/tests/e2e/env/CHANGELOG.md
index 724002c1e78..94be2bfd1f8 100644
--- a/tests/e2e/env/CHANGELOG.md
+++ b/tests/e2e/env/CHANGELOG.md
@@ -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,6 +15,7 @@
## Fixed
- Remove redundant `puppeteer` dependency
+- Support for admin user configuration from `default.json`
# 0.1.6
diff --git a/tests/e2e/env/bin/docker-compose.js b/tests/e2e/env/bin/docker-compose.js
index 39fcc3da72b..79e6038117d 100755
--- a/tests/e2e/env/bin/docker-compose.js
+++ b/tests/e2e/env/bin/docker-compose.js
@@ -4,7 +4,7 @@ const { spawnSync } = require( 'child_process' );
const program = require( 'commander' );
const path = require( 'path' );
const fs = require( 'fs' );
-const { getAppBase, getAppRoot, getAppName, getTestConfig } = require( '../utils' );
+const { getAdminConfig, getAppBase, getAppRoot, getAppName, getTestConfig } = require( '../utils' );
const dockerArgs = [];
let command = '';
@@ -29,7 +29,7 @@ program
.parse( process.argv );
const appPath = getAppRoot();
-const envVars = {};
+const envVars = getAdminConfig();
if ( appPath ) {
if ( 'up' === command ) {
diff --git a/tests/e2e/env/bin/wc-e2e.sh b/tests/e2e/env/bin/wc-e2e.sh
index 889ddaabd09..0f5eee0d23e 100755
--- a/tests/e2e/env/bin/wc-e2e.sh
+++ b/tests/e2e/env/bin/wc-e2e.sh
@@ -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
diff --git a/tests/e2e/env/builtin.md b/tests/e2e/env/builtin.md
index cd8ee3b0a34..3213e5387be 100644
--- a/tests/e2e/env/builtin.md
+++ b/tests/e2e/env/builtin.md
@@ -48,11 +48,6 @@ The built in container initialization needs to know the particulars of your test
"username": "admin",
"password": "password",
"email": "admin@woocommercecoree2etestsuite.com"
- },
- "customer": {
- "username": "customer",
- "password": "password",
- "email": "customer@woocommercecoree2etestsuite.com"
}
}
}
diff --git a/tests/e2e/env/package.json b/tests/e2e/env/package.json
index c68d945b2c7..e9136a75c09 100644
--- a/tests/e2e/env/package.json
+++ b/tests/e2e/env/package.json
@@ -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",
diff --git a/tests/e2e/env/utils/app-name.js b/tests/e2e/env/utils/app-name.js
index 1b3e69253d0..f70c85146eb 100644
--- a/tests/e2e/env/utils/app-name.js
+++ b/tests/e2e/env/utils/app-name.js
@@ -5,7 +5,7 @@ const path = require( 'path' );
/**
* Internal dependencies
*/
-const getTestConfig = require( './test-config' );
+const { getTestConfig } = require( './test-config' );
const getAppRoot = require( './app-root' );
const getAppName = () => {
diff --git a/tests/e2e/env/utils/get-base-url.js b/tests/e2e/env/utils/get-base-url.js
index 9b7a55942bd..8564a1ce00c 100644
--- a/tests/e2e/env/utils/get-base-url.js
+++ b/tests/e2e/env/utils/get-base-url.js
@@ -1,7 +1,7 @@
/**
* Provide the base test URL to bash scripts.
*/
-const getTestConfig = require( './test-config' );
+const { getTestConfig } = require( './test-config' );
const testConfig = getTestConfig();
console.log( testConfig.baseUrl );
diff --git a/tests/e2e/env/utils/index.js b/tests/e2e/env/utils/index.js
index 88bacb6c7b1..ce89ae395f7 100644
--- a/tests/e2e/env/utils/index.js
+++ b/tests/e2e/env/utils/index.js
@@ -1,10 +1,11 @@
const getAppRoot = require( './app-root' );
const { getAppName, getAppBase } = require( './app-name' );
-const getTestConfig = require( './test-config' );
+const { getTestConfig, getAdminConfig } = require( './test-config' );
module.exports = {
getAppBase,
getAppRoot,
getAppName,
getTestConfig,
+ getAdminConfig,
};
diff --git a/tests/e2e/env/utils/test-config.js b/tests/e2e/env/utils/test-config.js
index 5ab7a5bc6ce..966fe7c2543 100644
--- a/tests/e2e/env/utils/test-config.js
+++ b/tests/e2e/env/utils/test-config.js
@@ -27,4 +27,21 @@ const getTestConfig = () => {
return testConfig;
};
-module.exports = getTestConfig;
+/**
+ * Get user account settings for Docker configuration.
+ */
+const getAdminConfig = () => {
+ const testConfig = getTestConfig();
+ const adminConfig = {
+ 'WORDPRESS_LOGIN': testConfig.users.admin.username ? testConfig.users.admin.username : 'admin',
+ 'WORDPRESS_PASSWORD': testConfig.users.admin.password ? testConfig.users.admin.password : 'password',
+ 'WORDPRESS_EMAIL': testConfig.users.admin.email ? testConfig.users.admin.email : 'admin@woocommercecoree2etestsuite.com',
+ };
+
+ return adminConfig;
+};
+
+module.exports = {
+ getTestConfig,
+ getAdminConfig,
+};
diff --git a/tests/e2e/specs/front-end/test-cart-coupons.js b/tests/e2e/specs/front-end/test-cart-coupons.js
new file mode 100644
index 00000000000..66c70adbb88
--- /dev/null
+++ b/tests/e2e/specs/front-end/test-cart-coupons.js
@@ -0,0 +1,6 @@
+/*
+ * Internal dependencies
+ */
+const { runCartApplyCouponsTest } = require( '@woocommerce/e2e-core-tests' );
+
+runCartApplyCouponsTest();
diff --git a/tests/e2e/specs/front-end/test-checkout-coupons.js b/tests/e2e/specs/front-end/test-checkout-coupons.js
new file mode 100644
index 00000000000..6bc6e858302
--- /dev/null
+++ b/tests/e2e/specs/front-end/test-checkout-coupons.js
@@ -0,0 +1,6 @@
+/*
+ * Internal dependencies
+ */
+const { runCheckoutApplyCouponsTest } = require( '@woocommerce/e2e-core-tests' );
+
+runCheckoutApplyCouponsTest();
diff --git a/tests/e2e/utils/CHANGELOG.md b/tests/e2e/utils/CHANGELOG.md
index ac35c538321..62aaae9edc1 100644
--- a/tests/e2e/utils/CHANGELOG.md
+++ b/tests/e2e/utils/CHANGELOG.md
@@ -1,5 +1,7 @@
# Unreleased
+# 0.1.2
+
## Fixed
- Missing `config` package dependency
@@ -12,11 +14,13 @@
- `createSimpleOrder( status )` component which accepts an order status string and creates a basic order with that status
- `addProductToOrder( orderId, productName )` component which adds the provided productName to the passed in orderId
- `createCoupon( couponAmount )` component which accepts a coupon amount string (it defaults to 5) and creates a basic coupon. Returns the generated coupon code.
+- `evalAndClick( selector )` use Puppeteer page.$eval to select and click and element.
## Changes
- Deprecated `StoreOwnerFlow`, `CustomerFlow` in favour of `merchant`,`shopper`
- `createSimpleOrder( status )` returns the ID of the order that was created
+- Updated `createCoupon( couponAmount )` component by adding a new parameter `discountType` which allows you to use any coupon discount type in tests
# 0.1.1
diff --git a/tests/e2e/utils/README.md b/tests/e2e/utils/README.md
index 08481e99b8e..e04635ecb27 100644
--- a/tests/e2e/utils/README.md
+++ b/tests/e2e/utils/README.md
@@ -14,21 +14,20 @@ npm install @woocommerce/e2e-utils --save
Example:
~~~js
import {
- CustomerFlow,
- StoreOwnerFlow,
- createSimpleProduct,
- uiUnblocked
+ shopper,
+ merchant,
+ createSimpleProduct
} from '@woocommerce/e2e-utils';
describe( 'Cart page', () => {
beforeAll( async () => {
- await StoreOwnerFlow.login();
+ await merchant.login();
await createSimpleProduct();
- await StoreOwnerFlow.logout();
+ await merchant.logout();
} );
it( 'should display no item in the cart', async () => {
- await CustomerFlow.goToCart();
+ await shopper.goToCart();
await expect( page ).toMatchElement( '.cart-empty', { text: 'Your cart is currently empty.' } );
} );
} );
@@ -36,10 +35,11 @@ describe( 'Cart page', () => {
## Test Function
-### Merchant `StoreOwnerFlow`
+### Merchant `merchant`
| Function | Parameters | Description |
|----------|-------------|------------|
+| `goToOrder` | `orderId` | Go to view a single order |
| `login` | | Log in as merchant |
| `logout` | | Log out of merchant account |
| `openAllOrdersView` | | Go to the orders listing |
@@ -51,11 +51,9 @@ describe( 'Cart page', () => {
| `openPlugins` | | Go to the Plugins screen |
| `openSettings` | | Go to WooCommerce -> Settings |
| `runSetupWizard` | | Open the onboarding profiler |
-| `goToOrder` | `orderId` | Go to view a single order |
| `updateOrderStatus` | `orderId, status` | Update the status of an order |
-|----------|-------------|-------------|
-### Shopper `CustomerFlow`
+### Shopper `shopper`
| Function | Parameters | Description |
|----------|------------|-------------|
@@ -67,23 +65,28 @@ describe( 'Cart page', () => {
| `goToAccountDetails` | | Go to My Account -> Details |
| `goToCart` | | Go to the cart page |
| `goToCheckout` | | Go to the checkout page |
-| `goToShop` | | Go to the shop page |
-| `goToProduct` | `productId` | Go to a single product in the shop |
-| `goToOrders` | | Go to My Account -> Orders |
| `goToDownloads` | | Go to My Account -> Downloads |
+| `goToMyAccount` | | Go to the My Account page |
+| `goToOrders` | | Go to My Account -> Orders |
+| `goToProduct` | `productId` | Go to a single product in the shop |
+| `goToShop` | | Go to the shop page |
| `login` | | Log in as the shopper |
| `placeOrder` | | Place an order from the checkout page |
| `productIsInCheckout` | `productTitle, quantity, total, cartSubtotal` | Verify product is in cart on checkout page |
| `removeFromCart` | `productTitle` | Remove a product from the cart on the cart page |
| `setCartQuantity` | `productTitle, quantityValue` | Change the quantity of a product on the cart page |
-|----------|------------|-------------|
### Page Utilities
| Function | Parameters | Description |
|----------|------------|-------------|
+| `addProductToOrder` | `orderId, productName` | adds a product to an order using the product search |
| `clearAndFillInput` | `selector, value` | Replace the contents of an input with the passed value |
+| `clickFilter` | `selector` | helper method that clicks on a list page filter |
| `clickTab` | `tabName` | Click on a WooCommerce -> Settings tab |
+| `createCoupon` | `couponAmount` | creates a basic coupon. Default amount is 5. Returns the generated coupon code. |
+| `createSimpleOrder` | `status` | creates a basic order with the provided status string |
+| `moveAllItemsToTrash` | | helper method that checks every item in a list page and moves them to the trash |
| `settingsPageSaveChanges` | | Save the current WooCommerce settings page |
| `permalinkSettingsPageSaveChanges` | | Save the current Permalink settings |
| `setCheckbox` | `selector` | Check a checkbox |
@@ -95,7 +98,6 @@ describe( 'Cart page', () => {
| `verifyValueOfInputField` | `selector, value` | Verify an input contains the passed value |
| `clickFilter` | `selector` | Click on a list page filter |
| `moveAllItemsToTrash` | | Moves all items in a list view to the Trash |
-|----------|------------|-------------|
### Test Utilities
diff --git a/tests/e2e/utils/package.json b/tests/e2e/utils/package.json
index 64d2618e17a..36bd9dce48a 100644
--- a/tests/e2e/utils/package.json
+++ b/tests/e2e/utils/package.json
@@ -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": {
diff --git a/tests/e2e/utils/src/components.js b/tests/e2e/utils/src/components.js
index 60acef14b0a..7ef45aeb128 100644
--- a/tests/e2e/utils/src/components.js
+++ b/tests/e2e/utils/src/components.js
@@ -6,7 +6,7 @@
* Internal dependencies
*/
import { merchant } from './flows';
-import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './page-utils';
+import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick } 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
}
+ */
+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
@@ -403,17 +368,18 @@ const addProductToOrder = async ( orderId, productName ) => {
* Creates a basic coupon with the provided coupon amount. Returns the coupon code.
*
* @param couponAmount Amount to be applied. Defaults to 5.
+ * @param discountType Type of a coupon. Defaults to Fixed cart discount.
*/
-const createCoupon = async ( couponAmount = '5' ) => {
+const createCoupon = async ( couponAmount = '5', discountType = 'Fixed cart discount' ) => {
await merchant.openNewCoupon();
// Fill in coupon code
- let couponCode = 'code-' + new Date().getTime().toString();
+ let couponCode = 'Code-' + discountType + new Date().getTime().toString();
await expect(page).toFill( '#title', couponCode );
// Set general coupon data
await clickTab( 'General' );
- await expect(page).toSelect( '#discount_type', 'Fixed cart discount' );
+ await expect(page).toSelect( '#discount_type', discountType );
await expect(page).toFill( '#coupon_amount', couponAmount );
// Publish coupon
diff --git a/tests/e2e/utils/src/flows/merchant.js b/tests/e2e/utils/src/flows/merchant.js
index ce512174f36..f58d184a829 100644
--- a/tests/e2e/utils/src/flows/merchant.js
+++ b/tests/e2e/utils/src/flows/merchant.js
@@ -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;
diff --git a/tests/e2e/utils/src/page-utils.js b/tests/e2e/utils/src/page-utils.js
index 461348bc7a3..36ab0cdd805 100644
--- a/tests/e2e/utils/src/page-utils.js
+++ b/tests/e2e/utils/src/page-utils.js
@@ -160,7 +160,7 @@ const verifyValueOfInputField = async( selector, value ) => {
*
* @param {string} selector Selector of the filter link to be clicked.
*/
-const clickFilter = async( selector ) => {
+const clickFilter = async ( selector ) => {
await page.waitForSelector( selector );
await page.focus( selector );
await Promise.all( [
@@ -174,7 +174,7 @@ const clickFilter = async( selector ) => {
*
* If there's more than 20 items, it moves all 20 items on the current page.
*/
-const moveAllItemsToTrash = async() => {
+const moveAllItemsToTrash = async () => {
await setCheckbox( '#cb-select-all-1' );
await expect( page ).toSelect( '#bulk-action-selector-top', 'Move to Trash' );
await Promise.all( [
@@ -183,6 +183,19 @@ const moveAllItemsToTrash = async() => {
] );
};
+/**
+ * Use puppeteer page eval to click an element.
+ *
+ * Useful for clicking items that have been added to the DOM via ajax.
+ *
+ * @param {string} selector Selector of the filter link to be clicked.
+ */
+const evalAndClick = async ( selector ) => {
+ // We use this when `expect(page).toClick()` is unable to find the element
+ // See: https://github.com/puppeteer/puppeteer/issues/1769#issuecomment-637645219
+ page.$eval( selector, elem => elem.click() );
+};
+
export {
clearAndFillInput,
clickTab,
@@ -197,4 +210,5 @@ export {
verifyValueOfInputField,
clickFilter,
moveAllItemsToTrash,
+ evalAndClick,
};
diff --git a/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php b/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php
new file mode 100644
index 00000000000..907846f88f2
--- /dev/null
+++ b/tests/legacy/unit-tests/blocks/class-wc-tests-blocks-utils.php
@@ -0,0 +1,113 @@
+ 'blocks-page',
+ 'title' => 'Checkout',
+ 'content' => '
',
+ );
+
+ $page_id = wc_create_page( $page['name'], '', $page['title'], $page['content'] );
+
+ $this->assertTrue( WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/checkout' ) );
+ $this->assertFalse( WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/cart' ) );
+ }
+
+ /**
+ * @group block-utils
+ * Test: has_block_in_page.
+ *
+ */
+ public function test_has_block_in_page_on_page_with_no_blocks() {
+ $page = array(
+ 'name' => 'shortcode-page',
+ 'title' => 'Checkout',
+ 'content' => ' [woocommerce_checkout] ',
+ );
+
+ $page_id = wc_create_page( $page['name'], '', $page['title'], $page['content'] );
+
+ $this->assertFalse( WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/checkout' ) );
+ $this->assertFalse( WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/cart' ) );
+ }
+
+ /**
+ * @group block-utils
+ * Test: has_block_in_page.
+ *
+ */
+ public function test_has_block_in_page_on_page_with_multiple_blocks() {
+ $page = array(
+ 'name' => 'shortcode-page',
+ 'title' => 'Checkout',
+ 'content' => '
+
+
+
+
+
+
+ test
+ ',
+ );
+
+ $page_id = wc_create_page( $page['name'], '', $page['title'], $page['content'] );
+
+ $this->assertTrue( WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/featured-product' ) );
+ $this->assertTrue( WC_Blocks_Utils::has_block_in_page( $page_id, 'core/heading' ) );
+ }
+
+ /**
+ * @group block-utils
+ * Test: get_all_blocks_from_page.
+ *
+ */
+ public function test_get_all_blocks_from_page() {
+ $page = array(
+ 'name' => 'cart',
+ 'title' => 'Checkout',
+ 'content' => 'test1 test2 ',
+ );
+
+ wc_create_page( $page['name'], 'woocommerce_cart_page_id', $page['title'], $page['content'] );
+
+ $expected = array(
+ 0 => array(
+ 'blockName' => 'core/heading',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => 'test1 ',
+ 'innerContent' => array(
+ 0 => 'test1 ',
+ ),
+ ),
+ 1 => array(
+ 'blockName' => 'core/heading',
+ 'attrs' => array(),
+ 'innerBlocks' => array(),
+ 'innerHTML' => 'test2 ',
+ 'innerContent' => array(
+ 0 => 'test2 ',
+ ),
+ ),
+ );
+
+ $blocks = WC_Blocks_Utils::get_blocks_from_page( 'core/heading', 'cart' );
+
+ $this->assertEquals( $expected, $blocks );
+ }
+}
diff --git a/tests/php/includes/class-wc-comments-test.php b/tests/php/includes/class-wc-comments-test.php
new file mode 100644
index 00000000000..7b433e9c9cc
--- /dev/null
+++ b/tests/php/includes/class-wc-comments-test.php
@@ -0,0 +1,49 @@
+get_id() => 0,
+ $product2->get_id() => 0,
+ $product3->get_id() => 0,
+ );
+ $product_id_array = array_keys( $expected_review_count );
+
+ $this->assertEquals( $expected_review_count, WC_Comments::get_review_counts_for_product_ids( $product_id_array ) );
+
+ \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product2->get_id() );
+
+ \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product3->get_id() );
+ \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product3->get_id() );
+
+ $expected_review_count = array(
+ $product1->get_id() => 0,
+ $product2->get_id() => 1,
+ $product3->get_id() => 2,
+ );
+ $this->assertEquals( $expected_review_count, WC_Comments::get_review_counts_for_product_ids( $product_id_array ) );
+ }
+
+ /**
+ * Test get_review_count_for_product.
+ */
+ public function test_get_review_count_for_product() {
+ $product = WC_Helper_Product::create_simple_product();
+ $this->assertEquals( 0, WC_Comments::get_review_count_for_product( $product ) );
+
+ \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() );
+ $this->assertEquals( 1, WC_Comments::get_review_count_for_product( $product ) );
+
+ \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() );
+ $this->assertEquals( 2, WC_Comments::get_review_count_for_product( $product ) );
+ }
+}
diff --git a/tests/php/includes/class-wc-emails-tests.php b/tests/php/includes/class-wc-emails-tests.php
new file mode 100644
index 00000000000..9f7d3904a7c
--- /dev/null
+++ b/tests/php/includes/class-wc-emails-tests.php
@@ -0,0 +1,37 @@
+assertEquals( 10, has_action( 'woocommerce_email_header', array( $email_object, 'email_header' ) ) );
+ ob_start();
+ do_action( 'woocommerce_email_header', 'header' );
+ $content = ob_get_contents();
+ ob_end_clean();
+ $this->assertFalse( empty( $content ) );
+ }
+
+ /**
+ * Test that email_footer hooks are compatible with do_action calls with only param.
+ * This test should be dropped after all extensions are using compatible do_action calls.
+ */
+ public function test_email_footer_is_compatible_with_legacy_do_action() {
+ $email_object = new WC_Emails();
+ // 10 is expected priority of the hook.
+ $this->assertEquals( 10, has_action( 'woocommerce_email_footer', array( $email_object, 'email_footer' ) ) );
+ ob_start();
+ do_action( 'woocommerce_email_footer' );
+ $content = ob_get_contents();
+ ob_end_clean();
+ $this->assertFalse( empty( $content ) );
+ }
+}
diff --git a/tests/php/includes/data-stores/class-wc-shipping-zone-data-store-test.php b/tests/php/includes/data-stores/class-wc-shipping-zone-data-store-test.php
new file mode 100644
index 00000000000..cd8d1dbb6ba
--- /dev/null
+++ b/tests/php/includes/data-stores/class-wc-shipping-zone-data-store-test.php
@@ -0,0 +1,48 @@
+set_zone_name( 'California' );
+ $zone->set_zone_order( 3 );
+ $zone->add_location( 'US:CA', 'state' );
+ $zone->save();
+
+ $datastore = new WC_Shipping_Zone_Data_Store();
+ $datastore->read( $zone );
+ $this->assertSame( 'California', $zone->get_zone_name() );
+ $this->assertSame( 3, $zone->get_zone_order() );
+ $this->assertGreaterThan( 0, did_action( 'woocommerce_shipping_zone_loaded' ) );
+ }
+
+ /**
+ * @testdox read() sets default properties for shipping zone with ID 0.
+ */
+ public function test_read_for_shipping_zone_zero() {
+ $zone = new WC_Shipping_Zone( 0 );
+
+ $datastore = new WC_Shipping_Zone_Data_Store();
+ $datastore->read( $zone );
+ $this->assertSame( 0, $zone->get_zone_order() );
+ $this->assertGreaterThan( 0, did_action( 'woocommerce_shipping_zone_loaded' ) );
+ }
+
+ /**
+ * @testdox read() throws an exception if the zone ID cannot be found.
+ */
+ public function test_read_with_invalid_zone_id() {
+ $this->expectException( \Exception::class );
+
+ $zone = new WC_Shipping_Zone( -1 );
+
+ $datastore = new WC_Shipping_Zone_Data_Store();
+ $datastore->read( $zone );
+ }
+}
diff --git a/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithLoadMethod.php b/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithLoadMethod.php
new file mode 100644
index 00000000000..cd06a276fac
--- /dev/null
+++ b/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithLoadMethod.php
@@ -0,0 +1,35 @@
+sut = new DownloadPermissionsAdjuster();
+ $this->sut->init();
+
+ // This is needed for "product->set_downloads" to work without actual files.
+ add_filter(
+ 'woocommerce_downloadable_file_allowed_mime_types',
+ function() {
+ return array( 'foo' => 'nonsense/foo' );
+ }
+ );
+ add_filter(
+ 'woocommerce_downloadable_file_exists',
+ function( $exists, $filename ) {
+ return true;
+ },
+ 10,
+ 2
+ );
+ }
+
+ /**
+ * @testdox DownloadPermissionsAdjuster class hooks on 'adjust_download_permissions' on initialization.
+ */
+ public function test_class_hooks_on_adjust_download_permissions() {
+ remove_all_actions( 'adjust_download_permissions' );
+ $this->assertFalse( has_action( 'adjust_download_permissions' ) );
+ $this->setUp();
+ $this->assertTrue( has_action( 'adjust_download_permissions' ) );
+ }
+
+ /**
+ * @testdox 'maybe_schedule_adjust_download_permissions' does nothing if the product has no children.
+ */
+ public function test_no_adjustment_is_scheduled_if_product_has_no_children() {
+ $as_get_scheduled_actions_invoked = false;
+ $as_schedule_single_action_invoked = false;
+
+ $this->register_legacy_proxy_function_mocks(
+ array(
+ 'as_get_scheduled_actions' => function( $args, $return_format ) use ( &$as_get_scheduled_actions_invoked ) {
+ $as_get_scheduled_actions_invoked = true;
+ },
+ 'as_schedule_single_action' => function( $timestamp, $hook, $args ) use ( &$as_schedule_single_action_invoked ) {
+ $as_schedule_single_action_invoked = true;
+ },
+ )
+ );
+
+ $product = ProductHelper::create_simple_product();
+ $this->sut->maybe_schedule_adjust_download_permissions( $product );
+
+ $this->assertFalse( $as_get_scheduled_actions_invoked );
+ $this->assertFalse( $as_schedule_single_action_invoked );
+ }
+
+ /**
+ * @testdox 'maybe_schedule_adjust_download_permissions' does nothing if the an adjustment is already pending.
+ */
+ public function test_no_adjustment_is_scheduled_if_already_scheduled() {
+ $as_get_scheduled_actions_args = null;
+ $as_schedule_single_action_invoked = false;
+
+ $this->register_legacy_proxy_function_mocks(
+ array(
+ 'as_get_scheduled_actions' => function( $args, $return_format ) use ( &$as_get_scheduled_actions_args ) {
+ $as_get_scheduled_actions_args = $args;
+ return array( 1 );
+ },
+ 'as_schedule_single_action' => function( $timestamp, $hook, $args ) use ( &$as_schedule_single_action_invoked ) {
+ $as_schedule_single_action_invoked = true;
+ },
+ )
+ );
+
+ $product = ProductHelper::create_variation_product();
+ $this->sut->maybe_schedule_adjust_download_permissions( $product );
+
+ $expected_get_scheduled_actions_args = array(
+ 'hook' => 'adjust_download_permissions',
+ 'args' => array( $product->get_id() ),
+ 'status' => \ActionScheduler_Store::STATUS_PENDING,
+ );
+ $this->assertEquals( $expected_get_scheduled_actions_args, $as_get_scheduled_actions_args );
+ $this->assertFalse( $as_schedule_single_action_invoked );
+ }
+
+ /**
+ * @testdox 'maybe_schedule_adjust_download_permissions' schedules an adjustment if not scheduled already.
+ */
+ public function test_no_adjustment_is_scheduled_if_not_yet_scheduled() {
+ $as_get_scheduled_actions_args = null;
+ $as_schedule_single_action_args = null;
+
+ $this->register_legacy_proxy_function_mocks(
+ array(
+ 'as_get_scheduled_actions' => function( $params, $return_format ) use ( &$as_get_scheduled_actions_args ) {
+ $as_get_scheduled_actions_args = $params;
+ return array();
+ },
+ 'as_schedule_single_action' => function( $timestamp, $hook, $args ) use ( &$as_schedule_single_action_args ) {
+ $as_schedule_single_action_args = array( $timestamp, $hook, $args );
+ },
+ 'time' => function() {
+ return 0; },
+ )
+ );
+
+ $product = ProductHelper::create_variation_product();
+ $this->sut->maybe_schedule_adjust_download_permissions( $product );
+
+ $expected_get_scheduled_actions_args = array(
+ 'hook' => 'adjust_download_permissions',
+ 'args' => array( $product->get_id() ),
+ 'status' => \ActionScheduler_Store::STATUS_PENDING,
+ );
+ $this->assertEquals( $expected_get_scheduled_actions_args, $as_get_scheduled_actions_args );
+
+ $expected_as_schedule_single_action_args = array(
+ 1,
+ 'adjust_download_permissions',
+ array( $product->get_id() ),
+ );
+ $this->assertEquals( $expected_as_schedule_single_action_args, $as_schedule_single_action_args );
+ }
+
+ /**
+ * @testdox 'adjust_download_permissions' creates child download permissions when they are missing (see method comment for details).
+ */
+ public function test_adjust_download_permissions_creates_additional_permissions_if_not_exist() {
+ $download = array(
+ 'name' => 'the_file',
+ 'file' => 'the_file.foo',
+ );
+
+ $product = ProductHelper::create_variation_product();
+ $product->set_downloads( array( $download ) );
+ $product->save();
+ $parent_download_id = current( $product->get_downloads() )->get_id();
+
+ $child = wc_get_product( current( $product->get_children() ) );
+ $child->set_downloads( array( $download ) );
+ $child->save();
+ $child_download_id = current( $child->get_downloads() )->get_id();
+
+ $data_for_data_store =
+ array(
+ $product->get_id() =>
+ array(
+ (object) array(
+ 'data' => array(
+ 'download_id' => $parent_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ 'downloads_remaining' => 34,
+ 'access_granted' => '2000-01-01',
+ 'access_expires' => '2034-02-27',
+ ),
+ ),
+ ),
+ );
+
+ $data_store = $this->create_mock_data_store( $data_for_data_store );
+
+ $this->setUp();
+ $this->sut->adjust_download_permissions( $product->get_id() );
+
+ $expected_created_data = array(
+ 'download_id' => $child_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ 'product_id' => $child->get_id(),
+ 'downloads_remaining' => 34,
+ 'access_granted' => '2000-01-01',
+ 'access_expires' => '2034-02-27',
+ );
+
+ $this->assertEquals( $expected_created_data, $data_store->created_data );
+ }
+
+ /**
+ * @testdox 'adjust_download_permissions' doesn't create child download permissions that already exist.
+ */
+ public function test_adjust_download_permissions_dont_create_additional_permissions_if_already_exists() {
+ $download = array(
+ 'name' => 'the_file',
+ 'file' => 'the_file.foo',
+ );
+
+ $product = ProductHelper::create_variation_product();
+ $product->set_downloads( array( $download ) );
+ $product->save();
+ $parent_download_id = current( $product->get_downloads() )->get_id();
+
+ $child = wc_get_product( current( $product->get_children() ) );
+ $child->set_downloads( array( $download ) );
+ $child->save();
+ $child_download_id = current( $child->get_downloads() )->get_id();
+
+ $data_for_data_store =
+ array(
+ $product->get_id() =>
+ array(
+ (object) array(
+ 'data' => array(
+ 'download_id' => $parent_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ ),
+ ),
+ ),
+ $child->get_id() =>
+ array(
+ (object) array(
+ 'data' => array(
+ 'download_id' => $child_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ ),
+ ),
+ ),
+ );
+
+ $data_store = $this->create_mock_data_store( $data_for_data_store );
+
+ $this->setUp();
+ $this->sut->adjust_download_permissions( $product->get_id() );
+
+ $this->assertEmpty( $data_store->created_data );
+ }
+
+ /**
+ * @testdox 'adjust_download_permissions' creates child download permissions when one exists but for a different order or customer id.
+ *
+ * @testWith [9999, 5678]
+ * [1234, 9999]
+ * @param int $user_id User id the child download permission exists for.
+ * @param int $order_id Order id the child download permission exists for.
+ */
+ public function test_adjust_download_permissions_creates_additional_permissions_if_exists_but_not_matching( $user_id, $order_id ) {
+ $download = array(
+ 'name' => 'the_file',
+ 'file' => 'the_file.foo',
+ );
+
+ $product = ProductHelper::create_variation_product();
+ $product->set_downloads( array( $download ) );
+ $product->save();
+ $parent_download_id = current( $product->get_downloads() )->get_id();
+
+ $child = wc_get_product( current( $product->get_children() ) );
+ $child->set_downloads( array( $download ) );
+ $child->save();
+ $child_download_id = current( $child->get_downloads() )->get_id();
+
+ $data_for_data_store =
+ array(
+ $product->get_id() =>
+ array(
+ (object) array(
+ 'data' => array(
+ 'download_id' => $parent_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ ),
+ ),
+ ),
+ $child->get_id() =>
+ array(
+ (object) array(
+ 'data' => array(
+ 'download_id' => $child_download_id,
+ 'user_id' => $user_id,
+ 'order_id' => $order_id,
+ ),
+ ),
+ ),
+ );
+
+ $data_store = $this->create_mock_data_store( $data_for_data_store );
+
+ $this->setUp();
+ $this->sut->adjust_download_permissions( $product->get_id() );
+
+ $expected = array(
+ 'download_id' => $child_download_id,
+ 'user_id' => 1234,
+ 'order_id' => 5678,
+ 'product_id' => $child->get_id(),
+ );
+
+ $this->assertEquals( $expected, $data_store->created_data );
+ }
+
+ /**
+ * Create and register a mock customer downloads data store.
+ *
+ * @param array $data An array where keys are product ids, and values are what 'get_downloads' will return for that input.
+ * @return object An object that mocks the customer downloads data store.
+ */
+ private function create_mock_data_store( $data ) {
+ // phpcs:disable Squiz.Commenting
+ $data_store = new class($data) {
+ private $data;
+ public $created_data = null;
+
+ public function __construct( $data ) {
+ $this->data = $data;
+ }
+
+ public function get_downloads( $params ) {
+ if ( array_key_exists( $params['product_id'], $this->data ) ) {
+ return $this->data[ $params['product_id'] ];
+ } else {
+ return array();
+ }
+ }
+
+ public function create_from_data( $data ) {
+ $this->created_data = $data;
+ }
+ };
+ // phpcs:enable Squiz.Commenting
+
+ $this->register_legacy_proxy_class_mocks(
+ array(
+ 'WC_Data_Store' => $data_store,
+ )
+ );
+
+ return $data_store;
+ }
+}
diff --git a/tests/php/src/Proxies/LegacyProxyTest.php b/tests/php/src/Proxies/LegacyProxyTest.php
index e27e11b4c60..9d616368665 100644
--- a/tests/php/src/Proxies/LegacyProxyTest.php
+++ b/tests/php/src/Proxies/LegacyProxyTest.php
@@ -60,6 +60,18 @@ class LegacyProxyTest extends \WC_Unit_Test_Case {
$this->assertEquals( array( 'foo', 'bar' ), \ClassWithSingleton::$instance_args );
}
+ /**
+ * @testdox 'get_instance_of' uses the 'load' static method in classes that implement it, passing the supplied arguments.
+ */
+ public function test_get_instance_of_class_with_load_method_gets_an_instance_of_the_appropriate_class() {
+ // ClassWithLoadMethod is in the root namespace and thus can't be autoloaded.
+ require_once dirname( __DIR__ ) . '/Internal/DependencyManagement/ExampleClasses/ClassWithLoadMethod.php';
+
+ $loaded = $this->sut->get_instance_of( \ClassWithLoadMethod::class, 'foo', 'bar' );
+ $this->assertSame( \ClassWithLoadMethod::$loaded, $loaded );
+ $this->assertEquals( array( 'foo', 'bar' ), \ClassWithLoadMethod::$loaded_args );
+ }
+
/**
* @testdox 'get_instance_of' can be used to get an instance of a class implementing WC_Queue_Interface.
*/
diff --git a/woocommerce.php b/woocommerce.php
index e536627c59f..1ba8fe4d67f 100644
--- a/woocommerce.php
+++ b/woocommerce.php
@@ -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