Merge branch 'master' into e2e-shopper-pay-order

This commit is contained in:
Veljko 2021-01-27 17:38:03 +01:00
commit 57a798c0e9
92 changed files with 2197 additions and 603 deletions

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

@ -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

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

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.8.3",
"woocommerce/woocommerce-admin": "1.9.0-rc.3",
"woocommerce/woocommerce-blocks": "4.0.0"
},
"require-dev": {

72
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "758b097c13e89200b28bf3a3b5fc2752",
"content-hash": "5fac5fbdbcff552de109cd95d469e975",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -77,23 +77,20 @@
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1"
},
"time": "2020-10-28T19:00:31+00:00"
},
{
"name": "composer/installers",
"version": "v1.9.0",
"version": "v1.10.0",
"source": {
"type": "git",
"url": "https://github.com/composer/installers.git",
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca"
"reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"url": "https://api.github.com/repos/composer/installers/zipball/1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d",
"reference": "1a0357fccad9d1cc1ea0c9a05b8847fbccccb78d",
"shasum": ""
},
"require": {
@ -104,17 +101,18 @@
"shama/baton": "*"
},
"require-dev": {
"composer/composer": "1.6.* || 2.0.*@dev",
"composer/semver": "1.0.* || 2.0.*@dev",
"phpunit/phpunit": "^4.8.36",
"sebastian/comparator": "^1.2.4",
"composer/composer": "1.6.* || ^2.0",
"composer/semver": "^1 || ^3",
"phpstan/phpstan": "^0.12.55",
"phpstan/phpstan-phpunit": "^0.12.16",
"symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.3"
},
"type": "composer-plugin",
"extra": {
"class": "Composer\\Installers\\Plugin",
"branch-alias": {
"dev-master": "1.0-dev"
"dev-main": "1.x-dev"
}
},
"autoload": {
@ -152,6 +150,7 @@
"Porto",
"RadPHP",
"SMF",
"Starbug",
"Thelia",
"Whmcs",
"WolfCMS",
@ -192,6 +191,7 @@
"phpbb",
"piwik",
"ppi",
"processwire",
"puppet",
"pxcms",
"reindex",
@ -207,21 +207,21 @@
"zend",
"zikula"
],
"support": {
"issues": "https://github.com/composer/installers/issues",
"source": "https://github.com/composer/installers/tree/v1.9.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-04-07T06:57:05+00:00"
"time": "2021-01-14T11:07:16+00:00"
},
{
"name": "maxmind-db/reader",
@ -281,10 +281,6 @@
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0"
},
"time": "2019-12-19T22:59:03+00:00"
},
{
@ -359,10 +355,6 @@
"email",
"pre-processing"
],
"support": {
"issues": "https://github.com/MyIntervals/emogrifier/issues",
"source": "https://github.com/MyIntervals/emogrifier"
},
"time": "2019-12-26T19:37:31+00:00"
},
{
@ -412,10 +404,6 @@
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/master"
},
"time": "2017-02-14T16:28:37+00:00"
},
{
@ -507,24 +495,20 @@
],
"description": "Action Scheduler for WordPress and WooCommerce",
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/master"
},
"time": "2020-05-12T16:22:33+00:00"
},
{
"name": "woocommerce/woocommerce-admin",
"version": "1.8.3",
"version": "1.9.0-rc.3",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "534442980c34369f711efdb8e85fdcb1363be712"
"reference": "8c5b20cb6347959daf5403ee30e47061f335240b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/534442980c34369f711efdb8e85fdcb1363be712",
"reference": "534442980c34369f711efdb8e85fdcb1363be712",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/8c5b20cb6347959daf5403ee30e47061f335240b",
"reference": "8c5b20cb6347959daf5403ee30e47061f335240b",
"shasum": ""
},
"require": {
@ -556,11 +540,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v1.8.3"
},
"time": "2021-01-06T00:00:42+00:00"
"time": "2021-01-22T10:23:29+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -659,10 +639,6 @@
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
},
"time": "2020-05-03T08:27:20+00:00"
}
],
@ -678,5 +654,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "1.1.0"
}

View File

@ -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(),

View File

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

View File

@ -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';
}

View File

@ -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' );

View File

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

View File

@ -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 );

View File

@ -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 );

View File

@ -18,7 +18,7 @@ $untested_plugins_msg = sprintf(
?>
<div id="wc_untested_extensions_modal">
<div class="wc_untested_extensions_modal--content">
<h1><?php esc_html_e( "This is a major update, are you sure you're ready?", 'woocommerce' ); ?></h1>
<h1><?php esc_html_e( "Are you sure you're ready?", 'woocommerce' ); ?></h1>
<div class="wc_plugin_upgrade_notice extensions_warning">
<p><?php echo esc_html( $untested_plugins_msg ); ?></p>
@ -41,7 +41,7 @@ $untested_plugins_msg = sprintf(
</table>
</div>
<p><?php esc_html_e( 'As this is a major update, we strongly recommend creating a backup of your site before updating.', 'woocommerce' ); ?> <a href="https://woocommerce.com/2017/05/create-use-backups-woocommerce/" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p>
<p><?php esc_html_e( 'We strongly recommend creating a backup of your site before updating.', 'woocommerce' ); ?> <a href="https://woocommerce.com/2017/05/create-use-backups-woocommerce/" target="_blank"><?php esc_html_e( 'Learn more', 'woocommerce' ); ?></a></p>
<?php if ( current_user_can( 'update_plugins' ) ) : ?>
<div class="actions">

View File

@ -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,
),
)
);

View File

@ -9,6 +9,16 @@ if ( ! defined( 'ABSPATH' ) ) {
?>
<div class="wrap woocommerce">
<div id="message" class="error inline" style="margin-top:30px">
<p>
<strong>
<?php
/* translators: 1: Link URL */
echo wp_kses_post( sprintf( __( 'With the release of WooCommerce 4.0, these reports are being replaced. There is a new and better Analytics section available for users running WordPress 5.3+. Head on over to the <a href="%1$s">WooCommerce Analytics</a> or learn more about the new experience in the <a href="https://docs.woocommerce.com/document/woocommerce-analytics/" target="_blank">WooCommerce Analytics documentation</a>.', 'woocommerce' ), esc_url( wc_admin_url( '&path=/analytics/overview' ) ) ) );
?>
</strong>
</p>
</div>
<nav class="nav-tab-wrapper woo-nav-tab-wrapper">
<?php
foreach ( $reports as $key => $report_group ) {

View File

@ -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 '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'Page visibility should be <a href="%s" target="_blank">public</a>', 'woocommerce' ), 'https://wordpress.org/support/article/content-visibility/' ) ) . '</mark>';
$found_error = true;
} else {
// Shortcode check.
if ( $_page['shortcode_required'] ) {
if ( ! $_page['shortcode_present'] ) {
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Page does not contain the shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) . '</mark>';
// 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 '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_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'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
}

View File

@ -8,9 +8,7 @@
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<div id="poststuff" class="woocommerce-reports-wide">
<div class="postbox">

View File

@ -0,0 +1,84 @@
<?php
/**
* Blocks Utils
*
* Used by core components that need to work with blocks.
*
* @package WooCommerce\Blocks\Utils
* @version 5.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Blocks Utility class.
*/
class WC_Blocks_Utils {
/**
* 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().
*/
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;
}
}

View File

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

View File

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

View File

@ -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() ];
}
/**

View File

@ -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' );
}
/**

View File

@ -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',
),
);
/**

View File

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

View File

@ -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.

View File

@ -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();

View File

@ -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( '<div class="error"><p>%s %s</p></div>', $message_one, $message_two ); /* WPCS: xss ok. */
printf( '<div class="error"><p>%s %s</p></div>', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**

View File

@ -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
)
);
}

View File

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

View File

@ -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 );

View File

@ -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 );
}
/**

View File

@ -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();

View File

@ -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,
);
}

View File

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

View File

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

View File

@ -3568,7 +3568,12 @@ function wc_empty_cart_message() {
*/
function wc_page_noindex() {
if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) {
wp_no_robots();
// Adds support for WP 5.7.
if ( function_exists( 'wp_robots_no_robots' ) ) {
wp_robots_no_robots();
} else {
wp_no_robots();
}
}
}
add_action( 'wp_head', 'wc_page_noindex' );

View File

@ -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' );
}

View File

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

View File

@ -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.

View File

@ -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,
);
/**

View File

@ -0,0 +1,31 @@
<?php
/**
* DownloadPermissionsAdjusterServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
/**
* Service provider for the DownloadPermissionsAdjuster class.
*/
class DownloadPermissionsAdjusterServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
DownloadPermissionsAdjuster::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( DownloadPermissionsAdjuster::class );
}
}

View File

@ -1,6 +1,6 @@
<?php
/**
* Proxies class file.
* ProxiesServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

View File

@ -0,0 +1,160 @@
<?php
/**
* DownloadPermissionsAdjuster class file.
*/
namespace Automattic\WooCommerce\Internal;
use Automattic\WooCommerce\Proxies\LegacyProxy;
defined( 'ABSPATH' ) || exit;
/**
* Class to adjust download permissions on product save.
*/
class DownloadPermissionsAdjuster {
/**
* The downloads data store to use.
*
* @var WC_Data_Store
*/
private $downloads_data_store;
/**
* Class initialization, to be executed when the class is resolved by the container.
*
* @internal
*/
final public function init() {
$this->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;
}
}

View File

@ -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 );
}
}
}

View File

@ -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 );
}

View File

@ -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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/api",
"version": "0.1.0",
"version": "0.1.1",
"author": "Automattic",
"description": "A simple interface for interacting with a WooCommerce installation.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/api/README.md",

View File

@ -1,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' );

View File

@ -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

View File

@ -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' );
// ...

View File

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

View File

@ -9,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,

View File

@ -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 });

View File

@ -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

View File

@ -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?');

View File

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

View File

@ -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;

View File

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

3
tests/e2e/env/.env vendored
View File

@ -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

View File

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

View File

@ -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 ) {

View File

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

View File

@ -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"
}
}
}

View File

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

View File

@ -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 = () => {

View File

@ -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 );

View File

@ -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,
};

View File

@ -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,
};

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-utils",
"version": "0.1.1",
"version": "0.1.2",
"description": "End-To-End (E2E) test utils for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e-utils/README.md",
"repository": {

View File

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

View File

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

View File

@ -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,
};

View File

@ -0,0 +1,113 @@
<?php
/**
* Tests for the WC_Data class.
*
* @package WooCommerce\Tests\Blocks
*/
/**
* Class WC_Test_Blocks_Utils
*/
class WC_Test_Blocks_Utils extends WC_Unit_Test_Case {
/**
* @group block-utils
* Test: has_block_in_page.
*
*/
public function test_has_block_in_page_on_page_with_single_block() {
$page = array(
'name' => 'blocks-page',
'title' => 'Checkout',
'content' => '<!-- wp:woocommerce/checkout {"showOrderNotes":false} --> <div class="wp-block-woocommerce-checkout is-loading"></div> <!-- /wp:woocommerce/checkout -->',
);
$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' => '<!-- wp:shortcode --> [woocommerce_checkout] <!-- /wp:shortcode -->',
);
$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' => '<!-- wp:woocommerce/featured-product {"editMode":false,"productId":17} -->
<!-- wp:button {"align":"center"} -->
<div class="wp-block-button aligncenter"><a class="wp-block-button__link" href="https://blocks.local/product/beanie/">Shop now</a></div>
<!-- /wp:button -->
<!-- /wp:woocommerce/featured-product -->
<!-- wp:heading -->
<h2>test</h2>
<!-- /wp:heading -->',
);
$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' => '<!-- wp:heading --><h2>test1</h2><!-- /wp:heading --><!-- wp:heading --><h1>test2</h1><!-- /wp:heading -->',
);
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' => '<h2>test1</h2>',
'innerContent' => array(
0 => '<h2>test1</h2>',
),
),
1 => array(
'blockName' => 'core/heading',
'attrs' => array(),
'innerBlocks' => array(),
'innerHTML' => '<h1>test2</h1>',
'innerContent' => array(
0 => '<h1>test2</h1>',
),
),
);
$blocks = WC_Blocks_Utils::get_blocks_from_page( 'core/heading', 'cart' );
$this->assertEquals( $expected, $blocks );
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Tests for WC_Comments class.
*/
class WC_Comments_Tests extends \WC_Unit_Test_Case {
/**
* Test get_review_counts_for_product_ids().
*/
public function test_get_review_counts_for_product_ids() {
$product1 = WC_Helper_Product::create_simple_product();
$product2 = WC_Helper_Product::create_simple_product();
$product3 = WC_Helper_Product::create_simple_product();
$expected_review_count = array(
$product1->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 ) );
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Class WC_Emails_Tests.
*/
class WC_Emails_Tests extends \WC_Unit_Test_Case {
/**
* Test that email_header 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_header_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_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 ) );
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Class WC_Shipping_Zone_Data_Store_CPT_Test.
*/
class WC_Shipping_Zone_Data_Store_CPT_Test extends WC_Unit_Test_Case {
/**
* @testdox read() sets properties for normal, non-zero shipping zones.
*/
public function test_read_for_normal_shipping_zones() {
$zone = new WC_Shipping_Zone();
$zone->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 );
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* ClassWithSingleton class file.
*/
// This class is in the root namespace on purpose, since it simulates being a legacy class in the 'includes' directory.
/**
* An example of a class that holds a singleton instance.
*/
class ClassWithLoadMethod {
/**
* @var ClassWithLoadMethod The last instance of the class that has been loaded.
*/
public static $loaded;
/**
* @var array The arguments supplied to 'load'.
*/
public static $loaded_args;
/**
* Load an instance of the class.
*
* @param mixed ...$args Any arguments required by the method.
*
* @return ClassWithLoadMethod The singleton instance of the class.
*/
public static function load( ...$args ) {
self::$loaded = new ClassWithLoadMethod();
self::$loaded_args = $args;
return self::$loaded;
}
}

View File

@ -0,0 +1,358 @@
<?php
/**
* DownloadPermissionsAdjusterTest class file.
*/
namespace Automattic\WooCommerce\Tests\Internal;
use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
/**
* Tests for DownloadPermissionsAdjuster.
*/
class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var DownloadPermissionsAdjuster
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
$this->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;
}
}

View File

@ -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.
*/

View File

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