diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index e5f93efa41c..152860063a4 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -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 }}" }' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..ddf9249f8df --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/pr-code-coverage.yml b/.github/workflows/pr-code-coverage.yml index 0b12def78ac..bd01d32b521 100644 --- a/.github/workflows/pr-code-coverage.yml +++ b/.github/workflows/pr-code-coverage.yml @@ -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) diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml index 427760afd13..67964501c0b 100644 --- a/.github/workflows/pr-code-sniff.yml +++ b/.github/workflows/pr-code-sniff.yml @@ -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 diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml new file mode 100644 index 00000000000..b52a926ea49 --- /dev/null +++ b/.github/workflows/stalebot.yml @@ -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 repository’s 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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2f7c3afcc8a..00000000000 --- a/.travis.yml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index b24fb96bff1..4ff98b84e02 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Latest Stable Version WordPress.org downloads WordPress.org rating -Build Status +Build Status codecov

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

+

+ Made with đź’ś by WooCommerce.
+ We're hiring! Come work with us! +

diff --git a/assets/css/twenty-twenty-one.scss b/assets/css/twenty-twenty-one.scss index 1d2b5b2215b..3091ae71eb2 100644 --- a/assets/css/twenty-twenty-one.scss +++ b/assets/css/twenty-twenty-one.scss @@ -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; + } } /** diff --git a/assets/css/twenty-twenty.scss b/assets/css/twenty-twenty.scss index ad8cdab03be..3610d94c0d4 100644 --- a/assets/css/twenty-twenty.scss +++ b/assets/css/twenty-twenty.scss @@ -2474,6 +2474,12 @@ a.reset_variations { margin: 1.5rem 0; } } + + .woocommerce-ResetPassword { + .woocommerce-form-row--first { + float: none; + } + } } /** diff --git a/assets/js/admin/wc-enhanced-select.js b/assets/js/admin/wc-enhanced-select.js index 4eac44f6371..843ce80640c 100644 --- a/assets/js/admin/wc-enhanced-select.js +++ b/assets/js/admin/wc-enhanced-select.js @@ -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 diff --git a/assets/js/admin/wc-shipping-zone-methods.js b/assets/js/admin/wc-shipping-zone-methods.js index 08e4d097d0c..45d17f19d21 100644 --- a/assets/js/admin/wc-shipping-zone-methods.js +++ b/assets/js/admin/wc-shipping-zone-methods.js @@ -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 ); } diff --git a/assets/js/frontend/cart.js b/assets/js/frontend/cart.js index b4455119c08..cdee33f5d79 100644 --- a/assets/js/frontend/cart.js +++ b/assets/js/frontend/cart.js @@ -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 ) { diff --git a/changelog.txt b/changelog.txt index 3b6a1bae8da..d2100ece059 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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 diff --git a/composer.json b/composer.json index 28e7d40c079..385182966bc 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/composer.lock b/composer.lock index 519438fad91..2eb76ff282e 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index 4c282a8cc03..e07153da6e7 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -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 ); diff --git a/includes/abstracts/abstract-wc-settings-api.php b/includes/abstracts/abstract-wc-settings-api.php index f33a62365fc..35bdbe522b7 100644 --- a/includes/abstracts/abstract-wc-settings-api.php +++ b/includes/abstracts/abstract-wc-settings-api.php @@ -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 { get_description_html( $data ); // WPCS: XSS ok. ?> diff --git a/includes/admin/class-wc-admin-assets.php b/includes/admin/class-wc-admin-assets.php index 567e2efe7ca..01e62199ba5 100644 --- a/includes/admin/class-wc-admin-assets.php +++ b/includes/admin/class-wc-admin-assets.php @@ -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' ), ) ); diff --git a/includes/admin/class-wc-admin-dashboard-setup.php b/includes/admin/class-wc-admin-dashboard-setup.php index 8fdbc1e94de..0fb9f7cda83 100644 --- a/includes/admin/class-wc-admin-dashboard-setup.php +++ b/includes/admin/class-wc-admin-dashboard-setup.php @@ -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'] ); } diff --git a/includes/admin/class-wc-admin-settings.php b/includes/admin/class-wc-admin-settings.php index b36d9a889d6..69506066749 100644 --- a/includes/admin/class-wc-admin-settings.php +++ b/includes/admin/class-wc-admin-settings.php @@ -579,6 +579,47 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : post_title, + $option_value + ); + } + ?> + + + + + + + + + object->get_id() ) ) . '">' . $this->object->get_image( 'thumbnail' ) . ''; // 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() ) : ''; } /** - * 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() ) : ''; } /** - * 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 { ?> 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, diff --git a/includes/admin/settings/class-wc-settings-emails.php b/includes/admin/settings/class-wc-settings-emails.php index 361771ecc0e..f5b1f75a0fd 100644 --- a/includes/admin/settings/class-wc-settings-emails.php +++ b/includes/admin/settings/class-wc-settings-emails.php @@ -207,6 +207,10 @@ class WC_Settings_Emails extends WC_Settings_Page { 'autoload' => false, ), + array( + 'type' => 'sectionend', + 'id' => 'email_merchant_notes', + ), ) ); diff --git a/includes/admin/settings/class-wc-settings-integrations.php b/includes/admin/settings/class-wc-settings-integrations.php index b5859b6d155..12408d80185 100644 --- a/includes/admin/settings/class-wc-settings-integrations.php +++ b/includes/admin/settings/class-wc-settings-integrations.php @@ -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 ); diff --git a/includes/admin/settings/class-wc-settings-page.php b/includes/admin/settings/class-wc-settings-page.php index b652182da23..ca359acfffb 100644 --- a/includes/admin/settings/class-wc-settings-page.php +++ b/includes/admin/settings/class-wc-settings-page.php @@ -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 '
  • ' . $label . ' ' . ( end( $array_keys ) == $id ? '' : '|' ) . '
  • '; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
  • ' . esc_html( $label ) . ' ' . ( end( $array_keys ) === $id ? '' : '|' ) . '
  • '; } echo '
    '; diff --git a/includes/admin/settings/class-wc-settings-tax.php b/includes/admin/settings/class-wc-settings-tax.php index a80f2664cfb..0c45245ab51 100644 --- a/includes/admin/settings/class-wc-settings-tax.php +++ b/includes/admin/settings/class-wc-settings-tax.php @@ -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 } } diff --git a/includes/admin/views/html-admin-page-status-report.php b/includes/admin/views/html-admin-page-status-report.php index 421040eb023..1743566555d 100644 --- a/includes/admin/views/html-admin-page-status-report.php +++ b/includes/admin/views/html-admin-page-status-report.php @@ -514,7 +514,7 @@ $untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, Cons : - + diff --git a/includes/class-wc-ajax.php b/includes/class-wc-ajax.php index 5dbd366f4a3..10e27e7e2f4 100644 --- a/includes/class-wc-ajax.php +++ b/includes/class-wc-ajax.php @@ -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. */ diff --git a/includes/class-wc-cart-session.php b/includes/class-wc-cart-session.php index d8eb30ca8e9..50f2291a68c 100644 --- a/includes/class-wc-cart-session.php +++ b/includes/class-wc-cart-session.php @@ -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. diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 08451515652..4d92bbca60e 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -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; } diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php index a36be39d9c1..f83c30b2b8f 100644 --- a/includes/class-wc-comments.php +++ b/includes/class-wc-comments.php @@ -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. * diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php index 3c5c53079f3..d66efff9d2c 100644 --- a/includes/class-wc-customer.php +++ b/includes/class-wc-customer.php @@ -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; } /** diff --git a/includes/class-wc-session-handler.php b/includes/class-wc-session-handler.php index cacda9e59a2..0c6dda12484 100644 --- a/includes/class-wc-session-handler.php +++ b/includes/class-wc-session-handler.php @@ -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. */ diff --git a/includes/class-wc-structured-data.php b/includes/class-wc-structured-data.php index 070bea646b0..1d3778864e7 100644 --- a/includes/class-wc-structured-data.php +++ b/includes/class-wc-structured-data.php @@ -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(), diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index 65a4978cc2b..a59f60cc4de 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -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' ) ); } diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php index add78763cf2..19ba9b67ff5 100644 --- a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php @@ -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[\d]+)', array( - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), - 'type' => 'integer', - ), - ), + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\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; diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php index 0517a6a18ab..d61a52cdcb3 100644 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php @@ -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; diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php index 516aee8dfb7..6fef3703eb6 100644 --- a/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php @@ -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'] ); + } + } } diff --git a/includes/shortcodes/class-wc-shortcode-products.php b/includes/shortcodes/class-wc-shortcode-products.php index d0d089f905a..25ac8223eda 100644 --- a/includes/shortcodes/class-wc-shortcode-products.php +++ b/includes/shortcodes/class-wc-shortcode-products.php @@ -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; diff --git a/includes/wc-order-functions.php b/includes/wc-order-functions.php index 2047a6969a8..a17bae8244a 100644 --- a/includes/wc-order-functions.php +++ b/includes/wc-order-functions.php @@ -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 ); } } } diff --git a/lib/composer.json b/lib/composer.json index 7e9e4481f95..f3fa6563375 100644 --- a/lib/composer.json +++ b/lib/composer.json @@ -8,7 +8,7 @@ "psr/container": "^1.0" }, "require-dev": { - "league/container": "3.3.3" + "league/container": "3.3.5" }, "config": { "platform": { diff --git a/lib/composer.lock b/lib/composer.lock index ac31c819004..bff7bf66ac0 100644 --- a/lib/composer.lock +++ b/lib/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "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": [], diff --git a/package-lock.json b/package-lock.json index a23aaf9bbeb..2ec65648a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 diff --git a/readme.txt b/readme.txt index c18045352cd..b230c7250c7 100644 --- a/readme.txt +++ b/readme.txt @@ -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 diff --git a/templates/cart/mini-cart.php b/templates/cart/mini-cart.php index 57c73ff56d9..a3c1ecf2e4e 100644 --- a/templates/cart/mini-cart.php +++ b/templates/cart/mini-cart.php @@ -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' ); ?> ); ?> - + - + diff --git a/templates/checkout/form-pay.php b/templates/checkout/form-pay.php index b9244b6e210..e7bdd506ff3 100644 --- a/templates/checkout/form-pay.php +++ b/templates/checkout/form-pay.php @@ -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 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 ); diff --git a/templates/checkout/review-order.php b/templates/checkout/review-order.php index e85d43dc39c..576a8648387 100644 --- a/templates/checkout/review-order.php +++ b/templates/checkout/review-order.php @@ -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; ?> - get_name(), $cart_item, $cart_item_key ) . ' '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + get_name(), $cart_item, $cart_item_key ) ) . ' '; ?> ' . sprintf( '× %s', $cart_item['quantity'] ) . '', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> diff --git a/templates/content-widget-reviews.php b/templates/content-widget-reviews.php index d46cbd5873f..d16368479ae 100644 --- a/templates/content-widget-reviews.php +++ b/templates/content-widget-reviews.php @@ -21,14 +21,27 @@ defined( 'ABSPATH' ) || exit;
  • + + get_image(); ?> - get_name(); ?> + get_name() ); ?> comment_ID, 'rating', true ) ) ); ?> - comment_ID ) ); ?> + + comment_ID ) ); + ?> + + +
  • diff --git a/templates/emails/plain/email-order-items.php b/templates/emails/plain/email-order-items.php index bcd446630e9..61cf3fdbd52 100644 --- a/templates/emails/plain/email-order-items.php +++ b/templates/emails/plain/email-order-items.php @@ -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 ) ); } diff --git a/templates/order/order-details-item.php b/templates/order/order-details-item.php index 7894d37b92f..271b94c8892 100644 --- a/templates/order/order-details-item.php +++ b/templates/order/order-details-item.php @@ -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( '%s', $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( '%s', $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 ); diff --git a/tests/README.md b/tests/README.md index e15a9485241..099f6f4c6d0 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 `` 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. diff --git a/tests/bin/phpcs.sh b/tests/bin/phpcs.sh index bcaa0b96b17..27ccd170f15 100755 --- a/tests/bin/phpcs.sh +++ b/tests/bin/phpcs.sh @@ -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 diff --git a/tests/bin/travis.sh b/tests/bin/travis.sh deleted file mode 100755 index 2c5d161160e..00000000000 --- a/tests/bin/travis.sh +++ /dev/null @@ -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 diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 9a56a1e8641..30dbdb08310 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -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). diff --git a/tests/e2e/api/CHANGELOG.md b/tests/e2e/api/CHANGELOG.md index 0b14882e8e0..a2c57debaf4 100644 --- a/tests/e2e/api/CHANGELOG.md +++ b/tests/e2e/api/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased + +# 0.1.2 + ## Added - Support for the external product type. diff --git a/tests/e2e/api/README.md b/tests/e2e/api/README.md index 78b51205d3b..7eadd2d5de1 100644 --- a/tests/e2e/api/README.md +++ b/tests/e2e/api/README.md @@ -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" + } + ] +}); ``` diff --git a/tests/e2e/api/package-lock.json b/tests/e2e/api/package-lock.json index 17f36f2a80b..8fe554d6176 100644 --- a/tests/e2e/api/package-lock.json +++ b/tests/e2e/api/package-lock.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/api", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/tests/e2e/api/package.json b/tests/e2e/api/package.json index 976f39c82e9..933c01fd09d 100644 --- a/tests/e2e/api/package.json +++ b/tests/e2e/api/package.json @@ -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", diff --git a/tests/e2e/api/src/models/products/abstract/common.ts b/tests/e2e/api/src/models/products/abstract/common.ts index 88601fa6ad0..d9a4a3525db 100644 --- a/tests/e2e/api/src/models/products/abstract/common.ts +++ b/tests/e2e/api/src/models/products/abstract/common.ts @@ -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. */ diff --git a/tests/e2e/api/src/repositories/rest/products/external-product.ts b/tests/e2e/api/src/repositories/rest/products/external-product.ts index f2b323ad7b8..bd78edce17f 100644 --- a/tests/e2e/api/src/repositories/rest/products/external-product.ts +++ b/tests/e2e/api/src/repositories/rest/products/external-product.ts @@ -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 ), ); } diff --git a/tests/e2e/api/src/repositories/rest/products/grouped-product.ts b/tests/e2e/api/src/repositories/rest/products/grouped-product.ts index 94f1bef6a89..c2a9a4dd338 100644 --- a/tests/e2e/api/src/repositories/rest/products/grouped-product.ts +++ b/tests/e2e/api/src/repositories/rest/products/grouped-product.ts @@ -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 ), ); } diff --git a/tests/e2e/api/src/repositories/rest/products/simple-product.ts b/tests/e2e/api/src/repositories/rest/products/simple-product.ts index b4f9b5d0d2d..fb83634b643 100644 --- a/tests/e2e/api/src/repositories/rest/products/simple-product.ts +++ b/tests/e2e/api/src/repositories/rest/products/simple-product.ts @@ -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 ), ); } diff --git a/tests/e2e/api/src/repositories/rest/products/variable-product.ts b/tests/e2e/api/src/repositories/rest/products/variable-product.ts index 9133b4ccdee..2868ebfbeca 100644 --- a/tests/e2e/api/src/repositories/rest/products/variable-product.ts +++ b/tests/e2e/api/src/repositories/rest/products/variable-product.ts @@ -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 ), ); } diff --git a/tests/e2e/core-tests/CHANGELOG.md b/tests/e2e/core-tests/CHANGELOG.md index 7186fa300ff..614f8691dde 100644 --- a/tests/e2e/core-tests/CHANGELOG.md +++ b/tests/e2e/core-tests/CHANGELOG.md @@ -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 diff --git a/tests/e2e/core-tests/README.md b/tests/e2e/core-tests/README.md index f1d9381349c..c6b162afa1f 100644 --- a/tests/e2e/core-tests/README.md +++ b/tests/e2e/core-tests/README.md @@ -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 diff --git a/tests/e2e/core-tests/package.json b/tests/e2e/core-tests/package.json index f94e1081d3c..04dacc5d626 100644 --- a/tests/e2e/core-tests/package.json +++ b/tests/e2e/core-tests/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/e2e-core-tests", - "version": "0.1.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" diff --git a/tests/e2e/core-tests/specs/api/grouped-product.test.js b/tests/e2e/core-tests/specs/api/grouped-product.test.js index d1f12567305..7ad4f2f2bd7 100644 --- a/tests/e2e/core-tests/specs/api/grouped-product.test.js +++ b/tests/e2e/core-tests/specs/api/grouped-product.test.js @@ -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 ); + }); }); }); }; diff --git a/tests/e2e/core-tests/specs/index.js b/tests/e2e/core-tests/specs/index.js index b9b0f1381e3..110a80079fd 100644 --- a/tests/e2e/core-tests/specs/index.js +++ b/tests/e2e/core-tests/specs/index.js @@ -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 }; diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-analytics-page-loads.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-analytics-page-loads.test.js new file mode 100644 index 00000000000..15c7cda6b3c --- /dev/null +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-analytics-page-loads.test.js @@ -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; diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-customer-payment-page.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-customer-payment-page.test.js index 472c8e59fcc..8c597d3ed5a 100644 --- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-customer-payment-page.test.js +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-customer-payment-page.test.js @@ -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 diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js index ce392d06303..465d68583a7 100644 --- a/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-order-searching.test.js @@ -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 () => { diff --git a/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js b/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js index 1a49f147158..e7da3ffe377 100644 --- a/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js +++ b/tests/e2e/core-tests/specs/merchant/wp-admin-settings-shipping-zones.test.js @@ -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'); diff --git a/tests/e2e/core-tests/specs/shopper/front-end-checkout-create-account.test.js b/tests/e2e/core-tests/specs/shopper/front-end-checkout-create-account.test.js new file mode 100644 index 00000000000..4ccaab02e06 --- /dev/null +++ b/tests/e2e/core-tests/specs/shopper/front-end-checkout-create-account.test.js @@ -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; diff --git a/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js b/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js new file mode 100644 index 00000000000..c48e9cd9e9f --- /dev/null +++ b/tests/e2e/core-tests/specs/shopper/front-end-my-account-create-account.test.js @@ -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; diff --git a/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js b/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js index 1b0535bd76d..f18856a8fd1 100644 --- a/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js +++ b/tests/e2e/core-tests/specs/shopper/front-end-product-browse-search-sort.test.js @@ -4,9 +4,7 @@ */ const { shopper, - merchant, createSimpleProductWithCategory, - uiUnblocked, } = require( '@woocommerce/e2e-utils' ); /** diff --git a/tests/e2e/docker/init-wp-beta.sh b/tests/e2e/docker/init-wp-beta.sh index 6b44d74d0e6..d62e039d676 100755 --- a/tests/e2e/docker/init-wp-beta.sh +++ b/tests/e2e/docker/init-wp-beta.sh @@ -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 diff --git a/tests/e2e/env/CHANGELOG.md b/tests/e2e/env/CHANGELOG.md index 67cfcb4979a..4b0fa6d81e5 100644 --- a/tests/e2e/env/CHANGELOG.md +++ b/tests/e2e/env/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased + +# 0.2.1 + ## Added - Support for screenshots on test errors diff --git a/tests/e2e/env/README.md b/tests/e2e/env/README.md index 6ab21205bab..121e52440f1 100644 --- a/tests/e2e/env/README.md +++ b/tests/e2e/env/README.md @@ -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) diff --git a/tests/e2e/env/builtin.md b/tests/e2e/env/builtin.md index e02293d876e..7161bc80a4e 100644 --- a/tests/e2e/env/builtin.md +++ b/tests/e2e/env/builtin.md @@ -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 diff --git a/tests/e2e/env/external.md b/tests/e2e/env/external.md index 45da1345f61..12334f89379 100644 --- a/tests/e2e/env/external.md +++ b/tests/e2e/env/external.md @@ -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 .... diff --git a/tests/e2e/env/package.json b/tests/e2e/env/package.json index 210eba5dcc8..1e59ac7cd5e 100644 --- a/tests/e2e/env/package.json +++ b/tests/e2e/env/package.json @@ -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", diff --git a/tests/e2e/specs/front-end/test-checkout-create-account.js b/tests/e2e/specs/front-end/test-checkout-create-account.js new file mode 100644 index 00000000000..58cdf32b230 --- /dev/null +++ b/tests/e2e/specs/front-end/test-checkout-create-account.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runCheckoutCreateAccountTest } = require( '@woocommerce/e2e-core-tests' ); + +runCheckoutCreateAccountTest(); diff --git a/tests/e2e/specs/front-end/test-my-account-create-account.js b/tests/e2e/specs/front-end/test-my-account-create-account.js new file mode 100644 index 00000000000..cfcbfdc780b --- /dev/null +++ b/tests/e2e/specs/front-end/test-my-account-create-account.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runMyAccountCreateAccountTest } = require( '@woocommerce/e2e-core-tests' ); + +runMyAccountCreateAccountTest(); diff --git a/tests/e2e/specs/wp-admin/test-analytics-page-loads.js b/tests/e2e/specs/wp-admin/test-analytics-page-loads.js new file mode 100644 index 00000000000..6113889de83 --- /dev/null +++ b/tests/e2e/specs/wp-admin/test-analytics-page-loads.js @@ -0,0 +1,6 @@ +/* + * Internal dependencies + */ +const { runAnalyticsPageLoadsTest } = require( '@woocommerce/e2e-core-tests' ); + +runAnalyticsPageLoadsTest(); diff --git a/tests/e2e/utils/CHANGELOG.md b/tests/e2e/utils/CHANGELOG.md index fd7daaab9d2..e430ce8df00 100644 --- a/tests/e2e/utils/CHANGELOG.md +++ b/tests/e2e/utils/CHANGELOG.md @@ -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 diff --git a/tests/e2e/utils/README.md b/tests/e2e/utils/README.md index 11fabf1e36a..95830e44f59 100644 --- a/tests/e2e/utils/README.md +++ b/tests/e2e/utils/README.md @@ -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. diff --git a/tests/e2e/utils/package.json b/tests/e2e/utils/package.json index 7564461a3f7..fb359ff1a9e 100644 --- a/tests/e2e/utils/package.json +++ b/tests/e2e/utils/package.json @@ -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" diff --git a/tests/e2e/utils/src/components.js b/tests/e2e/utils/src/components.js index cc6de0161fe..c449622937b 100644 --- a/tests/e2e/utils/src/components.js +++ b/tests/e2e/utils/src/components.js @@ -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   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(); }; /** diff --git a/tests/e2e/utils/src/factories/simple-product.js b/tests/e2e/utils/src/factories/simple-product.js index 39a421ba820..c9dd0315409 100644 --- a/tests/e2e/utils/src/factories/simple-product.js +++ b/tests/e2e/utils/src/factories/simple-product.js @@ -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'; /** diff --git a/tests/e2e/utils/src/flows/constants.js b/tests/e2e/utils/src/flows/constants.js index cb743536d2b..bcf4d6b3419 100644 --- a/tests/e2e/utils/src/flows/constants.js +++ b/tests/e2e/utils/src/flows/constants.js @@ -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='; diff --git a/tests/e2e/utils/src/flows/merchant.js b/tests/e2e/utils/src/flows/merchant.js index c12fd64ed6f..d8c4c7ad390 100644 --- a/tests/e2e/utils/src/flows/merchant.js +++ b/tests/e2e/utils/src/flows/merchant.js @@ -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; diff --git a/tests/legacy/framework/class-wc-rest-unit-test-case.php b/tests/legacy/framework/class-wc-rest-unit-test-case.php index 5ad76c053e7..013ec0ace8d 100644 --- a/tests/legacy/framework/class-wc-rest-unit-test-case.php +++ b/tests/legacy/framework/class-wc-rest-unit-test-case.php @@ -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 ); + } } diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php index fcd4a145f58..1e460f2793e 100644 --- a/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php @@ -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(); diff --git a/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php b/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php index 4149f90fbb6..7af97b30165 100644 --- a/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php +++ b/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php @@ -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 ) { diff --git a/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php index 82ebf87cc05..83b678a86b9 100644 --- a/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php +++ b/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php @@ -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're almost there! Once you complete store setup you can start receiving orders.', 'Start selling', 'admin.php\?page=wc-admin&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', ), ), ); diff --git a/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php new file mode 100644 index 00000000000..e991716cc93 --- /dev/null +++ b/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller-tests.php @@ -0,0 +1,288 @@ +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 ); + } +}