Merge branch 'trunk' into e2e/e2e-merchant-settings-add-shipping-classes

This commit is contained in:
Rodel Calasagsag 2021-04-04 01:47:33 -07:00 committed by GitHub
commit cb53050892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2321 additions and 670 deletions

View File

@ -21,3 +21,17 @@ jobs:
asset_path: ${{ steps.build.outputs.zip_path }}
asset_name: woocommerce.zip
asset_content_type: application/zip
update-code-reference:
if: github.event.release.prerelease == false && github.event.release.draft == false && github.repository_owner == 'woocommerce'
name: Update Code Reference
needs: build
runs-on: ubuntu-latest
steps:
- name: Invoke Code Reference build and deploy workflow
uses: aurelien-baudet/workflow-dispatch@v2
with:
workflow: GitHub Pages deploy
repo: woocommerce/code-reference
token: ${{ secrets.CUSTOM_GH_TOKEN }}
ref: refs/heads/trunk
inputs: '{ "version": "${{ github.event.release.tag_name }}" }'

74
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: Run CI
on:
push:
branches:
- trunk
- 'release/**'
jobs:
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
timeout-minutes: 15
runs-on: ubuntu-latest
strategy:
fail-fast: false
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

@ -17,6 +17,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 100
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -49,3 +51,7 @@ jobs:
run: |
RUN_CODE_COVERAGE=1 bash ./tests/bin/phpunit.sh
exit 0
- name: Send code coverage to Codecov.
run: |
bash <(curl -s https://codecov.io/bash)

View File

@ -6,25 +6,17 @@ jobs:
name: Code sniff (PHP 7.4, WP Latest)
timeout-minutes: 15
runs-on: ubuntu-latest
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
with:
fetch-depth: 100
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
tools: composer
extensions: mysql
coverage: none
tools: composer, cs2pr
- name: Tool versions
run: |
@ -42,8 +34,9 @@ jobs:
- name: Setup and install composer
run: composer install
- name: Init DB and WP
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 latest
- name: Run code sniff
run: RUN_PHPCS=1 bash ./tests/bin/phpcs.sh
continue-on-error: true
run: ./tests/bin/phpcs.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.after }}"
- name: Show PHPCS results in PR
run: cs2pr ./phpcs-report.xml

20
.github/workflows/stalebot.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: 'Close stale needs-feedback issues'
on:
schedule:
- cron: '0 21 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "As a part of this repositorys maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed."
close-issue-message: 'This issue was closed because it has been 14 days with no activity.'
days-before-issue-stale: 7
days-before-issue-close: 7
days-before-pr-close: -1
only-issue-labels: 'needs feedback'
close-issue-label: "category: can't reproduce"
debug-only: true

View File

@ -1,110 +0,0 @@
version: ~> 1.0
# Specifies that Travis should create builds for trunk and release branches and also tags.
branches:
only:
- trunk
- /^\d+\.\d+(\.\d+)?(-\S*)?$/
- /^release\//
language: php
os:
- linux
dist: xenial
# Test main supported versions of PHP against latest WP.
php:
- "7.0"
- "7.1"
- "7.2"
- "7.3"
- "7.4"
- "8.0"
env:
- WP_VERSION=latest WP_MULTISITE=0
# Additional tests against stable PHP (min version is 7.0)
# and code coverage report.
jobs:
fast_finish: true
include:
- name: "Core E2E Tests"
env: WP_VERSION=latest WP_MULTISITE=0 RUN_E2E=1
install:
- nvm install
- npm install
- composer install --no-dev
script:
- npm run build:assets
- npm run docker:up
- npm run test:e2e
after_script:
- npm run docker:down
- name: "WP Nightly"
php: "7.4"
env: WP_VERSION=nightly WP_MULTISITE=0
- name: "WP Latest - 1"
php: "7.2"
env: WP_VERSION=5.5 WP_MULTISITE=0
- name: "WP Latest - 2"
php: "7.2"
env: WP_VERSION=5.4 WP_MULTISITE=0
- name: "Code Standards"
php: "7.4"
env: WP_VERSION=latest WP_MULTISITE=0 RUN_PHPCS=1
- name: "Code Coverage"
php: "7.4"
env: WP_VERSION=latest WP_MULTISITE=0 RUN_CODE_COVERAGE=1
allow_failures:
- php: "7.4"
env: WP_VERSION=latest WP_MULTISITE=0 RUN_CODE_COVERAGE=1
# Git clone depth
# By default Travis CI clones repositories to a depth of 50 commits. Using a depth of 1 makes this step a bit faster.
git:
depth: 1
# Since Xenial services are not started by default, we need to instruct it below to start.
services:
- mysql
- docker
cache:
directories:
- $HOME/.composer/cache
# Composer 2.0.7 introduced a change that broke the jetpack autoloader in PHP 7.0 - 7.3.
before_install:
- composer self-update 2.0.6
install:
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- |
# Remove Xdebug for a huge performance increase:
if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then
phpenv config-rm xdebug.ini
else
echo "xdebug.ini does not exist"
fi
- composer install
- |
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
- |
# Install WP Test suite:
if [[ ! -z "$WP_VERSION" ]]; then
bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION
fi
script:
- bash tests/bin/phpunit.sh
- bash tests/bin/phpcs.sh
after_script:
- bash tests/bin/travis.sh after

View File

@ -5,7 +5,7 @@
<a href="https://packagist.org/packages/woocommerce/woocommerce"><img src="https://poser.pugx.org/woocommerce/woocommerce/v/stable" alt="Latest Stable Version"></a>
<img src="https://img.shields.io/wordpress/plugin/dt/woocommerce.svg" alt="WordPress.org downloads">
<img src="https://img.shields.io/wordpress/plugin/r/woocommerce.svg" alt="WordPress.org rating">
<a href="https://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=trunk" alt="Build Status"></a>
<a href="https://github.com/woocommerce/woocommerce/actions/workflows/ci.yml"><img src="https://github.com/woocommerce/woocommerce/actions/workflows/ci.yml/badge.svg?branch=trunk" alt="Build Status"></a>
<a href="https://codecov.io/gh/woocommerce/woocommerce"><img src="https://codecov.io/gh/woocommerce/woocommerce/branch/trunk/graph/badge.svg" alt="codecov"></a>
</p>
@ -35,3 +35,9 @@ Support requests in issues on this repository will be closed on sight.
## Contributing to WooCommerce
If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) for more information how you can do this.
<p align="center">
<br/><br/>
Made with 💜 by <a href="https://woocommerce.com/">WooCommerce</a>.<br/>
<a href="https://woocommerce.com/careers/">We're hiring</a>! Come work with us!
</p>

View File

@ -348,6 +348,12 @@ a.button {
.woocommerce,
.woocommerce-page {
&.is-dark-theme {
.select2-dropdown {
color: var(--global--color-dark-gray);
}
}
table.shop_table {
td,
@ -1312,6 +1318,30 @@ a.reset_variations {
}
}
&.woocommerce-lost-password {
.woocommerce {
max-width: var(--responsive--alignwide-width) !important;
padding: 0 !important;
flex-wrap: wrap;
.woocommerce-notices-wrapper {
flex: 1 0 100%;
}
.woocommerce-ResetPassword {
.woocommerce-form-row--first {
float: none;
}
#user_login {
margin-bottom: 10px;
}
}
}
}
table.account-orders-table {
margin-top: 0;
border: 0;
@ -1433,6 +1463,11 @@ a.reset_variations {
}
.woocommerce-cart {
table.woocommerce-cart-form__contents {
thead, tfoot {
text-align: left;
}
}
.post-inner {
padding-top: 0;
@ -2123,6 +2158,10 @@ a.reset_variations {
.woocommerce-table--order-details {
margin-bottom: 2rem;
thead, tfoot {
text-align: left;
}
}
/**

View File

@ -2474,6 +2474,12 @@ a.reset_variations {
margin: 1.5rem 0;
}
}
.woocommerce-ResetPassword {
.woocommerce-form-row--first {
float: none;
}
}
}
/**

View File

@ -72,6 +72,49 @@ jQuery( function( $ ) {
$( this ).selectWoo( select2_args ).addClass( 'enhanced' );
});
function display_result( self, select2_args ) {
select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );
$( self ).selectWoo( select2_args ).addClass( 'enhanced' );
if ( $( self ).data( 'sortable' ) ) {
var $select = $(self);
var $list = $( self ).next( '.select2-container' ).find( 'ul.select2-selection__rendered' );
$list.sortable({
placeholder : 'ui-state-highlight select2-selection__choice',
forcePlaceholderSize: true,
items : 'li:not(.select2-search__field)',
tolerance : 'pointer',
stop: function() {
$( $list.find( '.select2-selection__choice' ).get().reverse() ).each( function() {
var id = $( self ).data( 'data' ).id;
var option = $select.find( 'option[value="' + id + '"]' )[0];
$select.prepend( option );
} );
}
});
// Keep multiselects ordered alphabetically if they are not sortable.
} else if ( $( self ).prop( 'multiple' ) ) {
$( self ).on( 'change', function(){
var $children = $( self ).children();
$children.sort(function(a, b){
var atext = a.text.toLowerCase();
var btext = b.text.toLowerCase();
if ( atext > btext ) {
return 1;
}
if ( atext < btext ) {
return -1;
}
return 0;
});
$( self ).html( $children );
});
}
}
// Ajax product search box
$( ':input.wc-product-search' ).filter( ':not(.enhanced)' ).each( function() {
var select2_args = {
@ -112,46 +155,48 @@ jQuery( function( $ ) {
}
};
select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );
display_result( this, select2_args );
});
// Ajax Page Search.
$( ':input.wc-page-search' ).filter( ':not(.enhanced)' ).each( function() {
var select2_args = {
allowClear: $( this ).data( 'allow_clear' ) ? true : false,
placeholder: $( this ).data( 'placeholder' ),
minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
escapeMarkup: function( m ) {
return m;
},
ajax: {
url: wc_enhanced_select_params.ajax_url,
dataType: 'json',
delay: 250,
data: function( params ) {
return {
term : params.term,
action : $( this ).data( 'action' ) || 'woocommerce_json_search_pages',
security : wc_enhanced_select_params.search_pages_nonce,
exclude : $( this ).data( 'exclude' ),
post_status : $( this ).data( 'post_status' ),
limit : $( this ).data( 'limit' ),
};
},
processResults: function( data ) {
var terms = [];
if ( data ) {
$.each( data, function( id, text ) {
terms.push( { id: id, text: text } );
} );
}
return {
results: terms
};
},
cache: true
}
};
$( this ).selectWoo( select2_args ).addClass( 'enhanced' );
if ( $( this ).data( 'sortable' ) ) {
var $select = $(this);
var $list = $( this ).next( '.select2-container' ).find( 'ul.select2-selection__rendered' );
$list.sortable({
placeholder : 'ui-state-highlight select2-selection__choice',
forcePlaceholderSize: true,
items : 'li:not(.select2-search__field)',
tolerance : 'pointer',
stop: function() {
$( $list.find( '.select2-selection__choice' ).get().reverse() ).each( function() {
var id = $( this ).data( 'data' ).id;
var option = $select.find( 'option[value="' + id + '"]' )[0];
$select.prepend( option );
} );
}
});
// Keep multiselects ordered alphabetically if they are not sortable.
} else if ( $( this ).prop( 'multiple' ) ) {
$( this ).on( 'change', function(){
var $children = $( this ).children();
$children.sort(function(a, b){
var atext = a.text.toLowerCase();
var btext = b.text.toLowerCase();
if ( atext > btext ) {
return 1;
}
if ( atext < btext ) {
return -1;
}
return 0;
});
$( this ).html( $children );
});
}
});
// Ajax customer search boxes

View File

@ -62,6 +62,9 @@
shippingMethod.trigger( 'change:methods' );
shippingMethod.changes = {};
shippingMethod.trigger( 'saved:methods' );
// Overrides the onbeforeunload callback added by settings.js.
window.onbeforeunload = null;
} else {
window.alert( data.strings.save_failed );
}

View File

@ -57,6 +57,28 @@ jQuery( function( $ ) {
$node.removeClass( 'processing' ).unblock();
};
/**
* Removes duplicate notices.
*
* @param {JQuery Object} notices
*/
var remove_duplicate_notices = function( notices ) {
var seen = [];
var new_notices = notices;
notices.each( function( index ) {
var text = $( this ).text();
if ( 'undefined' === typeof seen[ text ] ) {
seen[ text ] = true;
} else {
new_notices.splice( index, 1 );
}
} );
return new_notices;
};
/**
* Update the .woocommerce div with a string of html.
*
@ -67,7 +89,7 @@ jQuery( function( $ ) {
var $html = $.parseHTML( html_str );
var $new_form = $( '.woocommerce-cart-form', $html );
var $new_totals = $( '.cart_totals', $html );
var $notices = $( '.woocommerce-error, .woocommerce-message, .woocommerce-info', $html );
var $notices = remove_duplicate_notices( $( '.woocommerce-error, .woocommerce-message, .woocommerce-info', $html ) );
// No form, cannot do this.
if ( $( '.woocommerce-cart-form' ).length === 0 ) {

View File

@ -1,5 +1,15 @@
== Changelog ==
= 5.2.0 RC 2021-03-30 =
* Update - WooCommerce Admin package 2.1.4. #29520
* Fix - Don't remove existing coupons from order when an invalid REST API request for updating coupons is submitted. #29474
* Fix - Wrong logic for including or excluding the payments step in the list of completed tasks in the onboarding wizard. #29518
**WooCommerce Admin - 2.1.4**
* Fix - Adding New Zealand and Ireland to selective bundle option, previously missed. #6649
= 5.2.0 beta 2021-03-23 =
**WooCommerce**
@ -42,6 +52,8 @@
* Fix - add validation of the posted country codes on checkout. #28849
* Fix - Correctly display pagination arrows on RTL languages. #28523
* Fix - Invalid refund amount error on $0 refund when number of decimals is equal to 0. #27277
* Fix - "Sale" badge misaligned on products when displaying 1 item per row. #29425
* Fix - Revert a replacement of wp_redirect to wp_safe_redirect in WC_Checkout::process_order_payment that caused issues in the default PayPal interface. #29459
* Tweak - Added the Mercado Pago logo into the assets/images folder in order to use it in the payments setup task. #29365
* Tweak - Update the contributor guidelines. #29150
* Tweak - Introduced phone number input validation. #27242
@ -1342,6 +1354,7 @@
* Fix - Don't show duplicated update notifications on Woo Screens. #25828
* Fix - Escape MaxMind database URL. #25682
* Fix - System status report should correctly identify inactive package. #25830
* Fix - "Sale" badge misaligned on products when displaying 1 item per row. #29425
* Dev - Added support for placeholder attribute in quantity inputs. #25418
* Dev - Added `tax_status` and `tax_class` columns to the product meta data lookup table. #25428
* Dev - Introduced `woocommerce_top_rated_widget_args` filter. #25320

View File

@ -14,14 +14,14 @@
],
"require": {
"php": ">=7.0",
"automattic/jetpack-autoloader": "2.9.1",
"automattic/jetpack-autoloader": "2.10.1",
"automattic/jetpack-constants": "1.5.1",
"composer/installers": "~1.7",
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "2.1.3",
"woocommerce/woocommerce-admin": "2.1.4",
"woocommerce/woocommerce-blocks": "4.7.0"
},
"require-dev": {

35
composer.lock generated
View File

@ -4,32 +4,39 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dc5e21e29d4fb70bba776d20112c74f0",
"content-hash": "b1d6d94c8cfae572ab27c288c6865787",
"packages": [
{
"name": "automattic/jetpack-autoloader",
"version": "v2.9.1",
"version": "2.10.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88"
"reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88",
"reference": "d6ca2cc26ad6963e1be19b3338a9e98f40d9bd88",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
"reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1 || ^2.0"
},
"require-dev": {
"automattic/jetpack-changelogger": "^1.1",
"yoast/phpunit-polyfills": "0.2.0"
},
"type": "composer-plugin",
"extra": {
"class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin",
"mirror-repo": "Automattic/jetpack-autoloader"
"mirror-repo": "Automattic/jetpack-autoloader",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-master": "2.10.x-dev"
}
},
"autoload": {
"classmap": [
@ -45,9 +52,9 @@
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
"source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.9.1"
"source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1"
},
"time": "2021-02-05T19:07:06+00:00"
"time": "2021-03-30T15:15:59+00:00"
},
{
"name": "automattic/jetpack-constants",
@ -523,16 +530,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "2.1.3",
"version": "2.1.4",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "60f4297838569341ae88738a4a8a8090889faaac"
"reference": "f992b8c8664e72b00ee7283ba1d34e74e4b67ab0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/60f4297838569341ae88738a4a8a8090889faaac",
"reference": "60f4297838569341ae88738a4a8a8090889faaac",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/f992b8c8664e72b00ee7283ba1d34e74e4b67ab0",
"reference": "f992b8c8664e72b00ee7283ba1d34e74e4b67ab0",
"shasum": ""
},
"require": {
@ -566,9 +573,9 @@
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.1.3"
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.1.4"
},
"time": "2021-03-15T04:42:40+00:00"
"time": "2021-03-29T11:59:33+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",

View File

@ -1633,6 +1633,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
continue;
}
$saved_rate_ids[] = $tax->get_rate_id();
$tax->set_rate( $tax->get_rate_id() );
$tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 );
$tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) );
$tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 );

View File

@ -696,6 +696,7 @@ abstract class WC_Settings_API {
);
$data = wp_parse_args( $data, $defaults );
$value = $this->get_option( $key );
ob_start();
?>
@ -708,7 +709,15 @@ abstract class WC_Settings_API {
<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
<select class="select <?php echo esc_attr( $data['class'] ); ?>" name="<?php echo esc_attr( $field_key ); ?>" id="<?php echo esc_attr( $field_key ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php disabled( $data['disabled'], true ); ?> <?php echo $this->get_custom_attribute_html( $data ); // WPCS: XSS ok. ?>>
<?php foreach ( (array) $data['options'] as $option_key => $option_value ) : ?>
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $this->get_option( $key ) ) ); ?>><?php echo esc_html( $option_value ); ?></option>
<?php if ( is_array( $option_value ) ) : ?>
<optgroup label="<?php echo esc_attr( $option_key ); ?>">
<?php foreach ( $option_value as $option_key_inner => $option_value_inner ) : ?>
<option value="<?php echo esc_attr( $option_key_inner ); ?>" <?php selected( (string) $option_key_inner, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value_inner ); ?></option>
<?php endforeach; ?>
</optgroup>
<?php else : ?>
<option value="<?php echo esc_attr( $option_key ); ?>" <?php selected( (string) $option_key, esc_attr( $value ) ); ?>><?php echo esc_html( $option_value ); ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
<?php echo $this->get_description_html( $data ); // WPCS: XSS ok. ?>

View File

@ -145,6 +145,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
'search_products_nonce' => wp_create_nonce( 'search-products' ),
'search_customers_nonce' => wp_create_nonce( 'search-customers' ),
'search_categories_nonce' => wp_create_nonce( 'search-categories' ),
'search_pages_nonce' => wp_create_nonce( 'search-pages' ),
)
);

View File

@ -178,7 +178,7 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
}
// payments can't be used when woocommerce-payments exists and country is US.
if ( $is_woo_payment_installed || 'US' === $country ) {
if ( $is_woo_payment_installed && 'US' === $country ) {
unset( $this->tasks['payments'] );
}

View File

@ -579,6 +579,47 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
<?php
break;
case 'single_select_page_with_search':
$option_value = $value['value'];
$page = get_post( $option_value );
if ( ! is_null( $page ) ) {
$page = get_post( $option_value );
$option_display_name = sprintf(
/* translators: 1: page name 2: page ID */
__( '%1$s (ID: %2$s)', 'woocommerce' ),
$page->post_title,
$option_value
);
}
?>
<tr valign="top" class="single_select_page">
<th scope="row" class="titledesc">
<label for="<?php echo esc_attr( $value['id'] ); ?>"><?php echo esc_html( $value['title'] ); ?> <?php echo $tooltip_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></label>
</th>
<td class="forminp forminp-<?php echo esc_attr( sanitize_title( $value['type'] ) ); ?>">
<select
name="<?php echo esc_attr( $value['id'] ); ?>"
id="<?php echo esc_attr( $value['id'] ); ?>"
style="<?php echo esc_attr( $value['css'] ); ?>"
class="<?php echo esc_attr( $value['class'] ); ?>"
<?php echo implode( ' ', $custom_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
data-placeholder="<?php esc_attr_e( 'Search for a page&hellip;', 'woocommerce' ); ?>"
data-allow_clear="true"
data-exclude="<?php echo wc_esc_json( wp_json_encode( $value['args']['exclude'] ) ); ?>"
>
<option value=""></option>
<?php if ( ! is_null( $page ) ) { ?>
<option value="<?php echo esc_attr( $option_value ); ?>" selected="selected">
<?php echo wp_strip_all_tags( $option_display_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</option>
<?php } ?>
</select> <?php echo $description; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</td>
</tr>
<?php
break;
// Single country selects.
case 'single_select_country':
$country_setting = (string) $value['value'];

View File

@ -15,7 +15,7 @@ if ( class_exists( 'WC_Admin_List_Table_Orders', false ) ) {
}
if ( ! class_exists( 'WC_Admin_List_Table', false ) ) {
include_once __DIR__ . '/abstract-class-wc-admin-list-table.php';
include_once __DIR__ . '/abstract-class-wc-admin-list-table.php';
}
/**
@ -765,7 +765,7 @@ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {
}
?>
<select class="wc-customer-search" name="_customer_user" data-placeholder="<?php esc_attr_e( 'Filter by registered customer', 'woocommerce' ); ?>" data-allow_clear="true">
<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?><option>
<option value="<?php echo esc_attr( $user_id ); ?>" selected="selected"><?php echo htmlspecialchars( wp_kses_post( $user_string ) ); // htmlspecialchars to prevent XSS when rendered by selectWoo. ?></option>
</select>
<?php
}

View File

@ -148,7 +148,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
}
/**
* Render columm: thumb.
* Render column: thumb.
*/
protected function render_thumb_column() {
echo '<a href="' . esc_url( get_edit_post_link( $this->object->get_id() ) ) . '">' . $this->object->get_image( 'thumbnail' ) . '</a>'; // WPCS: XSS ok.
@ -203,21 +203,21 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
}
/**
* Render columm: sku.
* Render column: sku.
*/
protected function render_sku_column() {
echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : '<span class="na">&ndash;</span>';
}
/**
* Render columm: price.
* Render column: price.
*/
protected function render_price_column() {
echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : '<span class="na">&ndash;</span>';
}
/**
* Render columm: product_cat.
* Render column: product_cat.
*/
protected function render_product_cat_column() {
$terms = get_the_terms( $this->object->get_id(), 'product_cat' );
@ -234,7 +234,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
}
/**
* Render columm: product_tag.
* Render column: product_tag.
*/
protected function render_product_tag_column() {
$terms = get_the_terms( $this->object->get_id(), 'product_tag' );
@ -251,7 +251,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
}
/**
* Render columm: featured.
* Render column: featured.
*/
protected function render_featured_column() {
$url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' );
@ -265,7 +265,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
}
/**
* Render columm: is_in_stock.
* Render column: is_in_stock.
*/
protected function render_is_in_stock_column() {
if ( $this->object->is_on_backorder() ) {
@ -337,7 +337,7 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
?>
<select class="wc-category-search" name="product_cat" data-placeholder="<?php esc_attr_e( 'Filter by category', 'woocommerce' ); ?>" data-allow_clear="true">
<?php if ( $current_category_slug && $current_category ) : ?>
<option value="<?php echo esc_attr( $current_category_slug ); ?>" selected="selected"><?php echo esc_html( htmlspecialchars( wp_kses_post( $current_category->name ) ) ); ?><option>
<option value="<?php echo esc_attr( $current_category_slug ); ?>" selected="selected"><?php echo esc_html( htmlspecialchars( wp_kses_post( $current_category->name ) ) ); ?></option>
<?php endif; ?>
</select>
<?php

View File

@ -74,9 +74,9 @@ class WC_Settings_Advanced extends WC_Settings_Page {
/* Translators: %s Page contents. */
'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) ),
'id' => 'woocommerce_cart_page_id',
'type' => 'single_select_page',
'type' => 'single_select_page_with_search',
'default' => '',
'class' => 'wc-enhanced-select-nostd',
'class' => 'wc-page-search',
'css' => 'min-width:300px;',
'args' => array(
'exclude' =>
@ -94,9 +94,9 @@ class WC_Settings_Advanced extends WC_Settings_Page {
/* Translators: %s Page contents. */
'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) ),
'id' => 'woocommerce_checkout_page_id',
'type' => 'single_select_page',
'default' => '',
'class' => 'wc-enhanced-select-nostd',
'type' => 'single_select_page_with_search',
'default' => wc_get_page_id( 'checkout' ),
'class' => 'wc-page-search',
'css' => 'min-width:300px;',
'args' => array(
'exclude' =>
@ -114,9 +114,9 @@ class WC_Settings_Advanced extends WC_Settings_Page {
/* Translators: %s Page contents. */
'desc' => sprintf( __( 'Page contents: [%s]', 'woocommerce' ), apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) ),
'id' => 'woocommerce_myaccount_page_id',
'type' => 'single_select_page',
'type' => 'single_select_page_with_search',
'default' => '',
'class' => 'wc-enhanced-select-nostd',
'class' => 'wc-page-search',
'css' => 'min-width:300px;',
'args' => array(
'exclude' =>
@ -134,9 +134,9 @@ class WC_Settings_Advanced extends WC_Settings_Page {
'desc' => __( 'If you define a "Terms" page the customer will be asked if they accept them when checking out.', 'woocommerce' ),
'id' => 'woocommerce_terms_page_id',
'default' => '',
'class' => 'wc-enhanced-select-nostd',
'class' => 'wc-page-search',
'css' => 'min-width:300px;',
'type' => 'single_select_page',
'type' => 'single_select_page_with_search',
'args' => array( 'exclude' => wc_get_page_id( 'checkout' ) ),
'desc_tip' => true,
'autoload' => false,

View File

@ -207,6 +207,10 @@ class WC_Settings_Emails extends WC_Settings_Page {
'autoload' => false,
),
array(
'type' => 'sectionend',
'id' => 'email_merchant_notes',
),
)
);

View File

@ -2,17 +2,13 @@
/**
* WooCommerce Integration Settings
*
* @author WooThemes
* @category Admin
* @package WooCommerce\Admin
* @version 2.1.0
* @package WooCommerce\Admin
* @version 2.1.0
*/
use Automattic\Jetpack\Constants;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Settings_Integrations', false ) ) :
@ -50,7 +46,7 @@ if ( ! class_exists( 'WC_Settings_Integrations', false ) ) :
$current_section = current( $integrations )->id;
}
if ( sizeof( $integrations ) > 1 ) {
if ( count( $integrations ) > 1 ) {
foreach ( $integrations as $integration ) {
$title = empty( $integration->method_title ) ? ucfirst( $integration->id ) : $integration->method_title;
$sections[ strtolower( $integration->id ) ] = esc_html( $title );

View File

@ -2,14 +2,12 @@
/**
* WooCommerce Settings Page/Tab
*
* @author WooThemes
* @category Admin
* @package WooCommerce\Admin
* @version 2.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'WC_Settings_Page', false ) ) :
@ -66,7 +64,7 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
/**
* Add this page to settings.
*
* @param array $pages
* @param array $pages The pages array to add this page to.
*
* @return mixed
*/
@ -102,7 +100,7 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
$sections = $this->get_sections();
if ( empty( $sections ) || 1 === sizeof( $sections ) ) {
if ( empty( $sections ) || 1 === count( $sections ) ) {
return;
}
@ -111,7 +109,8 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
$array_keys = array_keys( $sections );
foreach ( $sections as $id => $label ) {
echo '<li><a href="' . admin_url( 'admin.php?page=wc-settings&tab=' . $this->id . '&section=' . sanitize_title( $id ) ) . '" class="' . ( $current_section == $id ? 'current' : '' ) . '">' . $label . '</a> ' . ( end( $array_keys ) == $id ? '' : '|' ) . ' </li>';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<li><a href="' . admin_url( 'admin.php?page=wc-settings&tab=' . $this->id . '&section=' . sanitize_title( $id ) ) . '" class="' . ( $current_section === $id ? 'current' : '' ) . '">' . esc_html( $label ) . '</a> ' . ( end( $array_keys ) === $id ? '' : '|' ) . ' </li>';
}
echo '</ul><br class="clear" />';

View File

@ -2,8 +2,6 @@
/**
* WooCommerce Tax Settings
*
* @author WooThemes
* @category Admin
* @package WooCommerce\Admin
* @version 2.1.0
*/
@ -66,6 +64,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
$tax_classes = WC_Tax::get_tax_classes();
foreach ( $tax_classes as $class ) {
/* translators: $s tax rate section name */
$sections[ sanitize_title( $class ) ] = sprintf( __( '%s rates', 'woocommerce' ), $class );
}
@ -95,7 +94,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
$tax_classes = WC_Tax::get_tax_class_slugs();
if ( 'standard' === $current_section || in_array( $current_section, $tax_classes, true ) ) {
if ( 'standard' === $current_section || in_array( $current_section, array_filter( $tax_classes ), true ) ) {
$this->output_tax_rates();
} else {
$settings = $this->get_settings();
@ -149,7 +148,19 @@ class WC_Settings_Tax extends WC_Settings_Page {
}
foreach ( $added as $name ) {
WC_Tax::create_tax_class( $name );
$tax_class = WC_Tax::create_tax_class( $name );
// Display any error that could be triggered while creating tax classes.
if ( is_wp_error( $tax_class ) ) {
WC_Admin_Settings::add_error(
sprintf(
/* translators: 1: tax class name 2: error message */
esc_html__( 'Additional tax class "%1$s" couldn\'t be saved. %2$s.', 'woocommerce' ),
esc_html( $name ),
$tax_class->get_error_message()
)
);
}
}
return null;
@ -201,6 +212,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
'wc_tax_nonce' => wp_create_nonce( 'wc_tax_nonce-class:' . $current_class ),
'base_url' => $base_url,
'rates' => array_values( WC_Tax::get_rates_for_tax_class( $current_class ) ),
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
'page' => ! empty( $_GET['p'] ) ? absint( $_GET['p'] ) : 1,
'limit' => 100,
'countries' => $countries,
@ -278,6 +290,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
'tax_rate_priority',
);
// phpcs:disable WordPress.Security.NonceVerification.Missing
foreach ( $tax_rate_keys as $tax_rate_key ) {
if ( isset( $_POST[ $tax_rate_key ], $_POST[ $tax_rate_key ][ $key ] ) ) {
$tax_rate[ $tax_rate_key ] = wc_clean( wp_unslash( $_POST[ $tax_rate_key ][ $key ] ) );
@ -288,6 +301,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
$tax_rate['tax_rate_shipping'] = isset( $_POST['tax_rate_shipping'][ $key ] ) ? 1 : 0;
$tax_rate['tax_rate_order'] = $order;
$tax_rate['tax_rate_class'] = $class;
// phpcs:enable WordPress.Security.NonceVerification.Missing
return $tax_rate;
}
@ -298,7 +312,8 @@ class WC_Settings_Tax extends WC_Settings_Page {
public function save_tax_rates() {
global $wpdb;
$current_class = sanitize_title( $this->get_current_tax_class() );
$current_class = sanitize_title( $this->get_current_tax_class() );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing
$posted_countries = wc_clean( wp_unslash( $_POST['tax_rate_country'] ) );
// get the tax rate id of the first submited row.
@ -310,13 +325,14 @@ class WC_Settings_Tax extends WC_Settings_Page {
$index = isset( $tax_rate_order ) ? $tax_rate_order : 0;
// Loop posted fields.
// phpcs:disable WordPress.Security.NonceVerification.Missing
foreach ( $posted_countries as $key => $value ) {
$mode = ( 0 === strpos( $key, 'new-' ) ) ? 'insert' : 'update';
$tax_rate = $this->get_posted_tax_rate( $key, $index ++, $current_class );
if ( 'insert' === $mode ) {
$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
} elseif ( 1 === absint( $_POST['remove_tax_rate'][ $key ] ) ) {
} elseif ( isset( $_POST['remove_tax_rate'][ $key ] ) && 1 === absint( $_POST['remove_tax_rate'][ $key ] ) ) {
$tax_rate_id = absint( $key );
WC_Tax::_delete_tax_rate( $tax_rate_id );
continue;
@ -332,6 +348,7 @@ class WC_Settings_Tax extends WC_Settings_Page {
WC_Tax::_update_tax_rate_cities( $tax_rate_id, wc_clean( wp_unslash( $_POST['tax_rate_city'][ $key ] ) ) );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
}

View File

@ -514,7 +514,7 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, Cons
<tbody>
<tr>
<td data-export-label="WC Database Version"><?php esc_html_e( 'WooCommerce database version', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'The version of WooCommerce that the database is formatted for. This should be the same as your WooCommerce version.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'The database version for WooCommerce. Note that it may not match WooCommerce core version and that is normal.', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
<td><?php echo esc_html( $database['wc_database_version'] ); ?></td>
</tr>
<tr>

View File

@ -155,6 +155,7 @@ class WC_AJAX {
'json_search_downloadable_products_and_variations',
'json_search_customers',
'json_search_categories',
'json_search_pages',
'term_ordering',
'product_ordering',
'refund_line_items',
@ -794,10 +795,14 @@ class WC_AJAX {
$loop = intval( $_POST['loop'] );
$file_counter = 0;
$order = wc_get_order( $order_id );
$items = $order->get_items();
foreach ( $product_ids as $product_id ) {
$product = wc_get_product( $product_id );
$files = $product->get_downloads();
foreach ( $items as $item ) {
$product = $item->get_product();
if ( ! in_array( $product->get_id(), $product_ids, true ) ) {
continue;
}
$files = $product->get_downloads();
if ( ! $order->get_billing_email() ) {
wp_die();
@ -805,7 +810,7 @@ class WC_AJAX {
if ( ! empty( $files ) ) {
foreach ( $files as $download_id => $file ) {
$inserted_id = wc_downloadable_file_permission( $download_id, $product_id, $order );
$inserted_id = wc_downloadable_file_permission( $download_id, $product_id, $order, $item->get_quantity(), $item );
if ( $inserted_id ) {
$download = new WC_Customer_Download( $inserted_id );
$loop ++;
@ -1766,6 +1771,47 @@ class WC_AJAX {
wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) );
}
/**
* Ajax request handling for page searching.
*/
public static function json_search_pages() {
ob_start();
check_ajax_referer( 'search-pages', 'security' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( -1 );
}
$search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
$limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : -1;
$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
$args = array(
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'posts_per_page' => $limit,
'post_type' => 'page',
'post_status' => array( 'publish', 'private', 'draft' ),
's' => $search_text,
'post__not_in' => $exclude_ids,
);
$search_results_query = new WP_Query( $args );
$pages_results = array();
foreach ( $search_results_query->get_posts() as $post ) {
$pages_results[ $post->ID ] = sprintf(
/* translators: 1: page name 2: page ID */
__( '%1$s (ID: %2$s)', 'woocommerce' ),
get_the_title( $post ),
$post->ID
);
}
wp_send_json( apply_filters( 'woocommerce_json_search_found_pages', $pages_results ) );
}
/**
* Ajax request handling for categories ordering.
*/

View File

@ -175,6 +175,10 @@ final class WC_Cart_Session {
if ( $update_cart_session || is_null( WC()->session->get( 'cart_totals', null ) ) ) {
WC()->session->set( 'cart', $this->get_cart_for_session() );
$this->cart->calculate_totals();
if ( $merge_saved_cart ) {
$this->persistent_cart_update();
}
}
// If this is a re-order, redirect to the cart page to get rid of the `order_again` query string.

View File

@ -977,7 +977,8 @@ class WC_Checkout {
$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
if ( ! is_ajax() ) {
wp_safe_redirect( $result['redirect'] );
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
wp_redirect( $result['redirect'] );
exit;
}

View File

@ -51,6 +51,9 @@ class WC_Comments {
// Set comment type.
add_action( 'preprocess_comment', array( __CLASS__, 'update_comment_type' ), 1 );
// Validate product reviews if requires verified owners.
add_action( 'pre_comment_on_post', array( __CLASS__, 'validate_product_review_verified_owners' ) );
}
/**
@ -444,6 +447,36 @@ class WC_Comments {
return $comment_data;
}
/**
* Validate product reviews if requires a verified owner.
*
* @param int $comment_post_id Post ID.
*/
public static function validate_product_review_verified_owners( $comment_post_id ) {
// Only validate if option is enabled.
if ( 'yes' !== get_option( 'woocommerce_review_rating_verification_required' ) ) {
return;
}
// Validate only products.
if ( 'product' !== get_post_type( $comment_post_id ) ) {
return;
}
// Skip if is a verified owner.
if ( wc_customer_bought_product( '', get_current_user_id(), $comment_post_id ) ) {
return;
}
wp_die(
esc_html__( 'Only logged in customers who have purchased this product may leave a review.', 'woocommerce' ),
esc_html__( 'Reviews can only be left by "verified owners"', 'woocommerce' ),
array(
'code' => 403,
)
);
}
/**
* Determines if a comment is of the default type.
*

View File

@ -449,7 +449,19 @@ class WC_Customer extends WC_Legacy_Customer {
* @return array
*/
public function get_billing( $context = 'view' ) {
return $this->get_prop( 'billing', $context );
$value = null;
$prop = 'billing';
if ( array_key_exists( $prop, $this->data ) ) {
$changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
$value = array_merge( $this->data[ $prop ], $changes );
if ( 'view' === $context ) {
$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
}
}
return $value;
}
/**
@ -580,7 +592,19 @@ class WC_Customer extends WC_Legacy_Customer {
* @return array
*/
public function get_shipping( $context = 'view' ) {
return $this->get_prop( 'shipping', $context );
$value = null;
$prop = 'shipping';
if ( array_key_exists( $prop, $this->data ) ) {
$changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array();
$value = array_merge( $this->data[ $prop ], $changes );
if ( 'view' === $context ) {
$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
}
}
return $value;
}
/**

View File

@ -75,7 +75,7 @@ class WC_Session_Handler extends WC_Session {
add_action( 'wp_logout', array( $this, 'destroy_session' ) );
if ( ! is_user_logged_in() ) {
add_filter( 'nonce_user_logged_out', array( $this, 'nonce_user_logged_out' ) );
add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 );
}
}
@ -187,6 +187,25 @@ class WC_Session_Handler extends WC_Session {
return $customer_id;
}
/**
* Get session unique ID for requests if session is initialized or user ID if logged in.
* Introduced to help with unit tests.
*
* @since 5.3.0
* @return string
*/
public function get_customer_unique_id() {
$customer_id = '';
if ( $this->has_session() && $this->_customer_id ) {
$customer_id = $this->_customer_id;
} elseif ( is_user_logged_in() ) {
$customer_id = (string) get_current_user_id();
}
return $customer_id;
}
/**
* Get the session cookie, if set. Otherwise return false.
*
@ -288,13 +307,33 @@ class WC_Session_Handler extends WC_Session {
/**
* When a user is logged out, ensure they have a unique nonce by using the customer/session ID.
*
* @deprecated 5.3.0
* @param int $uid User ID.
* @return string
* @return int|string
*/
public function nonce_user_logged_out( $uid ) {
wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' );
return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
}
/**
* When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID.
* This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called.
*
* @since 5.3.0
* @param int $uid User ID.
* @param string $action The nonce action.
* @return int|string
*/
public function maybe_update_nonce_user_logged_out( $uid, $action ) {
if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) {
return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid;
}
return $uid;
}
/**
* Cleanup session data from the database and clear caches.
*/

View File

@ -198,7 +198,7 @@ class WC_Structured_Data {
$markup = array(
'@type' => 'Product',
'@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist.
'name' => $product->get_name(),
'name' => wp_kses_post( $product->get_name() ),
'url' => $permalink,
'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ),
);
@ -477,7 +477,7 @@ class WC_Structured_Data {
),
'itemOffered' => array(
'@type' => 'Product',
'name' => apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ),
'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ),
'sku' => $product_exists ? $product->get_sku() : '',
'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),

View File

@ -815,6 +815,7 @@ class WC_Tax {
$existing = self::get_tax_classes();
$existing_slugs = self::get_tax_class_slugs();
$name = wc_clean( $name );
if ( in_array( $name, $existing, true ) ) {
return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) );
@ -824,6 +825,11 @@ class WC_Tax {
$slug = sanitize_title( $name );
}
// Stop if there's no slug.
if ( ! $slug ) {
return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) );
}
if ( in_array( $slug, $existing_slugs, true ) ) {
return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) );
}

View File

@ -4,8 +4,6 @@
*
* Handles requests to the /taxes endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
@ -40,67 +38,79 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
* Register the routes for taxes.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
@ -200,7 +210,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
public function get_items( $request ) {
global $wpdb;
$prepared_args = array();
$prepared_args = array();
$prepared_args['order'] = $request['order'];
$prepared_args['number'] = $request['per_page'];
if ( ! empty( $request['offset'] ) ) {
@ -208,9 +218,10 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$orderby_possibles = array(
'id' => 'tax_rate_id',
'order' => 'tax_rate_order',
$orderby_possibles = array(
'id' => 'tax_rate_id',
'order' => 'tax_rate_order',
'priority' => 'tax_rate_priority',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
$prepared_args['class'] = $request['class'];
@ -223,30 +234,42 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
*/
$prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request );
$query = "
$orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] );
$query = "
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rates
WHERE 1 = 1
%s
ORDER BY {$orderby}
LIMIT %%d, %%d
";
$wpdb_prepare_args = array(
$prepared_args['offset'],
$prepared_args['number'],
);
// Filter by tax class.
if ( ! empty( $prepared_args['class'] ) ) {
if ( empty( $prepared_args['class'] ) ) {
$query = sprintf( $query, '' );
} else {
$class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : '';
$query .= " AND tax_rate_class = '$class'";
array_unshift( $wpdb_prepare_args, $class );
$query = sprintf( $query, 'WHERE tax_rate_class = %s' );
}
// Order tax rates.
$order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) );
// Pagination.
$pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] );
// Query taxes.
$results = $wpdb->get_results( $query . $order_by . $pagination );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results(
$wpdb->prepare(
$query,
$wpdb_prepare_args
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$taxes = array();
foreach ( $results as $tax ) {
$data = $this->prepare_item_for_response( $tax, $request );
$data = $this->prepare_item_for_response( $tax, $request );
$taxes[] = $this->prepare_response_for_collection( $data );
}
@ -254,10 +277,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
// Query only for ids.
$wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query );
$wpdb->get_results(
$wpdb->prepare(
$query,
$wpdb_prepare_args
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
// Calculate totals.
$total_taxes = (int) $wpdb->num_rows;
@ -287,13 +318,13 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
* Take tax data from the request and return the updated or newly created rate.
*
* @param WP_REST_Request $request Full details about the request.
* @param stdClass|null $current Existing tax object.
* @param stdClass|null $current Existing tax object.
* @return object
*/
protected function create_or_update_tax( $request, $current = null ) {
$id = absint( isset( $request['id'] ) ? $request['id'] : 0 );
$data = array();
$fields = array(
$id = absint( isset( $request['id'] ) ? $request['id'] : 0 );
$data = array();
$fields = array(
'tax_rate_country',
'tax_rate_state',
'tax_rate',
@ -321,25 +352,25 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
// Add to data array.
switch ( $key ) {
case 'tax_rate_priority' :
case 'tax_rate_compound' :
case 'tax_rate_shipping' :
case 'tax_rate_order' :
case 'tax_rate_priority':
case 'tax_rate_compound':
case 'tax_rate_shipping':
case 'tax_rate_order':
$data[ $field ] = absint( $request[ $key ] );
break;
case 'tax_rate_class' :
case 'tax_rate_class':
$data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : '';
break;
default :
default:
$data[ $field ] = wc_clean( $request[ $key ] );
break;
}
}
if ( $id ) {
WC_Tax::_update_tax_rate( $id, $data );
} else {
if ( ! $id ) {
$id = WC_Tax::_insert_tax_rate( $data );
} elseif ( $data ) {
WC_Tax::_update_tax_rate( $id, $data );
}
// Add locales.
@ -487,13 +518,12 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
/**
* Prepare a single tax output for response.
*
* @param stdClass $tax Tax object.
* @param stdClass $tax Tax object.
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $tax, $request ) {
global $wpdb;
$id = (int) $tax->tax_rate_id;
$data = array(
'id' => $id,
@ -510,18 +540,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard',
);
// Get locales from a tax rate.
$locales = $wpdb->get_results( $wpdb->prepare( "
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
", $id ) );
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
$data[ $locale->location_type ] = $locale->location_code;
}
}
$data = $this->add_tax_rate_locales( $data, $tax );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
@ -550,7 +569,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
*/
protected function prepare_links( $tax ) {
$links = array(
'self' => array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ),
),
'collection' => array(
@ -561,6 +580,38 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
return $links;
}
/**
* Add tax rate locales to the response array.
*
* @param array $data Response data.
* @param stdClass $tax Tax object.
*
* @return array
*/
protected function add_tax_rate_locales( $data, $tax ) {
global $wpdb;
// Get locales from a tax rate.
$locales = $wpdb->get_results(
$wpdb->prepare(
"
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
",
$tax->tax_rate_id
)
);
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
$data[ $locale->location_type ] = $locale->location_code;
}
}
return $data;
}
/**
* Get the Taxes schema, conforming to JSON Schema.
*
@ -572,18 +623,18 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'title' => 'tax',
'type' => 'object',
'properties' => array(
'id' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'country' => array(
'country' => array(
'description' => __( 'Country ISO 3166 code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'state' => array(
'description' => __( 'State code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
@ -593,17 +644,17 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'rate' => array(
'rate' => array(
'description' => __( 'Tax rate.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'name' => array(
'description' => __( 'Tax rate name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
@ -626,12 +677,12 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
'default' => true,
'context' => array( 'view', 'edit' ),
),
'order' => array(
'order' => array(
'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'class' => array(
'class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'default' => 'standard',
@ -654,54 +705,55 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
$params['context'] = $this->get_context_param();
$params['context']['default'] = 'view';
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'enum' => array( 'asc', 'desc' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
$params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'enum' => array( 'asc', 'desc' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'default' => 'order',
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'enum' => array(
$params['orderby'] = array(
'default' => 'order',
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'enum' => array(
'id',
'order',
'priority',
),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['class'] = array(
'description' => __( 'Sort by tax class.', 'woocommerce' ),
'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
'sanitize_callback' => 'sanitize_title',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
$params['class'] = array(
'description' => __( 'Sort by tax class.', 'woocommerce' ),
'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
'sanitize_callback' => 'sanitize_title',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;

View File

@ -34,31 +34,58 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V2_Controller {
* @return bool
*/
protected function calculate_coupons( $request, $order ) {
if ( ! isset( $request['coupon_lines'] ) || ! is_array( $request['coupon_lines'] ) ) {
if ( ! isset( $request['coupon_lines'] ) ) {
return false;
}
// Remove all coupons first to ensure calculation is correct.
foreach ( $order->get_items( 'coupon' ) as $coupon ) {
$order->remove_coupon( $coupon->get_code() );
}
// Validate input and at the same time store the processed coupon codes to apply.
$coupon_codes = array();
$discounts = new WC_Discounts( $order );
$current_order_coupons = array_values( $order->get_coupons() );
$current_order_coupon_codes = array_map(
function( $coupon ) {
return $coupon->get_code();
},
$current_order_coupons
);
foreach ( $request['coupon_lines'] as $item ) {
if ( is_array( $item ) ) {
if ( ! empty( $item['id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 );
}
if ( ! empty( $item['id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 );
}
if ( empty( $item['code'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
}
if ( empty( $item['code'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
}
$results = $order->apply_coupon( wc_clean( $item['code'] ) );
$coupon_code = wc_format_coupon_code( wc_clean( $item['code'] ) );
$coupon = new WC_Coupon( $coupon_code );
if ( is_wp_error( $results ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 );
// Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons.
if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) {
$check_result = $discounts->is_coupon_valid( $coupon );
if ( is_wp_error( $check_result ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 );
}
}
$coupon_codes[] = $coupon_code;
}
// Remove all coupons first to ensure calculation is correct.
foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) {
$order->remove_coupon( $existing_coupon->get_code() );
}
// Apply the coupons.
foreach ( $coupon_codes as $new_coupon ) {
$results = $order->apply_coupon( $new_coupon );
if ( is_wp_error( $results ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 );
}
}
return true;

View File

@ -24,4 +24,118 @@ class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Add tax rate locales to the response array.
*
* @param array $data Response data.
* @param stdClass $tax Tax object.
*
* @return array
*/
protected function add_tax_rate_locales( $data, $tax ) {
global $wpdb;
$data = parent::add_tax_rate_locales( $data, $tax );
$data['postcodes'] = array();
$data['cities'] = array();
// Get locales from a tax rate.
$locales = $wpdb->get_results(
$wpdb->prepare(
"
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
",
$tax->tax_rate_id
)
);
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
if ( 'postcode' === $locale->location_type ) {
$data['postcodes'][] = $locale->location_code;
} elseif ( 'city' === $locale->location_type ) {
$data['cities'][] = $locale->location_code;
}
}
}
return $data;
}
/**
* Get the taxes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['postcodes'] = array(
'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['cities'] = array(
'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['postcode']['description'] =
__( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' );
$schema['properties']['city']['description'] =
__( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' );
return $schema;
}
/**
* Create a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response The response, or an error.
*/
public function create_item( $request ) {
$this->adjust_cities_and_postcodes( $request );
return parent::create_item( $request );
}
/**
* Update a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response The response, or an error.
*/
public function update_item( $request ) {
$this->adjust_cities_and_postcodes( $request );
return parent::update_item( $request );
}
/**
* Convert array "cities" and "postcodes" parameters
* into semicolon-separated strings "city" and "postcode".
*
* @param WP_REST_Request $request The request to adjust.
*/
private function adjust_cities_and_postcodes( &$request ) {
if ( isset( $request['cities'] ) ) {
$request['city'] = join( ';', $request['cities'] );
}
if ( isset( $request['postcodes'] ) ) {
$request['postcode'] = join( ';', $request['postcodes'] );
}
}
}

View File

@ -84,7 +84,7 @@ class WC_Shortcode_Products {
* Get shortcode type.
*
* @since 3.2.0
* @return array
* @return string
*/
public function get_type() {
return $this->type;

View File

@ -366,9 +366,10 @@ function wc_orders_count( $status ) {
* @param int|WC_Product $product Product instance or ID.
* @param WC_Order $order Order data.
* @param int $qty Quantity purchased.
* @param WC_Order_Item $item Item of the order.
* @return int|bool insert id or false on failure.
*/
function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1 ) {
function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) {
if ( is_numeric( $product ) ) {
$product = wc_get_product( $product );
}
@ -390,7 +391,7 @@ function wc_downloadable_file_permission( $download_id, $product, $order, $qty =
$download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) );
}
$download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty );
$download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item );
return $download->save();
}
@ -420,7 +421,7 @@ function wc_downloadable_product_permissions( $order_id, $force = false ) {
$downloads = $product->get_downloads();
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity() );
wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item );
}
}
}

View File

@ -8,7 +8,7 @@
"psr/container": "^1.0"
},
"require-dev": {
"league/container": "3.3.3"
"league/container": "3.3.5"
},
"config": {
"platform": {

19
lib/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": "df548645b5c00d585705cd10c6ffd3f7",
"content-hash": "9ae561875707d59bc392f6329d4f565a",
"packages": [
{
"name": "psr/container",
@ -59,21 +59,21 @@
"packages-dev": [
{
"name": "league/container",
"version": "3.3.3",
"version": "3.3.5",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05"
"reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/7dc67bdf89efc338e674863c0ea70a63efe4de05",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05",
"url": "https://api.github.com/repos/thephpleague/container/zipball/048ab87810f508dbedbcb7ae941b606eb8ee353b",
"reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/container": "^1.0"
"psr/container": "^1.0.0 || ^2.0.0"
},
"provide": {
"psr/container-implementation": "^1.0"
@ -83,11 +83,14 @@
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"squizlabs/php_codesniffer": "^3.3"
"roave/security-advisories": "dev-master",
"scrutinizer/ocular": "^1.8",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev",
"dev-3.x": "3.x-dev",
"dev-2.x": "2.x-dev",
"dev-1.x": "1.x-dev"
@ -127,7 +130,7 @@
"type": "github"
}
],
"time": "2020-09-28T13:38:44+00:00"
"time": "2021-03-16T09:42:56+00:00"
}
],
"aliases": [],

2
package-lock.json generated
View File

@ -9307,7 +9307,7 @@
}
},
"prettier": {
"version": "npm:wp-prettier@1.19.1",
"version": "npm:prettier@1.19.1",
"resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-1.19.1.tgz",
"integrity": "sha512-mqAC2r1NDmRjG+z3KCJ/i61tycKlmADIjxnDhQab+KBxSAGbF/W7/zwB2guy/ypIeKrrftNsIYkNZZQKf3vJcg==",
"dev": true

View File

@ -1,5 +1,5 @@
=== WooCommerce ===
Contributors: automattic, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho
Contributors: automattic, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho, barryhughes-1
Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce
Requires at least: 5.5
Tested up to: 5.7
@ -160,6 +160,16 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
= 5.2.0 RC 2021-03-30 =
* Update - WooCommerce Admin package 2.1.4. #29520
* Fix - Don't remove existing coupons from order when an invalid REST API request for updating coupons is submitted. #29474
* Fix - Wrong logic for including or excluding the payments step in the list of completed tasks in the onboarding wizard. #29518
**WooCommerce Admin - 2.1.4**
* Fix - Adding New Zealand and Ireland to selective bundle option, previously missed. #6649
= 5.2.0 beta 2021-03-23 =
**WooCommerce**
@ -202,6 +212,8 @@ WooCommerce comes with some sample data you can use to see how products look; im
* Fix - add validation of the posted country codes on checkout. #28849
* Fix - Correctly display pagination arrows on RTL languages. #28523
* Fix - Invalid refund amount error on $0 refund when number of decimals is equal to 0. #27277
* Fix - Revert a replacement of wp_redirect to wp_safe_redirect in WC_Checkout::process_order_payment that caused issues in the default PayPal interface. #29459
* Fix - "Sale" badge misaligned on products when displaying 1 item per row. #29425
* Tweak - Added the Mercado Pago logo into the assets/images folder in order to use it in the payments setup task. #29365
* Tweak - Update the contributor guidelines. #29150
* Tweak - Introduced phone number input validation. #27242

View File

@ -14,7 +14,7 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 3.7.0
* @version 5.2.0
*/
defined( 'ABSPATH' ) || exit;
@ -53,10 +53,10 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
);
?>
<?php if ( empty( $product_permalink ) ) : ?>
<?php echo $thumbnail . $product_name; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $thumbnail . wp_kses_post( $product_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<a href="<?php echo esc_url( $product_permalink ); ?>">
<?php echo $thumbnail . $product_name; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $thumbnail . wp_kses_post( $product_name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</a>
<?php endif; ?>
<?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

View File

@ -12,7 +12,7 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 3.4.0
* @version 5.2.0
*/
defined( 'ABSPATH' ) || exit;
@ -40,7 +40,7 @@ $totals = $order->get_order_item_totals(); // phpcs:ignore WordPress.WP.GlobalVa
<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_order_item_class', 'order_item', $item, $order ) ); ?>">
<td class="product-name">
<?php
echo apply_filters( 'woocommerce_order_item_name', esc_html( $item->get_name() ), $item, false ); // @codingStandardsIgnoreLine
echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, false ) );
do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, false );

View File

@ -12,7 +12,7 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 3.8.0
* @version 5.2.0
*/
defined( 'ABSPATH' ) || exit;
@ -35,7 +35,7 @@ defined( 'ABSPATH' ) || exit;
?>
<tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ); ?>">
<td class="product-name">
<?php echo apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key ) . '&nbsp;'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
<?php echo apply_filters( 'woocommerce_checkout_cart_item_quantity', ' <strong class="product-quantity">' . sprintf( '&times;&nbsp;%s', $cart_item['quantity'] ) . '</strong>', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</td>

View File

@ -21,14 +21,27 @@ defined( 'ABSPATH' ) || exit;
<li>
<?php do_action( 'woocommerce_widget_product_review_item_start', $args ); ?>
<?php
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<a href="<?php echo esc_url( get_comment_link( $comment->comment_ID ) ); ?>">
<?php echo $product->get_image(); ?>
<span class="product-title"><?php echo $product->get_name(); ?></span>
<span class="product-title"><?php echo wp_kses_post( $product->get_name() ); ?></span>
</a>
<?php echo wc_get_rating_html( intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ) ); ?>
<span class="reviewer"><?php echo sprintf( esc_html__( 'by %s', 'woocommerce' ), get_comment_author( $comment->comment_ID ) ); ?></span>
<span class="reviewer">
<?php
/* translators: %s: Comment author. */
echo sprintf( esc_html__( 'by %s', 'woocommerce' ), get_comment_author( $comment->comment_ID ) );
?>
</span>
<?php
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<?php do_action( 'woocommerce_widget_product_review_item_end', $args ); ?>
</li>

View File

@ -12,11 +12,11 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates\Emails\Plain
* @version 3.7.0
* @version 5.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
exit; // Exit if accessed directly.
}
foreach ( $items as $item_id => $item ) :
@ -30,15 +30,18 @@ foreach ( $items as $item_id => $item ) :
$purchase_note = $product->get_purchase_note();
}
echo apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, false );
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, false ) );
if ( $show_sku && $sku ) {
echo ' (#' . $sku . ')';
}
echo ' X ' . apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item );
echo ' = ' . $order->get_formatted_line_subtotal( $item ) . "\n";
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
// allow other plugins to add additional product information here
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, $plain_text );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo strip_tags(
wc_display_item_meta(
$item,
@ -52,10 +55,10 @@ foreach ( $items as $item_id => $item ) :
)
);
// allow other plugins to add additional product information here
// allow other plugins to add additional product information here.
do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, $plain_text );
}
// Note
// Note.
if ( $show_purchase_note && $purchase_note ) {
echo "\n" . do_shortcode( wp_kses_post( $purchase_note ) );
}

View File

@ -12,7 +12,7 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 3.7.0
* @version 5.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -30,7 +30,7 @@ if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
$is_visible = $product && $product->is_visible();
$product_permalink = apply_filters( 'woocommerce_order_item_permalink', $is_visible ? $product->get_permalink( $item ) : '', $item, $order );
echo apply_filters( 'woocommerce_order_item_name', $product_permalink ? sprintf( '<a href="%s">%s</a>', $product_permalink, $item->get_name() ) : $item->get_name(), $item, $is_visible ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $product_permalink ? sprintf( '<a href="%s">%s</a>', $product_permalink, $item->get_name() ) : $item->get_name(), $item, $is_visible ) );
$qty = $item->get_quantity();
$refunded_qty = $order->get_qty_refunded_for_item( $item_id );

View File

@ -2,7 +2,6 @@
This document discusses unit tests. See [the e2e README](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e) to learn how to setup testing environment for running e2e tests and run them.
## Table of contents
- [WooCommerce Tests](#woocommerce-tests)
@ -51,7 +50,6 @@ Example:
**Important**: The `<db-name>` database will be created if it doesn't exist and all data will be removed during testing.
## Running Tests
Change to the plugin root directory and type:
@ -78,9 +76,9 @@ WooCommerce currently supports PHP versions from 7.0 up to 8.0, and this poses a
To workaround this, the testing strategy used by WooCommerce is as follows:
* We normally use PHPUnit 6.5.14
* For PHP 8 we use [a custom fork of PHPUnit 7.5.20 with support for PHP 8](https://github.com/woocommerce/phpunit/pull/1). The Travis build is configured to use this fork instead of the old version 6 when running in PHP 8.
* For PHP 8 we use [a custom fork of PHPUnit 7.5.20 with support for PHP 8](https://github.com/woocommerce/phpunit/pull/1). WooCommerce's GitHub Actions CI workflow is configured to use this fork instead of the old version 6 when running in PHP 8.
If you want to run the tests locally under PHP 8 you'll need to temporarily modify `composer.json` to use the custom PHPUnit fork in the same way that the Travis setup script does. These are the commands that you'll need (run them after a regular `composer install`):
If you want to run the tests locally under PHP 8 you'll need to temporarily modify `composer.json` to use the custom PHPUnit fork in the same way that the GitHub Actions CI workflow file does. These are the commands that you'll need (run them after a regular `composer install`):
```shell
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
@ -92,7 +90,6 @@ composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-req
Just remember that you can't include the modified `composer.json` in any commit!
## Writing Tests
There are three different unit test directories:
@ -122,12 +119,10 @@ General guidelines for all the unit tests:
* Filters persist between test cases so be sure to remove them in your test method or in the `tearDown()` method.
* Use data providers where possible. Be sure that their name is like `data_provider_function_to_test` (i.e. the data provider for `test_is_postcode` would be `data_provider_test_is_postcode`). Read more about data providers [here](https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers).
## Automated Tests
Tests are automatically run with [Travis-CI](https://travis-ci.org/woocommerce/woocommerce) for each commit and pull request.
Tests are automatically run with [GitHub Actions](https://github.com/woocommerce/woocommerce/actions/workflows/ci.yml) for each commit and pull request.
## Code Coverage
Code coverage is available on [Codecov](https://codecov.io/gh/woocommerce/woocommerce/) which receives updated data after each Travis build.
Code coverage is available on [Codecov](https://codecov.io/gh/woocommerce/woocommerce/) which receives updated data after each build.

View File

@ -1,11 +1,14 @@
#!/usr/bin/env bash
if [[ ${RUN_PHPCS} == 1 ]]; then
CHANGED_FILES=`git diff --name-only --diff-filter=ACMR $TRAVIS_COMMIT_RANGE | grep \\\\.php | awk '{print}' ORS=' '`
IGNORE="tests/cli/,includes/libraries/,includes/api/legacy/"
COMMIT_RANGE="${1}...${2}"
CHANGED_FILES=`git diff --name-only --diff-filter=ACMR $COMMIT_RANGE | grep \\\\.php | awk '{print}' ORS=' '`
IGNORE="tests/cli/,includes/libraries/,includes/api/legacy/"
if [ "$CHANGED_FILES" != "" ]; then
echo "Running Code Sniffer."
vendor/bin/phpcs --ignore=$IGNORE --encoding=utf-8 -s -n -p $CHANGED_FILES
fi
if [ "$CHANGED_FILES" != "" ]; then
echo "Changed files: $CHANGED_FILES"
echo "Running Code Sniffer."
./vendor/bin/phpcs --ignore=$IGNORE --encoding=utf-8 -s -n -p --report-full --report-checkstyle=./phpcs-report.xml ${CHANGED_FILES}
else
echo "No changes found. Skipping PHPCS run."
fi

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
# usage: travis.sh before|after
if [ $1 == 'after' ]; then
if [[ ${RUN_CODE_COVERAGE} == 1 ]]; then
bash <(curl -s https://codecov.io/bash)
wget https://scrutinizer-ci.com/ocular.phar
chmod +x ocular.phar
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
fi
fi

View File

@ -55,7 +55,7 @@ This section explains how e2e tests are working behind the scenes. These are not
### Test Environment
We recommend using Docker for running tests locally in order for the test environment to match the setup on Travis CI (where Docker is also used for running tests). [An official WordPress Docker image](https://github.com/docker-library/docs/blob/master/wordpress/README.md) is used to build the site. Once the site using the WP Docker image is built, the current WooCommerce dev branch is mapped into the `plugins` folder of that newly built test site.
We recommend using Docker for running tests locally in order for the test environment to match the setup on Github CI (where Docker is also used for running tests). [An official WordPress Docker image](https://github.com/docker-library/docs/blob/master/wordpress/README.md) is used to build the site. Once the site using the WP Docker image is built, the current WooCommerce dev branch is mapped into the `plugins` folder of that newly built test site.
### Test Variables
@ -77,13 +77,13 @@ The jest test sequencer uses the following test variables:
}
```
If you need to modify the port for your local test environment (eg. port is already in use), copy `tests/e2e/env/config/default.json` to `tests/e2e/config/default.json` and edit that copy. Only edit this file while your test container is `down`.
If you need to modify the port for your local test environment (eg. port is already in use), edit `tests/e2e/config/default.json`. Only edit this file while your test container is `down`.
### Jest test sequencer
[Jest](https://jestjs.io/) is being used to run e2e tests. Jest sequencer introduces tools that can be used to specify the order in which the tests are being run. In our case, they are being run in alphabetical order of the directories where tests are located. This way, tests in the new directory `activate-and-setup` will run first. By default, jest runs tests ordered by the time it takes to run the test (the test that takes longer to run will be run first, the test that takes less time to run will run last).
[Jest](https://jestjs.io/) is being used to run e2e tests. Jest sequencer introduces tools that can be used to specify the order in which the tests are being run. In our case, they are being run in alphabetical order of the directories where tests are located. This way, tests in the directory `activate-and-setup` will run first. By default, jest runs tests ordered by the time it takes to run the test (the test that takes longer to run will be run first, the test that takes less time to run will run last).
The Setup Wizard e2e test (located in `activate-and-setup` directory) will run first. This ensures that WooCommerce is active and the setup wizard has been completed. This is necessary because `docker:up` creates a brand new install of WordPress and WooCommerce.
The Setup Wizard e2e test runs first to ensure that WooCommerce is active and that the setup wizard has been completed. This is necessary because `docker:up` creates a brand new install of WordPress and WooCommerce.
### Chromium Download
@ -99,23 +99,25 @@ Puppeteer will still automatically download Chromium when needed.
### Prep work for running tests
Run the following in a terminal/command line window
- `cd` to the WooCommerce plugin folder
- `git checkout trunk` or checkout the branch where you need to run tests
- `git checkout trunk` (or the branch where you need to run tests)
- Run `nvm use`
- `nvm use`
- Run `npm install`
- `npm install`
- Run `composer install --no-dev`
- `composer install --no-dev`
- Run `npm run build:assets`
- `npm run build:assets`
- Run `npm install jest --global` (this only needs to be done once)
- `npm install jest --global` (this only needs to be done once)
- Run `npx wc-e2e docker:up` - it will build the test site using Docker.
- `npx wc-e2e docker:up` (this will build the test site using Docker)
- Run `docker ps` - to confirm that the Docker containers are running. You should see the log that looks similar to below indicating that everything had been built as expected:
- Use `docker ps` to confirm that the Docker containers are running. You should see a log similar to one below indicating that everything had been built as expected:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
@ -305,7 +307,7 @@ describe( 'Merchant can create virtual product', () => {
} );
```
Next, you can start filling up each section with relevant functions (test building blocks). Note, that we have the `@woocommerce/e2e-utils` package where many reusable helper functions can be found for writing tests. For example, `flows.js` of `@woocommerce/e2e-utils` package contains `merchant` object that has `login` method. As a result, in the test it can be used as `await merchant.login();` so the first `it()` section of the test will become:
Next, you can start filling up each section with relevant functions (test building blocks). Note, that we have the `@woocommerce/e2e-utils` package where many reusable helper functions can be found for writing tests. For example, `merchant.js` of `@woocommerce/e2e-utils` package contains `merchant` object that has `login` method. As a result, in the test it can be used as `await merchant.login();` so the first `it()` section of the test will become:
```
it( 'merchant can log in', async () => {
@ -327,7 +329,7 @@ it( 'merchant can create virtual product', async () => {
You would then continue writing the test using utilities where possible.
Make sure to utilize the functions of the `@automattic/puppeteer-utils` package where possible. For example, if you need to wait for certain element to be ready to be clicked on and then click on it, you can use `waitAndClick()` function:
Make sure to utilize the functions of the `@automattic/puppeteer-utils` package where possible. For example, if you need to wait for a certain element to be ready to be clicked on and then click on it, you can use `waitAndClick()` function:
```
await waitAndClick( page, '#selector' );
@ -351,4 +353,6 @@ In the example above, you can see that `allows customer to see downloads` part o
## Debugging tests
The test sequencer (`npx wc-e2e test:e2e`) includes support for saving [screenshots on test errors](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env#test-screenshots) which can be sent to a Slack channel via a [Slackbot](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env#slackbot-setup).
For Puppeteer debugging, follow [Google's documentation](https://developers.google.com/web/tools/puppeteer/debugging).

View File

@ -1,5 +1,8 @@
# Unreleased
# 0.1.2
## Added
- Support for the external product type.

View File

@ -49,11 +49,19 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
### Repositories
As a convenience utility we've created repositories for core data types that can simplify interacting with the API.
As a convenience utility we've created repositories for core data types that can simplify interacting with the API:
- `SimpleProduct`
- `ExternalProduct`
- `GroupedProduct`
- `VariableProduct`
- `ProductVariation`
- `Coupon`
These repositories provide CRUD methods for ease-of-use:
```javascript
import { SimpleProduct } from '@woocommerce/api';
import { HTTPClientFactory, SimpleProduct } from '@woocommerce/api';
// Prepare the HTTP client that will be consumed by the repository.
// This is necessary so that it can make requests to the REST API.
@ -68,5 +76,86 @@ const product = repository.create( { name: 'Simple Product', regularPrice: '9.99
// The response will be one of the models with structured properties and TypeScript support.
product.id;
```
#### Repository Methods
The following methods are available on all repositories:
- `create( {...properties} )` - Create a single object of the model type
- `delete( objectId )` - Delete a single object of the model type
- `list` - Retrieve a list of the existing objects of that model type
- `read( objectId )` - Read a single object of the model type
- `update( objectId, {...properties} )` - Update a single object of the model type
#### Child Repositories
`ProductVariation` is a child model repository. In child model repositories, each method requires the `parentId` as the first parameter:
```javascript
import { HTTPClientFactory, VariableProduct, ProductVariation } from '@woocommerce/api';
const httpClient = HTTPClientFactory.build( 'https://example.com' )
.withBasicAuth( 'username', 'password' )
.withIndexPermalinks()
.create();
const productRepository = VariableProduct.restRepository( httpClient );
const variationRepository = ProductVariation.restRepository( httpClient );
const product = await productRepository.create({
"name": "Variable Product with Three Attributes",
"defaultAttributes": [
{
"id": 0,
"name": "Size",
"option": "Medium"
},
{
"id": 0,
"name": "Colour",
"option": "Blue"
}
],
"attributes": [
{
"id": 0,
"name": "Colour",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Red",
"Green",
"Blue"
],
"sortOrder": 0
},
{
"id": 0,
"name": "Size",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Small",
"Medium",
"Large"
],
"sortOrder": 0
}
]
});
const variation = await variationRepository.create( product.id, {
"regularPrice": "19.99",
"attributes": [
{
"name": "Size",
"option": "Large"
},
{
"name": "Colour",
"option": "Red"
}
]
});
```

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/api",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@ -27,6 +27,14 @@ export const baseProductURL = () => '/wc/v3/products/';
*/
export const buildProductURL = ( id: ModelID ) => baseProductURL() + id;
/**
* A common delete product URL builder.
*
* @param {ModelID} id the id of the product.
* @return {string} RESTful Url.
*/
export const deleteProductURL = ( id: ModelID ) => buildProductURL( id ) + '?force=true';
/**
* The base for all product types.
*/

View File

@ -3,6 +3,7 @@ import { ModelRepository } from '../../../framework';
import {
baseProductURL,
buildProductURL,
deleteProductURL,
ExternalProduct,
CreatesExternalProducts,
DeletesExternalProducts,
@ -61,6 +62,6 @@ export function externalProductRESTRepository( httpClient: HTTPClient ): ListsEx
restCreate< ExternalProductRepositoryParams >( baseProductURL, ExternalProduct, httpClient, transformer ),
restRead< ExternalProductRepositoryParams >( buildProductURL, ExternalProduct, httpClient, transformer ),
restUpdate< ExternalProductRepositoryParams >( buildProductURL, ExternalProduct, httpClient, transformer ),
restDelete< ExternalProductRepositoryParams >( buildProductURL, httpClient ),
restDelete< ExternalProductRepositoryParams >( deleteProductURL, httpClient ),
);
}

View File

@ -10,6 +10,7 @@ import {
UpdatesGroupedProducts,
baseProductURL,
buildProductURL,
deleteProductURL,
} from '../../../models';
import {
createProductTransformer,
@ -55,6 +56,6 @@ export function groupedProductRESTRepository( httpClient: HTTPClient ): ListsGro
restCreate< GroupedProductRepositoryParams >( baseProductURL, GroupedProduct, httpClient, transformer ),
restRead< GroupedProductRepositoryParams >( buildProductURL, GroupedProduct, httpClient, transformer ),
restUpdate< GroupedProductRepositoryParams >( buildProductURL, GroupedProduct, httpClient, transformer ),
restDelete< GroupedProductRepositoryParams >( buildProductURL, httpClient ),
restDelete< GroupedProductRepositoryParams >( deleteProductURL, httpClient ),
);
}

View File

@ -4,6 +4,7 @@ import {
SimpleProduct,
baseProductURL,
buildProductURL,
deleteProductURL,
CreatesSimpleProducts,
DeletesSimpleProducts,
ListsSimpleProducts,
@ -70,6 +71,6 @@ export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimp
restCreate< SimpleProductRepositoryParams >( baseProductURL, SimpleProduct, httpClient, transformer ),
restRead< SimpleProductRepositoryParams >( buildProductURL, SimpleProduct, httpClient, transformer ),
restUpdate< SimpleProductRepositoryParams >( buildProductURL, SimpleProduct, httpClient, transformer ),
restDelete< SimpleProductRepositoryParams >( buildProductURL, httpClient ),
restDelete< SimpleProductRepositoryParams >( deleteProductURL, httpClient ),
);
}

View File

@ -10,6 +10,7 @@ import {
UpdatesVariableProducts,
baseProductURL,
buildProductURL,
deleteProductURL,
} from '../../../models';
import {
createProductTransformer,
@ -67,6 +68,6 @@ export function variableProductRESTRepository( httpClient: HTTPClient ): ListsVa
restCreate< VariableProductRepositoryParams >( baseProductURL, VariableProduct, httpClient, transformer ),
restRead< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
restUpdate< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
restDelete< VariableProductRepositoryParams >( buildProductURL, httpClient ),
restDelete< VariableProductRepositoryParams >( deleteProductURL, httpClient ),
);
}

View File

@ -1,26 +1,28 @@
# Unreleased
# 0.1.3
## Added
- Shopper My Account Create Account
## Fixed
- removed use of ES6 `import`
# 0.1.2
## Added
- api package test for variable products and product variations
- api package test for grouped products
- api package test for external products
- api package test for coupons
# 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
- Merchant Product Edit tests
- Merchant Product Search tests
- Shopper Single Product tests
- Shopper My Account Pay Order
- Shopper Checkout Apply Coupon
- Shopper Shop Browse Search Sort
- Merchant Orders Customer Checkout Page
- Shopper Cart Apply Coupon
@ -28,6 +30,18 @@
- Merchant Settings Shipping Zones
- Shopper Variable product info updates on different variations
- Merchant order emails flow
- Merchant analytics page load tests
- Shopper Checkout Create Account
# 0.1.1
## Added
- 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 Checkout Apply Coupon
## Fixed

View File

@ -45,22 +45,24 @@ The functions to access the core tests are:
### Merchant
- `runMerchantTests` - Run all merchant tests
- `runCreateCouponTest` - Merchant can create coupon
- `runCreateOrderTest` - Merchant can create order
- `runAddNewShippingZoneTest` - Merchant can create shipping zones and let shopper test them
- `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
- `runCreateCouponTest` - Merchant can create coupon
- `runCreateOrderTest` - Merchant can create order
- `runMerchantOrdersCustomerPaymentPage` - Merchant can visit the customer payment page
- `runMerchantOrderEmailsTest` - Merchant can receive order emails and resend emails by Order Actions
- `runOrderStatusFilterTest` - Merchant can filter orders by order status
- `runOrderRefundTest` - Merchant can refund an order
- `runOrderApplyCouponTest` - Merchant can apply a coupon to an order
- `runOrderSearchingTest` - Merchant can search for order via different terms
- `runProductEditDetailsTest` - Merchant can edit an existing product
- `runProductSearchTest` - Merchant can search for a product and view it
- `runMerchantOrdersCustomerPaymentPage` - Merchant can visit the customer payment page
- `runOrderSearchingTest` - Merchant can search for order via different terms
- `runAddNewShippingZoneTest` - Merchant can create shipping zones and let shopper test them
- `runProductSettingsTest` - Merchant can update product settings
- `runTaxSettingsTest` - Merchant can update tax settings
- `runUpdateGeneralSettingsTest` - Merchant can update general settings
- `runMerchantOrderEmailsTest` - Merchant can receive order emails and resend emails by Order Actions
- `runAnalyticsPageLoadsTest` - Merchant can load and see all pages in Analytics
### Shopper
@ -70,12 +72,21 @@ The functions to access the core tests are:
- `runCheckoutApplyCouponsTest` - Shopper can use coupons on checkout
- `runCheckoutPageTest` - Shopper can complete checkout
- `runMyAccountPageTest` - Shopper can access my account page
- `runSingleProductPageTest` - Shopper can view single product page in many variations (simple, variable, grouped)
- `runMyAccountPayOrderTest` - Shopper can pay for his order in My Account
- `runCartApplyCouponsTest` - Shopper can apply coupons in the cart
- `runCheckoutApplyCouponsTest` - Shopper can apply coupons in the checkout
- `runMyAccountPayOrderTest` - Shopper can pay for their order in My Account
- `runProductBrowseSearchSortTest` - Shopper can browse, search & sort products
- `runSingleProductPageTest` - Shopper can view single product page in many variations (simple, variable, grouped)
- `runVariableProductUpdateTest` - Shopper can view and update variations on a variable product
- `runCheckoutCreateAccountTest` - Shopper can create an account during checkout
- `runMyAccountCreateAccountTest` - Shopper can create an account via my account page
### REST API
- `runApiTests` - Run all API tests
- `runExternalProductAPITest` - Can create, read, and delete an external product
- `runGroupedProductAPITest` - Can create, read, and delete a grouped product
- `runVariableProductAPITest` - Can create, read, and delete a variable product and its variations
- `runCouponApiTest` - Can create, read, and delete a coupon
## Contributing a new test

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-core-tests",
"version": "0.1.1",
"version": "0.1.3",
"description": "End-To-End (E2E) tests for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/core-tests/README.md",
"repository": {
@ -15,8 +15,8 @@
"faker": "^5.5.2"
},
"peerDependencies": {
"@woocommerce/api": "^0.1.1",
"@woocommerce/e2e-utils": "^0.1.2"
"@woocommerce/api": "^0.1.2",
"@woocommerce/e2e-utils": "^0.1.4"
},
"publishConfig": {
"access": "public"

View File

@ -80,6 +80,10 @@ const runGroupedProductAPITest = () => {
it('can delete a grouped product', async () => {
const status = repository.delete( product.id );
expect( status ).toBeTruthy();
// Delete the simple "child" products.
groupedProducts.forEach( ( productId ) => {
repository.delete( productId );
});
});
});
};

View File

@ -16,8 +16,10 @@ const runCheckoutApplyCouponsTest = require( './shopper/front-end-checkout-coupo
const runCheckoutPageTest = require( './shopper/front-end-checkout.test' );
const runMyAccountPageTest = require( './shopper/front-end-my-account.test' );
const runMyAccountPayOrderTest = require( './shopper/front-end-my-account-pay-order.test' );
const runMyAccountCreateAccountTest = require( './shopper/front-end-my-account-create-account.test' );
const runSingleProductPageTest = require( './shopper/front-end-single-product.test' );
const runVariableProductUpdateTest = require( './shopper/front-end-variable-product-updates.test' );
const runCheckoutCreateAccountTest = require( './shopper/front-end-checkout-create-account.test' );
// Merchant tests
const runAddNewShippingZoneTest = require ( './merchant/wp-admin-settings-shipping-zones.test' );
@ -37,6 +39,7 @@ const runProductSearchTest = require( './merchant/wp-admin-product-search.test'
const runMerchantOrdersCustomerPaymentPage = require( './merchant/wp-admin-order-customer-payment-page.test' );
const runMerchantOrderEmailsTest = require( './merchant/wp-admin-order-emails.test' );
const runOrderSearchingTest = require( './merchant/wp-admin-order-searching.test' );
const runAnalyticsPageLoadsTest = require( './merchant/wp-admin-analytics-page-loads.test' );
// REST API tests
const runExternalProductAPITest = require( './api/external-product.test' );
@ -59,8 +62,10 @@ const runShopperTests = () => {
runCheckoutPageTest();
runMyAccountPageTest();
runMyAccountPayOrderTest();
runMyAccountCreateAccountTest();
runSingleProductPageTest();
runVariableProductUpdateTest();
runCheckoutCreateAccountTest();
};
const runMerchantTests = () => {
@ -81,6 +86,7 @@ const runMerchantTests = () => {
runProductEditDetailsTest();
runProductSearchTest();
runMerchantOrdersCustomerPaymentPage();
runAnalyticsPageLoadsTest();
}
const runApiTests = () => {
@ -129,5 +135,8 @@ module.exports = {
runAddNewShippingZoneTest,
runProductBrowseSearchSortTest,
runApiTests,
runAddShippingClassesTest
runAddShippingClassesTest,
runAnalyticsPageLoadsTest,
runCheckoutCreateAccountTest,
runMyAccountCreateAccountTest
};

View File

@ -0,0 +1,105 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const {
merchant,
} = require( '@woocommerce/e2e-utils' );
/**
* External dependencies
*/
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* Quick check for page title and no data message.
*
* @param pageTitle Page title in H1.
* @param element Defaults to '.d3-chart__empty-message'
* @param elementText Defaults to 'No data for the selected date range'
*/
const checkHeadingAndElement = async (
pageTitle, element = '.d3-chart__empty-message', elementText = 'No data for the selected date range') => {
await expect(page).toMatchElement('h1', {text: pageTitle});
await expect(page).toMatchElement(element, elementText);
};
const runAnalyticsPageLoadsTest = () => {
describe('Analytics > Opening Top Level Pages', () => {
beforeAll(async () => {
await merchant.login();
});
it('can see Overview page properly', async () => {
// Go to "overview" page and verify it
await merchant.openAnalyticsPage('overview');
await checkHeadingAndElement('Overview');
});
it('can see Products page properly', async () => {
// Go to "products" page and verify it
await merchant.openAnalyticsPage('products');
await checkHeadingAndElement('Products');
});
it('can see Revenue page properly', async () => {
// Go to "revenue" page and verify it
await merchant.openAnalyticsPage('revenue');
await checkHeadingAndElement('Revenue');
});
it('can see Orders page properly', async () => {
// Go to "orders" page and verify it
await merchant.openAnalyticsPage('orders');
await checkHeadingAndElement('Orders');
});
it('can see Variations page properly', async () => {
// Go to "variations" page and verify it
await merchant.openAnalyticsPage('variations');
await checkHeadingAndElement('Variations');
});
it('can see Categories page properly', async () => {
// Go to "categories" page and verify it
await merchant.openAnalyticsPage('categories');
await checkHeadingAndElement('Categories');
});
it('can see Coupons page properly', async () => {
// Go to "coupons" page and verify it
await merchant.openAnalyticsPage('coupons');
await checkHeadingAndElement('Coupons');
});
it('can see Taxes page properly', async () => {
// Go to "taxes" page and verify it
await merchant.openAnalyticsPage('taxes');
await checkHeadingAndElement('Taxes');
});
it('can see Downloads page properly', async () => {
// Go to "downloads" page and verify it
await merchant.openAnalyticsPage('downloads');
await checkHeadingAndElement('Downloads');
});
it('can see Stock page properly', async () => {
// Go to "stock" page and verify it
await merchant.openAnalyticsPage('stock');
await checkHeadingAndElement('Stock', '.components-button > span', 'Product / Variation');
});
it('can see Settings page properly', async () => {
// Go to "settings" page and verify it
await merchant.openAnalyticsPage('settings');
await checkHeadingAndElement('Settings', 'h2', 'Analytics Settings');
});
});
}
module.exports = runAnalyticsPageLoadsTest;

View File

@ -1,5 +1,5 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests, jest/no-standalone-expect */
import {createSimpleProduct} from "@woocommerce/e2e-utils";
const { createSimpleProduct } = require( '@woocommerce/e2e-utils' );
/**
* Internal dependencies

View File

@ -6,7 +6,6 @@
const {
merchant,
clearAndFillInput,
selectOptionInSelect2,
searchForOrder,
createSimpleProduct,
addProductToOrder,
@ -20,40 +19,36 @@ const runOrderSearchingTest = () => {
await merchant.login();
await createSimpleProduct('Wanted Product');
await Promise.all([
// Create new order for testing
await merchant.openNewOrder(),
await page.waitForSelector('#order_status'),
await page.click('#customer_user'),
await page.click('span.select2-search > input.select2-search__field'),
await page.type('span.select2-search > input.select2-search__field', 'Customer'),
await page.waitFor(2000), // to avoid flakyness
await page.keyboard.press('Enter'),
]);
// Create new order for testing
await merchant.openNewOrder();
await page.waitForSelector('#order_status');
await page.click('#customer_user');
await page.click('span.select2-search > input.select2-search__field');
await page.type('span.select2-search > input.select2-search__field', 'Customer');
await page.waitFor(2000); // to avoid flakyness
await page.keyboard.press('Enter');
await Promise.all([
// Change the shipping data
await page.waitFor(1000), // to avoid flakiness
await page.waitForSelector('#_shipping_first_name'),
await clearAndFillInput('#_shipping_first_name', 'Tim'),
await clearAndFillInput('#_shipping_last_name', 'Clark'),
await clearAndFillInput('#_shipping_address_1', 'Oxford Ave'),
await clearAndFillInput('#_shipping_address_2', 'Linwood Ave'),
await clearAndFillInput('#_shipping_city', 'Buffalo'),
await clearAndFillInput('#_shipping_postcode', '14201'),
await page.keyboard.press('Tab'),
await page.keyboard.press('Tab'),
await page.keyboard.press('Enter'),
await page.select('select[name="_shipping_state"]', 'NY'),
]);
// Change the shipping data
await page.waitFor(1000); // to avoid flakiness
await page.click('.billing-same-as-shipping');
await page.keyboard.press('Enter');
await page.waitForSelector('#_shipping_first_name');
await clearAndFillInput('#_shipping_first_name', 'Tim');
await clearAndFillInput('#_shipping_last_name', 'Clark');
await clearAndFillInput('#_shipping_address_1', 'Oxford Ave');
await clearAndFillInput('#_shipping_address_2', 'Linwood Ave');
await clearAndFillInput('#_shipping_city', 'Buffalo');
await clearAndFillInput('#_shipping_postcode', '14201');
// Get the post id
const variablePostId = await page.$('#post_ID');
orderId = (await(await variablePostId.getProperty('value')).jsonValue());
// Save new order
// Save new order and add desired product to order
await clickUpdateOrder('Order updated.', true);
await addProductToOrder(orderId, 'Wanted Product');
// Open All Orders view
await merchant.openAllOrdersView();
});
@ -126,7 +121,7 @@ const runOrderSearchingTest = () => {
})
it('can search for order by shipping state name', async () => {
await searchForOrder('NY', orderId, 'John Doe');
await searchForOrder('CA', orderId, 'John Doe');
})
it('can search for order by item name', async () => {

View File

@ -17,7 +17,7 @@ const {
const config = require( 'config' );
const simpleProductPrice = config.has( 'products.simple.price' ) ? config.get( 'products.simple.price' ) : '9.99';
const simpleProductName = config.get( 'products.simple.name' );
const california = 'California, United States (US)';
const california = 'state:US:CA';
const sanFranciscoZIP = '94107';
const shippingZoneNameUS = 'US with Flat rate';
const shippingZoneNameFL = 'CA with Free shipping';
@ -90,10 +90,6 @@ const runAddNewShippingZoneTest = () => {
await selectOptionInSelect2('New York');
await expect(page).toClick('button[name="calc_shipping"]');
// Set shipping postcode to 10010
await clearAndFillInput('#calc_shipping_postcode', '10010');
await expect(page).toClick('button[name="calc_shipping"]');
// Verify shipping costs
await page.waitForSelector('.order-total');
await expect(page).toMatchElement('.shipping .amount', {text: '$10.00'});
@ -102,6 +98,7 @@ const runAddNewShippingZoneTest = () => {
it('allows customer to benefit from a Free shipping if in CA', async () => {
await page.reload();
// Set shipping state to California
await expect(page).toClick('a.shipping-calculator-button');
await expect(page).toClick('#select2-calc_shipping_state-container');
@ -119,6 +116,7 @@ const runAddNewShippingZoneTest = () => {
it('allows customer to benefit from a free Local pickup if in SF', async () => {
await page.reload();
// Set shipping postcode to 94107
await expect(page).toClick('a.shipping-calculator-button');
await clearAndFillInput('#calc_shipping_postcode', '94107');

View File

@ -0,0 +1,62 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests, jest/expect-expect */
/**
* Internal dependencies
*/
const {
shopper,
merchant,
createSimpleProduct,
uiUnblocked,
setCheckbox,
settingsPageSaveChanges,
} = require( '@woocommerce/e2e-utils' );
/**
* External dependencies
*/
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );
const runCheckoutCreateAccountTest = () => {
describe('Shopper Checkout Create Account', () => {
beforeAll(async () => {
await merchant.login();
await createSimpleProduct();
await merchant.openSettings('account');
await setCheckbox('#woocommerce_enable_signup_and_login_from_checkout');
await settingsPageSaveChanges();
await merchant.logout();
await shopper.goToShop();
await shopper.addToCartFromShopPage(simpleProductName);
await uiUnblocked();
await shopper.goToCheckout();
});
it('can create an account during checkout', async () => {
// Fill all the details for a new customer
await shopper.fillBillingDetails(config.get('addresses.customer.billing'));
await uiUnblocked();
// Set checkbox for creating account during checkout
await setCheckbox('#createaccount');
// Place an order
await shopper.placeOrder();
await expect(page).toMatchElement('h1.entry-title', {text: 'Order received'});
});
it('can verify that the customer has been created', async () => {
await merchant.login();
await merchant.openAllUsersView();
await expect(page).toMatchElement('td.email.column-email > a', {text: 'john.doe@example.com'});
});
});
};
module.exports = runCheckoutCreateAccountTest;

View File

@ -0,0 +1,41 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const {
shopper,
merchant,
setCheckbox,
settingsPageSaveChanges,
} = require( '@woocommerce/e2e-utils' );
const runMyAccountCreateAccountTest = () => {
describe('Shopper My Account Create Account', () => {
beforeAll(async () => {
await merchant.login();
// Set checkbox in the settings to enable registration in my account
await merchant.openSettings('account');
await setCheckbox('#woocommerce_enable_myaccount_registration');
await settingsPageSaveChanges();
await merchant.logout();
});
it('can create a new account via my account', async () => {
await shopper.gotoMyAccount();
await page.waitForSelector('.woocommerce-form-register');
await expect(page).toFill('input#reg_email', 'john.doe.test@example.com');
await expect(page).toClick('button[name="register"]');
await page.waitForNavigation({waitUntil: 'networkidle0'});
await expect(page).toMatchElement('h1', 'My account');
// Verify user has been created successfully
await merchant.login();
await merchant.openAllUsersView();
await expect(page).toMatchElement('td.email.column-email > a', {text: 'john.doe.test@example.com'});
});
});
};
module.exports = runMyAccountCreateAccountTest;

View File

@ -4,9 +4,7 @@
*/
const {
shopper,
merchant,
createSimpleProductWithCategory,
uiUnblocked,
} = require( '@woocommerce/e2e-utils' );
/**

View File

@ -2,13 +2,16 @@
echo "Initializing WooCommerce E2E"
wp plugin install woocommerce --activate
wp plugin activate woocommerce
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html
# we cannot create API keys for the API, so we using basic auth, this plugin allows that.
wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate
# install the WP Mail Logging plugin to test emails
wp plugin install wp-mail-logging --activate
echo "Updating to WordPress Nightly Point Release"
wp plugin install wordpress-beta-tester --activate

View File

@ -1,5 +1,8 @@
# Unreleased
# 0.2.1
## Added
- Support for screenshots on test errors

View File

@ -11,7 +11,7 @@ npm install jest --global
## Configuration
The `@woocommerce/e2e-environment` package exports configuration objects that can be consumed in JavaScript config files in your project. Additionally, it includes a hosting container for running tests and includes instructions for creating your Travis CI setup.
The `@woocommerce/e2e-environment` package exports configuration objects that can be consumed in JavaScript config files in your project. Additionally, it includes a basic hosting container for running tests and includes instructions for creating your Travis CI setup.
### Babel Config
@ -56,7 +56,7 @@ module.exports = useE2EEsLintConfig( {
### Jest Config
The E2E environment uses Jest as a test runner. Extending the base config is needed in order for Jest to run your project's test files.
The E2E environment uses Jest as a test runner. Extending the base config is necessary in order for Jest to run your project's test files.
```js
const path = require( 'path' );
@ -69,7 +69,7 @@ const jestConfig = useE2EJestConfig( {
module.exports = jestConfig;
```
**NOTE:** Your project's Jest config file is expected to be: `tests/e2e/config/jest.config.js`.
**NOTE:** Your project's Jest config file is: `tests/e2e/config/jest.config.js`.
#### Test Screenshots
@ -79,7 +79,7 @@ The test sequencer provides a screenshot function for test failures. To enable s
WC_E2E_SCREENSHOTS=1 npx wc-e2e test:e2e
```
Screenshots will be saved to `tests/e2e/screenshots`
Screenshots will be saved to `tests/e2e/screenshots`. This folder is cleared at the beginning of each test run.
### Jest Puppeteer Config
@ -125,11 +125,11 @@ module.exports = puppeteerConfig;
### Jest Setup
Jest provides setup and teardown functions similar to PHPUnit. The default setup and teardown is in [`tests/e2e/env/src/setup/jest.setup.js`](src/setup/jest.setup.js). Additional setup and teardown functions can be added to [`tests/e2e/config/jest.setup.js`](../config/jest.setup.js)
Jest provides [setup and teardown functions](https://jestjs.io/docs/setup-teardown) similar to PHPUnit. The default setup and teardown is in [`tests/e2e/env/src/setup/jest.setup.js`](src/setup/jest.setup.js). Additional setup and teardown functions can be added to [`tests/e2e/config/jest.setup.js`](../config/jest.setup.js)
### Container Setup
Depending on the project and testing scenario, the built in testing environment container might not be the best solution for testing. This could be local testing where there is already a testing container, a repository that isn't a plugin or theme and there are multiple folders mapped into the container, or similar. The `e2e-environment` container supports using either the built in container or an external container. See the the appropriate readme for details:
Depending on the project and testing scenario, the built in testing environment container might not be the best solution for testing. This could be local testing where there is already a testing container, a repository that isn't a plugin or theme and there are multiple folders mapped into the container, or similar. The `e2e-environment` test runner supports using either the built in container or an external container. See the appropriate readme for details:
- [Built In Container](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env/builtin.md)
- [External Container](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/env/external.md)

View File

@ -34,6 +34,35 @@ echo "Initializing WooCommerce E2E"
wp plugin activate woocommerce
wp theme install twentynineteen --activate
```
### Adhoc Initialization
The container build script supports an initialization script parameter
```shell script
npx wc-e2e docker:up tests/e2e/docker/init-wp-beta.sh
```
This script updates WordPress to the latest nightly point release
```shell script
#!/bin/bash
echo "Initializing WooCommerce E2E"
wp plugin install woocommerce --activate
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html
# we cannot create API keys for the API, so we using basic auth, this plugin allows that.
wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate
echo "Updating to WordPress Nightly Point Release"
wp plugin install wordpress-beta-tester --activate
wp core check-update
```
### Container Configuration

View File

@ -66,7 +66,7 @@ version: ~> 1.0
script:
- npm install jest --global
# add your initialization script here
- npm explore @woocommerce/e2e-environment -- npm run test:e2e
- npx wc-e2e test:e2e
....

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,30 @@
# Unreleased
# 0.1.4
## Fixed
- build issue with faker import
# 0.1.3
## Added
- `selectOptionInSelect2( selector, value )` util helper method that search and select in any select2 type field
- `searchForOrder( value, orderId, customerName )` util helper method that search order with different terms
- `addShippingZoneAndMethod( zoneName, zoneLocation, zipCode, zoneMethod )` util helper method for adding shipping zones with shipping methods
- `createSimpleProductWithCategory` component which creates a simple product with categories, containing three parameters for title, price and category name.
- `applyCoupon( couponName )` util helper method which applies previously created coupon to cart or checkout
- `removeCoupon()` util helper method that removes a single coupon within cart or checkout
- `selectOrderAction( action )` util helper method to select and initiate an order action in the Order Action postbox
- `merchant.openEmailLog()` go to the WP Mail Log page
- `deleteAllEmailLogs` delete all email logs in the WP Mail Log plugin
- `clickUpdateOrder( noticeText, waitForSave )` util helper that clicks the `Update` button on an order
## Changed
- Added coupon type parameter to `createCoupon( couponAmount, couponType )`. Default coupon type is fixed cart.
# 0.1.2
## Fixed
@ -16,16 +41,6 @@
- `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.
- `selectOptionInSelect2( selector, value )` util helper method that search and select in any select2 type field
- `searchForOrder( value, orderId, customerName )` util helper method that search order with different terms
- `addShippingZoneAndMethod( zoneName, zoneLocation, zipCode, zoneMethod )` util helper method for adding shipping zones with shipping methods
- `createSimpleProductWithCategory` component which creates a simple product with categories, containing three parameters for title, price and category name.
- `applyCoupon( couponName )` util helper method which applies previously created coupon to cart or checkout
- `removeCoupon()` util helper method that removes a single coupon within cart or checkout
- `selectOrderAction( action )` util helper method to select and initiate an order action in the Order Action postbox
- `merchant.openEmailLog()` go to the WP Mail Log page
- `deleteAllEmailLogs` delete all email logs in the WP Mail Log plugin
- `clickUpdateOrder( noticeText, waitForSave )` util helper that clicks the `Update` button on an order
## Changes

View File

@ -21,9 +21,7 @@ import {
describe( 'Cart page', () => {
beforeAll( async () => {
await merchant.login();
await createSimpleProduct();
await merchant.logout();
} );
it( 'should display no item in the cart', async () => {
@ -55,6 +53,8 @@ describe( 'Cart page', () => {
| `runSetupWizard` | | Open the onboarding profiler |
| `updateOrderStatus` | `orderId, status` | Update the status of an order |
| `openEmailLog` | | Open the WP Mail Log page |
| `openAnalyticsPage` | | Open any Analytics page |
| `openAllUsersView` | | Open the All Users page |
### Shopper `shopper`
@ -85,20 +85,33 @@ describe( 'Cart page', () => {
| Function | Parameters | Description |
|----------|------------|-------------|
| `addProductToOrder` | `orderId, productName` | adds a product to an order using the product search |
| `applyCoupon` | `couponName` | helper method which applies a coupon in cart or checkout |
| `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. |
| `clickUpdateOrder` | `noticeText`, `waitForSave` | Helper method to click the Update button on the order details page |
| `completeOnboardingWizard` | | completes the onboarding wizard with some default settings |
| `createCoupon` | `couponAmount`, `couponType` | creates a basic coupon. Default amount is 5. Default coupon type is fixed discount. Returns the generated coupon code. |
| `createGroupedProduct` | | creates a grouped product for the grouped product tests. Returns the product id. |
| `createSimpleOrder` | `status` | creates a basic order with the provided status string |
| `createSimpleProduct` | | creates the simple product configured in default.json. Returns the product id. |
| `createSimpleProductWithCategory` | `name`, `price`,`categoryName` | creates a simple product used passed values. Returns the product id. |
| `createVariableProduct` | | creates a variable product for the variable product tests. Returns the product id. |
| `deleteAllEmailLogs` | | deletes the emails generated by WP Mail Logging plugin |
| `evalAndClick` | `selector` | helper method that clicks an element inserted in the DOM by a script |
| `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 |
| `removeCoupon` | | helper method that removes a single coupon within cart or checkout |
| `selectOptionInSelect2` | `selector, value` | helper method that searchs for select2 type fields and select plus insert value inside |
| `selectOrderAction` | `action` | Helper method to select an order action in the `Order Actions` postbox |
| `setCheckbox` | `selector` | Check a checkbox |
| `unsetCheckbox` | `selector` | Uncheck a checkbox |
| `settingsPageSaveChanges` | | Save the current WooCommerce settings page |
| `uiUnblocked` | | Wait until the page is unblocked |
| `verifyPublishAndTrash` | `button, publishNotice, publishVerification, trashVerification` | Verify that an item can be published and trashed |
| `unsetCheckbox` | `selector` | Uncheck a checkbox |
| `verifyAndPublish` | `noticeText` | Verify that an item can be published |
| `verifyCheckboxIsSet` | `selector` | Verify that a checkbox is checked |
| `verifyCheckboxIsUnset` | `selector` | Verify that a checkbox is unchecked |
| `verifyPublishAndTrash` | `button, publishNotice, publishVerification, trashVerification` | Verify that an item can be published and trashed |
| `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 |
@ -113,4 +126,4 @@ describe( 'Cart page', () => {
### Test Utilities
As of version 0.1.2, all test utilities from [`@wordpress/e2e-test-utils`](https://www.npmjs.com/package/@wordpress/e2e-test-utils) are available through this package.
As of version 0.1.3, all test utilities from [`@wordpress/e2e-test-utils`](https://www.npmjs.com/package/@wordpress/e2e-test-utils) are available through this package.

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/e2e-utils",
"version": "0.1.2",
"version": "0.1.4",
"description": "End-To-End (E2E) test utils for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e-utils/README.md",
"repository": {
@ -18,7 +18,7 @@
"fishery": "^1.2.0"
},
"peerDependencies": {
"@woocommerce/api": "^0.1.0"
"@woocommerce/api": "^0.1.2"
},
"publishConfig": {
"access": "public"

View File

@ -6,7 +6,14 @@
* Internal dependencies
*/
import { merchant } from './flows';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset, evalAndClick, selectOptionInSelect2, setCheckbox } from './page-utils';
import {
clickTab,
uiUnblocked,
verifyCheckboxIsUnset,
selectOptionInSelect2,
setCheckbox,
unsetCheckbox
} from './page-utils';
import factories from './factories';
const config = require( 'config' );
@ -143,7 +150,8 @@ const completeOnboardingWizard = async () => {
await waitAndClickPrimary( false );
// Skip installing extensions
await evalAndClick( '.components-checkbox-control__input' );
await unsetCheckbox( '.components-checkbox-control__input' );
await verifyCheckboxIsUnset( '.components-checkbox-control__input' );
await waitAndClickPrimary();
// Theme section
@ -464,11 +472,11 @@ const createCoupon = async ( couponAmount = '5', discountType = 'Fixed cart disc
* Adds a shipping zone along with a shipping method.
*
* @param zoneName Shipping zone name.
* @param zoneLocation Shiping zone location. Defaults to United States (US).
* @param zoneLocation Shiping zone location. Defaults to country:US. For states use: state:US:CA
* @param zipCode Shipping zone zip code. Defaults to empty one space.
* @param zoneMethod Shipping method type. Defaults to flat_rate (use also: free_shipping or local_pickup)
*/
const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States (US)', zipCode = ' ', zoneMethod = 'flat_rate' ) => {
const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'country:US', zipCode = ' ', zoneMethod = 'flat_rate' ) => {
await merchant.openNewShipping();
// Fill shipping zone name
@ -476,12 +484,7 @@ const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States
await expect(page).toFill('input#zone_name', zoneName);
// Select shipping zone location
// (.toSelect is not best option here because a lot of &nbsp are present in country/state names)
await expect(page).toFill('#zone_locations', zoneLocation);
await uiUnblocked();
await page.keyboard.press('Tab');
await uiUnblocked();
await page.keyboard.press('Enter');
await expect(page).toSelect('select[name="zone_locations"]', zoneLocation);
// Fill shipping zone postcode if needed otherwise just put empty space
await page.waitForSelector('a.wc-shipping-zone-postcodes-toggle');
@ -491,14 +494,12 @@ const addShippingZoneAndMethod = async ( zoneName, zoneLocation = 'United States
await expect(page).toClick('button#submit');
// Add shipping zone method
await uiUnblocked();
await page.waitFor(1000);
await expect(page).toClick('button.wc-shipping-zone-add-method', {text:'Add shipping method'});
await page.waitForSelector('.wc-shipping-zone-method-selector');
await expect(page).toSelect('select[name="add_method_id"]', zoneMethod);
await uiUnblocked();
await expect(page).toClick('button#btn-ok');
await page.waitForSelector('#zone_locations');
await uiUnblocked();
};
/**

View File

@ -1,5 +1,5 @@
import { SimpleProduct } from '@woocommerce/api';
import faker from 'faker/locale/en';
const faker = require( 'faker/locale/en' );
import { Factory } from 'fishery';
/**

View File

@ -17,6 +17,8 @@ export const WP_ADMIN_NEW_PRODUCT = baseUrl + 'wp-admin/post-new.php?post_type=p
export const WP_ADMIN_WC_SETTINGS = baseUrl + 'wp-admin/admin.php?page=wc-settings&tab=';
export const WP_ADMIN_PERMALINK_SETTINGS = baseUrl + 'wp-admin/options-permalink.php';
export const WP_ADMIN_NEW_SHIPPING_ZONE = baseUrl + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new';
export const WP_ADMIN_ANALYTICS_PAGES = baseUrl + 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2F';
export const WP_ADMIN_ALL_USERS_VIEW = baseUrl + 'wp-admin/users.php';
export const SHOP_PAGE = baseUrl + 'shop';
export const SHOP_PRODUCT_PAGE = baseUrl + '?p=';

View File

@ -19,7 +19,9 @@ const {
WP_ADMIN_PLUGINS,
WP_ADMIN_SETUP_WIZARD,
WP_ADMIN_WC_SETTINGS,
WP_ADMIN_NEW_SHIPPING_ZONE
WP_ADMIN_NEW_SHIPPING_ZONE,
WP_ADMIN_ANALYTICS_PAGES,
WP_ADMIN_ALL_USERS_VIEW,
} = require( './constants' );
const baseUrl = config.get( 'url' );
@ -182,6 +184,18 @@ const merchant = {
waitUntil: 'networkidle0',
} );
},
openAnalyticsPage: async ( pageName ) => {
await page.goto( WP_ADMIN_ANALYTICS_PAGES + pageName, {
waitUntil: 'networkidle0',
} );
},
openAllUsersView: async () => {
await page.goto( WP_ADMIN_ALL_USERS_VIEW, {
waitUntil: 'networkidle0',
} );
},
};
module.exports = merchant;

View File

@ -4,11 +4,18 @@
*
* Provides REST API specific methods and setup/teardown.
*
* @package WooCommerce\Tests
* @since 3.0
*/
/**
* Base class for REST related unit test classes.
*/
class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
/**
* @var WP_REST_Server
*/
protected $server;
/**
@ -36,4 +43,64 @@ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
unset( $this->server );
$wp_rest_server = null;
}
/**
* Perform a REST request.
*
* @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
* @param string $verb HTTP verb for the request, default is GET.
* @param array|null $body_params Body parameters for the request, null if none are required.
* @param array|null $query_params Query string parameters for the request, null if none are required.
* @return array Result from the request.
*/
public function do_rest_request( $url, $verb = 'GET', $body_params = null, $query_params = null ) {
if ( '/' !== $url[0] ) {
$url = '/wc/v3/' . $url;
}
$request = new WP_REST_Request( $verb, $url );
if ( ! is_null( $query_params ) ) {
$request->set_query_params( $query_params );
}
if ( ! is_null( $body_params ) ) {
$request->set_body_params( $body_params );
}
return $this->server->dispatch( $request );
}
/**
* Perform a GET REST request.
*
* @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
* @param array|null $query_params Query string parameters for the request, null if none are required.
* @return WP_REST_Response The response for the request.
*/
public function do_rest_get_request( $url, $query_params = null ) {
return $this->do_rest_request( $url, 'GET', null, $query_params );
}
/**
* Perform a POST REST request.
*
* @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
* @param array|null $body_params Body parameters for the request, null if none are required.
* @param array|null $query_params Query string parameters for the request, null if none are required.
* @return array Result from the request.
*/
public function do_rest_post_request( $url, $body_params = null, $query_params = null ) {
return $this->do_rest_request( $url, 'POST', $body_params, $query_params );
}
/**
* Perform a PUT REST request.
*
* @param string $url The endpopint url, if it doesn't start with '/' it'll be prepended with '/wc/v3/'.
* @param array|null $body_params Body parameters for the request, null if none are required.
* @param array|null $query_params Query string parameters for the request, null if none are required.
* @return array Result from the request.
*/
public function do_rest_put_request( $url, $body_params = null, $query_params = null ) {
return $this->do_rest_request( $url, 'PUT', $body_params, $query_params );
}
}

View File

@ -6,6 +6,9 @@
* @since 3.5.0
*/
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
/**
* Class WC_Tests_API_Orders
*/
@ -51,7 +54,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
// Create 10 orders.
for ( $i = 0; $i < 10; $i++ ) {
$this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user );
$this->orders[] = OrderHelper::create_order( $this->user );
}
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) );
@ -67,8 +70,8 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
public function test_get_items_ordered_by_modified() {
wp_set_current_user( $this->user );
$order1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user );
$order2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user );
$order1 = OrderHelper::create_order( $this->user );
$order2 = OrderHelper::create_order( $this->user );
$order1->set_status( 'completed' );
$order1->save();
@ -80,7 +83,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$request->set_query_params(
array(
'orderby' => 'modified',
'order' => 'asc',
'order' => 'asc',
)
);
$response = $this->server->dispatch( $request );
@ -90,7 +93,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$request->set_query_params(
array(
'orderby' => 'modified',
'order' => 'desc',
'order' => 'desc',
)
);
$response = $this->server->dispatch( $request );
@ -105,7 +108,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_items_without_permission() {
wp_set_current_user( 0 );
$this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$this->orders[] = OrderHelper::create_order();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) );
$this->assertEquals( 401, $response->get_status() );
}
@ -116,7 +119,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_item() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$order->add_meta_data( 'key', 'value' );
$order->add_meta_data( 'key2', 'value2' );
$order->save();
@ -140,7 +143,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_item_without_permission() {
wp_set_current_user( 0 );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$this->orders[] = $order;
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) );
$this->assertEquals( 401, $response->get_status() );
@ -152,18 +155,18 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
public function test_get_item_with_line_items_meta_data() {
wp_set_current_user( $this->user );
$attribute_name = 'Site Level Type';
$site_level_attribute_id = wc_create_attribute( array( 'name' => $attribute_name ) );
$attribute_name = 'Site Level Type';
$site_level_attribute_id = wc_create_attribute( array( 'name' => $attribute_name ) );
$site_level_attribute_slug = wc_attribute_taxonomy_name_by_id( $site_level_attribute_id );
// Register the attribute so that wp_insert_term will be successful.
register_taxonomy( $site_level_attribute_slug, array( 'product' ), array() );
$term_name = 'Site Level Value - Wood';
$term_name = 'Site Level Value - Wood';
$site_level_term_insertion_result = wp_insert_term( $term_name, $site_level_attribute_slug );
$site_level_term = get_term( $site_level_term_insertion_result['term_id'] );
$site_level_term = get_term( $site_level_term_insertion_result['term_id'] );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
$variation = wc_get_product( $product->get_children()[0] );
$line_item = new WC_Order_Item_Product();
@ -172,12 +175,12 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
array( 'variation' => array( "attribute_{$site_level_attribute_slug}" => $site_level_term->slug ) )
);
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$order->add_item( $line_item );
$order->save();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) );
$data = $response->get_data();
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( $order->get_id(), $data['id'] );
@ -205,18 +208,18 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
public function test_get_item_with_variation_parent_name() {
wp_set_current_user( $this->user );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
$variation = wc_get_product( $product->get_children()[0] );
$line_item = new WC_Order_Item_Product();
$line_item->set_product( $variation );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$order->add_item( $line_item );
$order->save();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) );
$data = $response->get_data();
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( $order->get_id(), $data['id'] );
@ -248,7 +251,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_item_refund_id() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$refund = wc_create_refund(
array(
'order_id' => $order->get_id(),
@ -352,7 +355,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$this->assertEquals( 1, count( $data['line_items'] ) );
$this->assertEquals( 1, count( $data['shipping_lines'] ) );
$shipping = current( $order->get_items( 'shipping' ) );
$shipping = current( $order->get_items( 'shipping' ) );
$expected_shipping_line = array(
'id' => $shipping->get_id(),
'method_title' => $shipping->get_method_title(),
@ -542,7 +545,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_update_order() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
@ -569,7 +572,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_update_order_remove_items() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$fee = new WC_Order_Item_Fee();
$fee->set_props(
array(
@ -610,7 +613,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
public function test_update_order_after_delete_product() {
wp_set_current_user( $this->user );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( 1, $product );
$order = OrderHelper::create_order( 1, $product );
$product->delete( true );
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
@ -621,8 +624,8 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
array(
'line_items' => array(
array(
'id' => $item->get_id(),
'quantity' => 10,
'id' => $item->get_id(),
'quantity' => 10,
),
),
)
@ -652,56 +655,274 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$this->assertEquals( $expected, $data['line_items'][0] );
}
/**
* Data provider for test_update_order_add_coupons.
*
* @return array Data for test_update_order_add_coupons.
*/
public function data_provider_for_test_update_order_add_coupons() {
return array(
// Successful case, no previous coupon, it gets created.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'code' => 'fake-coupon-2',
),
),
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 200,
),
'expected_order_coupon_code_after_request' => 'fake-coupon-2',
),
// Successful case with previous coupon, it gets replaced.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'code' => 'fake-coupon-2',
),
),
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 200,
),
'expected_order_coupon_code_after_request' => 'fake-coupon-2',
),
// Bad request: invalid coupon name, no previous coupon, it doesn't get added.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'code' => 'not-existing-coupon',
),
),
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon "not-existing-coupon" does not exist!',
),
'expected_order_coupon_code_after_request' => null,
),
// Bad request: invalid coupon name, coupon existed, it's kept.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'code' => 'not-existing-coupon',
),
),
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon "not-existing-coupon" does not exist!',
),
'expected_order_coupon_code_after_request' => 'fake-coupon',
),
// Bad request: has coupon id, no previous coupon, it doesn't get added.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'id' => '1234',
'code' => 'fake-coupon-2',
),
),
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon item ID is readonly.',
),
'expected_order_coupon_code_after_request' => null,
),
// Bad request: has coupon id, previous coupon existed, it's kept.
array(
'request_body' => array(
'coupon_lines' => array(
array(
'id' => '1234',
'code' => 'fake-coupon-2',
),
),
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon item ID is readonly.',
),
'expected_order_coupon_code_after_request' => 'fake-coupon',
),
// Bad request: no coupon code, no previous coupon, it doesn't get added.
array(
'request_body' => array(
'coupon_lines' => array(
array(),
),
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon code is required.',
),
'expected_order_coupon_code_after_request' => null,
),
// Bad request: no coupon code, previous coupon existed, it's kept.
array(
'request_body' => array(
'coupon_lines' => array(
array(),
),
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 400,
'message' => 'Coupon code is required.',
),
'expected_order_coupon_code_after_request' => 'fake-coupon',
),
// Bad request: invalid input ('coupon_lines' is not an array), no previous coupon, it doesn't get added.
array(
'request_body' => array(
'coupon_lines' => 1234,
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 400,
'message' => 'Invalid parameter(s): coupon_lines',
),
'expected_order_coupon_code_after_request' => null,
),
// Bad request: invalid input ('coupon_lines' is not an array), previous coupon existed, it's kept.
array(
'request_body' => array(
'coupon_lines' => 1234,
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 400,
'message' => 'Invalid parameter(s): coupon_lines',
),
'expected_order_coupon_code_after_request' => 'fake-coupon',
),
// Bad request: invalid input ('coupon_lines' has non-array elements), no previous coupon, it doesn't get added.
array(
'request_body' => array(
'coupon_lines' => array( 1234 ),
),
'order_has_coupon_before_request' => false,
'expected_request_result' => array(
'code' => 400,
'message' => 'Invalid parameter(s): coupon_lines',
),
'expected_order_coupon_code_after_request' => null,
),
// Bad request: invalid input ('coupon_lines' has non-array elements), previous coupon existed, it's kept.
array(
'request_body' => array(
'coupon_lines' => array( 1234 ),
),
'order_has_coupon_before_request' => true,
'expected_request_result' => array(
'code' => 400,
'message' => 'Invalid parameter(s): coupon_lines',
),
'expected_order_coupon_code_after_request' => 'fake-coupon',
),
);
}
/**
* Tests updating an order and adding a coupon.
*
* @dataProvider data_provider_for_test_update_order_add_coupons
*
* @param array $request_body The body for the API request.
* @param bool $order_has_coupon_before_request If true, the order will have 'fake-coupon' applied before the API request.
* @param array $expected_request_result Expected result from the API request, with 'code' and optionally 'message'.
* @param string $expected_order_coupon_code_after_request Code of the expected applied coupon after the API request, null if it shouldn't have a coupon applied.
*
* @since 3.5.0
*/
public function test_update_order_add_coupons() {
public function test_update_order_add_coupons( $request_body, $order_has_coupon_before_request, $expected_request_result, $expected_order_coupon_code_after_request ) {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order_item = current( $order->get_items() );
$coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' );
// Create order and coupons.
$order = OrderHelper::create_order();
$original_order_amount = $order->get_total();
$coupons = array();
$coupon = CouponHelper::create_coupon( 'fake-coupon' );
$coupon->set_amount( 5 );
$coupon->save();
$coupons['fake-coupon'] = $coupon;
$coupon = CouponHelper::create_coupon( 'fake-coupon-2' );
$coupon->set_amount( 10 );
$coupon->save();
$coupons['fake-coupon-2'] = $coupon;
if ( $order_has_coupon_before_request ) {
$order->apply_coupon( $coupons['fake-coupon'] );
}
// Perform the request.
// Let's try a well-formed request first of all.
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
'coupon_lines' => array(
array(
'code' => 'fake-coupon',
),
),
)
);
$request->set_body_params( $request_body );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 1, $data['coupon_lines'] );
$this->assertEquals( '45.00', $data['total'] );
// Check the response and the actual order data after the operation.
// Let's repeat, but this time we'll specify the item ID for the coupon: this is
// a readonly property and we expect the request to fail as a result.
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
'coupon_lines' => array(
array(
'id' => 123,
'code' => 'fake-coupon',
),
),
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( $expected_request_result['code'], $response->get_status() );
$this->assertEquals( 400, $response->get_status() );
$this->assertEquals( 'woocommerce_rest_coupon_item_id_readonly', $data['code'] );
$order = wc_get_order( $order->get_id() );
$order_coupons = array_values( $order->get_coupons() );
if ( is_null( $expected_order_coupon_code_after_request ) ) {
$expected_coupon = null;
$expected_order_amount = $original_order_amount;
} else {
$expected_coupon = $coupons[ $expected_order_coupon_code_after_request ];
$expected_order_amount = number_format( 50 - $expected_coupon->get_amount(), 2 );
}
$is_ok_status = $response->get_status() < 300;
if ( $is_ok_status ) {
$this->assertEquals( $expected_order_amount, $data['total'] );
$this->assertCount( 1, $data['coupon_lines'] );
} else {
$this->assertEquals( $expected_request_result['message'], $data['message'] );
}
if ( is_null( $expected_order_coupon_code_after_request ) ) {
$this->assertEquals( '50.00', $order->get_total() );
$this->assertCount( 0, $order_coupons );
} else {
$this->assertEquals( number_format( $expected_order_amount, 2 ), $order->get_total() );
$this->assertCount( 1, $order_coupons );
$this->assertEquals( $expected_coupon->get_code(), $order_coupons[0]->get_code() );
}
}
/**
@ -711,9 +932,9 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_update_order_remove_coupons() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$order_item = current( $order->get_items() );
$coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' );
$coupon = CouponHelper::create_coupon( 'fake-coupon' );
$coupon->set_amount( 5 );
$coupon->save();
@ -723,7 +944,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
// Check that the coupon is applied.
$this->assertEquals( '45.00', $order->get_total() );
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
@ -752,7 +973,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_invalid_coupon() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
@ -779,7 +1000,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_update_order_without_permission() {
wp_set_current_user( 0 );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
@ -822,7 +1043,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_delete_order() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() );
$request->set_param( 'force', true );
$response = $this->server->dispatch( $request );
@ -837,7 +1058,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_delete_order_without_permission() {
wp_set_current_user( 0 );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() );
$request->set_param( 'force', true );
$response = $this->server->dispatch( $request );
@ -865,9 +1086,9 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
public function test_orders_batch() {
wp_set_current_user( $this->user );
$order1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order1 = OrderHelper::create_order();
$order2 = OrderHelper::create_order();
$order3 = OrderHelper::create_order();
$request = new WP_REST_Request( 'POST', '/wc/v3/orders/batch' );
$request->set_body_params(
@ -904,7 +1125,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_order_schema() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'OPTIONS', '/wc/v3/orders/' . $order->get_id() );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -919,8 +1140,8 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_order_line_items_schema() {
wp_set_current_user( $this->user );
$order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
$request = new WP_REST_Request( 'OPTIONS', '/wc/v3/orders/' . $order->get_id() );
$order = OrderHelper::create_order();
$request = new WP_REST_Request( 'OPTIONS', '/wc/v3/orders/' . $order->get_id() );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

View File

@ -10,6 +10,9 @@
*/
class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
/**
* Setup.
*/
public function setUp() {
parent::setUp();
@ -17,6 +20,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->create_session();
}
/**
* @testdox Test that save data should insert new row.
*/
public function test_save_data_should_insert_new_row() {
$current_session_data = $this->get_session_from_db( $this->session_key );
// delete session to make sure a new row is created in the DB.
@ -35,6 +41,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertEquals( array( 'cart' => 'new cart' ), wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) );
}
/**
* @testdox Test that save data should replace existing row.
*/
public function test_save_data_should_replace_existing_row() {
$current_session_data = $this->get_session_from_db( $this->session_key );
@ -49,23 +58,35 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertTrue( is_numeric( $updated_session_data->session_expiry ) );
}
/**
* @testdox Test that get_setting() should use cache.
*/
public function test_get_session_should_use_cache() {
$session = $this->handler->get_session( $this->session_key );
$this->assertEquals( array( 'cart' => 'fake cart' ), $session );
}
/**
* @testdox Test that get_setting() shouldn't use cache.
*/
public function test_get_session_should_not_use_cache() {
wp_cache_delete( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP );
$session = $this->handler->get_session( $this->session_key );
$this->assertEquals( array( 'cart' => 'fake cart' ), $session );
}
/**
* @testdox Test that get_setting() should return default value.
*/
public function test_get_session_should_return_default_value() {
$default_session = array( 'session' => 'default' );
$session = $this->handler->get_session( 'non-existent key', $default_session );
$this->assertEquals( $default_session, $session );
}
/**
* @testdox Test delete_session().
*/
public function test_delete_session() {
global $wpdb;
@ -82,6 +103,9 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertNull( $session_id );
}
/**
* @testdox Test update_session_timestamp().
*/
public function test_update_session_timestamp() {
global $wpdb;
@ -98,6 +122,14 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertEquals( $timestamp, $session_expiry );
}
/**
* @testdox Test that nonce of user logged out is only changed by WooCommerce.
*/
public function test_maybe_update_nonce_user_logged_out() {
$this->assertEquals( 1, $this->handler->maybe_update_nonce_user_logged_out( 1, 'wp_rest' ) );
$this->assertEquals( $this->handler->get_customer_unique_id(), $this->handler->maybe_update_nonce_user_logged_out( 1, 'woocommerce-something' ) );
}
/**
* Helper function to create a WC session and save it to the DB.
*/
@ -113,7 +145,7 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
/**
* Helper function to get session data from DB.
*
* @param string $session_key
* @param string $session_key Session key.
* @return stdClass
*/
protected function get_session_from_db( $session_key ) {

View File

@ -14,12 +14,22 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
* Set up
*/
public function setUp() {
// set default country to US so that 'payments' task does not get added.
// we want to remove payment tasks as they depend on installation & activation.
update_option( 'woocommerce_default_country', 'US' );
// Set default country to non-US so that 'payments' task gets added but 'woocommerce-payments' doesn't,
// by default it won't be considered completed but we can manually change that as needed.
update_option( 'woocommerce_default_country', 'JP' );
parent::setUp();
}
/**
* Tear down
*/
public function tearDown() {
remove_all_filters( 'woocommerce_available_payment_gateways' );
parent::tearDown();
}
/**
* Includes widget class and return the class.
*
@ -75,13 +85,21 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
}
/**
* Tests the widget output when 0 task has been completed.
* Tests the widget output when 1 task has been completed.
*/
public function test_initial_widget_output() {
// Force the "payments" task to be considered incomplete.
add_filter(
'woocommerce_available_payment_gateways',
function() {
return array();
}
);
$html = $this->get_widget_output();
$required_strings = array(
'Step 0 of 5',
'Step 0 of 6',
'You&#039;re almost there! Once you complete store setup you can start receiving orders.',
'Start selling',
'admin.php\?page=wc-admin&amp;path=%2Fsetup-wizard',
@ -96,9 +114,22 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
* Tests completed task count as it completes one by one
*/
public function test_widget_renders_completed_task_count() {
$completed_tasks = array();
// Force the "payments" task to be considered completed
// by faking a valid payment gateway.
add_filter(
'woocommerce_available_payment_gateways',
function() {
return array(
new class() extends WC_Payment_Gateway {
},
);
}
);
$completed_tasks = array( 'payments' );
$tasks = $this->get_widget()->get_tasks();
$tasks_count = count( $tasks );
unset( $tasks['payments'] ); // That one is completed already.
foreach ( $tasks as $key => $task ) {
array_push( $completed_tasks, $key );
update_option( 'woocommerce_task_list_tracked_completed_tasks', $completed_tasks );
@ -108,7 +139,7 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
if ( $completed_tasks_count === $tasks_count ) {
$this->assertEmpty( $this->get_widget_output() );
} else {
$this->assertRegexp( "/Step ${completed_tasks_count} of 5/", $this->get_widget_output() );
$this->assertRegexp( "/Step ${completed_tasks_count} of 6/", $this->get_widget_output() );
}
}
}
@ -122,13 +153,13 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
array(
array(
'woocommerce_task_list_complete' => 'yes',
'woocommerce_task_list_hidden' => 'no',
'woocommerce_task_list_hidden' => 'no',
),
),
array(
array(
'woocommerce_task_list_complete' => 'no',
'woocommerce_task_list_hidden' => 'yes',
'woocommerce_task_list_hidden' => 'yes',
),
),
);

View File

@ -0,0 +1,288 @@
<?php
/**
* class WC_REST_Taxes_Controller_Tests.
* Taxes Controller tests for V3 REST API.
*/
class WC_REST_Taxes_Controller_Tests extends WC_REST_Unit_Test_Case {
/**
* Runs before any test.
*/
public function setUp() {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
}
/**
* Data provider for test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes.
*
* @return array
*/
public function data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes() {
return array(
array(
array(
'city' => 'Osaka;Kyoto;Kobe',
'postcode' => '5555;7777;8888',
),
'create',
),
array(
array(
'cities' => array(
'Osaka',
'Kyoto',
'Kobe',
),
'postcodes' => array(
'5555',
'7777',
'8888',
),
),
'create',
),
array(
array(
'city' => 'Osaka;Kyoto;Kobe',
'postcode' => '5555;7777;8888',
),
'update',
),
array(
array(
'cities' => array(
'Osaka',
'Kyoto',
'Kobe',
),
'postcodes' => array(
'5555',
'7777',
'8888',
),
),
'update',
),
);
}
/**
* @testdox It is possible to create or update a tax rate passing either "city"/"postcode" (strings) or "cities"/"postcodes" (arrays) fields.
*
* @dataProvider data_provider_for_test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes
*
* @param array $request_body The body for the REST request.
* @param string $action The action to perform, 'create' or 'update'.
*/
public function test_can_create_and_update_tax_rates_with_multiple_cities_and_postcodes( $request_body, $action ) {
global $wpdb;
wp_set_current_user( $this->user );
if ( 'create' === $action ) {
$tax_rate_id = null;
$request_body = array_merge(
$request_body,
array(
'country' => 'JP',
'rate' => '1',
'name' => 'Fake Tax',
)
);
$verb = 'POST';
$url = 'taxes';
$success_status = 201;
} else {
$tax_rate_id = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_name' => 'Fake Tax',
)
);
WC_Tax::_update_tax_rate_cities( $tax_rate_id, 'Tokyo' );
WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, '0000' );
$verb = 'PUT';
$url = 'taxes/' . $tax_rate_id;
$success_status = 200;
}
$response = $this->do_rest_request( $url, $verb, $request_body );
$this->assertEquals( $success_status, $response->get_status() );
if ( ! $tax_rate_id ) {
$tax_rate_id = $response->get_data()['id'];
}
$data = $wpdb->get_results(
$wpdb->prepare(
"SELECT location_type, GROUP_CONCAT(location_code SEPARATOR ';') as items
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id=%d
GROUP BY location_type",
$tax_rate_id
),
OBJECT_K
);
$this->assertEquals( 'OSAKA;KYOTO;KOBE', $data['city']->items );
$this->assertEquals( '5555;7777;8888', $data['postcode']->items );
}
/**
* @testdox The response for tax rate(s) includes the "city"/"postcode" (strings) and "cities"/"postcodes" (arrays) fields.
*
* @testWith [true]
* [false]
*
* @param bool $request_one True to request only one tax, false to request all the taxes.
*/
public function test_get_tax_response_includes_cities_and_postcodes_as_arrays( $request_one ) {
wp_set_current_user( $this->user );
$tax_id = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_name' => 'Fake Tax',
)
);
WC_Tax::_update_tax_rate_cities( $tax_id, 'Osaka;Kyoto;Kobe' );
WC_Tax::_update_tax_rate_postcodes( $tax_id, '5555;7777;8888' );
if ( $request_one ) {
$response = $this->do_rest_get_request( 'taxes/' . $tax_id );
} else {
$response = $this->do_rest_get_request( 'taxes' );
}
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
if ( ! $request_one ) {
$data = current( $data );
}
$this->assertEquals( 'KOBE', $data['city'] );
$this->assertEquals( '8888', $data['postcode'] );
$this->assertEquals( array( 'OSAKA', 'KYOTO', 'KOBE' ), $data['cities'] );
$this->assertEquals( array( '5555', '7777', '8888' ), $data['postcodes'] );
}
/**
* @testdox The response of a REST API request for taxes can be sorted by priority.
*
* @testWith ["asc"]
* ["desc"]
*
* @param string $order_type Sort type, 'asc' or 'desc'.
*/
public function test_get_tax_response_can_be_sorted_by_priority( $order_type ) {
wp_set_current_user( $this->user );
$tax_id_1 = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_priority' => 1,
'tax_rate_name' => 'Fake Tax 1',
)
);
$tax_id_3 = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_priority' => 3,
'tax_rate_name' => 'Fake Tax 3',
)
);
$tax_id_2 = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_priority' => 2,
'tax_rate_name' => 'Fake Tax 2',
)
);
$response = $this->do_rest_get_request(
'taxes',
array(
'orderby' => 'priority',
'order' => $order_type,
)
);
$this->assertEquals( 200, $response->get_status() );
$data = array_values( $response->get_data() );
$ids = array_map(
function( $item ) {
return $item['id'];
},
$data
);
if ( 'asc' === $order_type ) {
$expected = array( $tax_id_1, $tax_id_2, $tax_id_3 );
} else {
$expected = array( $tax_id_3, $tax_id_2, $tax_id_1 );
}
$this->assertEquals( $expected, $ids );
}
/**
* @testdox Tax rates can be queries filtering by tax class.
*
* @testWith ["standard"]
* ["reduced-rate"]
* ["zero-rate"]
*
* @param string $class The tax class name to try getting the taxes for.
*/
public function test_can_get_taxes_filtering_by_class( $class ) {
wp_set_current_user( $this->user );
$classes = array( 'standard', 'reduced-rate', 'zero-rate' );
$tax_ids_by_class = array();
foreach ( $classes as $class ) {
$tax_id = WC_Tax::_insert_tax_rate(
array(
'tax_rate_country' => 'JP',
'tax_rate' => '1',
'tax_rate_priority' => 1,
'tax_rate_name' => 'Fake Tax',
'tax_rate_class' => $class,
)
);
$tax_ids_by_class[ $class ] = $tax_id;
}
$response = $this->do_rest_get_request(
'taxes',
array(
'class' => $class,
)
);
$this->assertEquals( 200, $response->get_status() );
$data = array_values( $response->get_data() );
$ids = array_map(
function( $item ) {
return $item['id'];
},
$data
);
$this->assertEquals( array( $tax_ids_by_class[ $class ] ), $ids );
}
}