Merge branch 'trunk' into pr/31317

This commit is contained in:
vedanshujain 2022-01-14 17:20:46 +05:30
commit 7b8f3be261
477 changed files with 23340 additions and 7501 deletions

View File

@ -1,3 +1,4 @@
module.exports = {
root: true,
extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
};

View File

@ -1,12 +0,0 @@
---
name: "\U0001F512 Security issue"
about: Please report security issues *only* via https://www.hackerone.com
title: ''
labels: ''
assignees: ''
---
For security reasons, please report all security issues via https://hackerone.com/automattic/. Also, if the issue is valid, a bug bounty will be paid out to you.
Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released).

95
.github/ISSUE_TEMPLATE/1-bug-report.yml vendored Normal file
View File

@ -0,0 +1,95 @@
name: 🐞 Bug Report
description: Report a bug if something isn't working as expected in WooCommerce Core.
body:
- type: markdown
attributes:
value: |
### Thanks for contributing!
Please provide us with the information requested in this bug report.
Without these details, we won't be able to fully evaluate this issue.
Bug reports lacking detail, or for any other reason than to report a bug, may be closed without action.
While our goal is to address all the issues reported in this repository, GitHub should be treated as a place to report confirmed bugs only.
- If you have a support request or custom code related question please follow one of the steps below:
- Review [WooCommerce Self-Service Guide](https://woocommerce.com/document/woocommerce-self-service-guide/) to see if the solutions listed there apply to your case;
- If you are a paying customer of WooCommerce, contact WooCommerce support by [opening a ticket or starting a live chat](https://woocommerce.com/contact-us/);
- Make a post on [WooCommerce community forum](https://wordpress.org/support/plugin/woocommerce/)
- To get help on custom code questions go to the [WooCommerce Community Slack](https://woocommerce.com/community-slack/) and visit the `#developers` channel.
Make sure to look through the [existing `type: bug` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22) to see whether your bug has already been submitted.
Feel free to contribute to any existing issues.
Search tip: You can filter our issues using [our labels](https://github.com/woocommerce/woocommerce/labels).
Search tip: Make use of [GitHub's search syntax to refine your search](https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests).
- type: checkboxes
id: prerequisites
attributes:
label: Prerequisites
description: Please confirm these before submitting the issue.
options:
- label: I have carried out troubleshooting steps and I believe I have found a bug.
- label: I have searched for similar bugs in both open and closed issues and cannot find a duplicate.
validations:
required: true
- type: textarea
id: summary
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
placeholder: |
A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
placeholder: |
A clear and concise description of what actually happens. Please be as descriptive as possible;
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Attach screenshot(s) or recording(s) directly by dragging & dropping.
placeholder: |
1. Go to
2. Click on
3. Scroll down to
4. See error
validations:
required: true
- type: textarea
id: environment
attributes:
label: WordPress Environment
description: |
We use the [WooCommerce System Status Report](https://woocommerce.com/document/understanding-the-woocommerce-system-status-report/) to help us evaluate the issue.
Without this report we won't be able to fully evaluate this issue.
placeholder: |
The System Status Report is found in your WordPress admin under **WooCommerce > Status**.
Please select “Get system report”, then “Copy for support”, and then paste it here.
validations:
required: true
- type: checkboxes
id: isolating
attributes:
label: Isolating the problem
description: |
Please try testing your site for theme and plugins conflict.
To do that deactivate all plugins except for WooCommerce and switch to a default WordPress theme or [Storefront](https://en-gb.wordpress.org/themes/storefront/). Then test again.
If the issue is resolved with the default theme and all plugins deactivated, it means that one of your plugins or a theme is causing the issue.
You will then need to enable it one by one and test every time you do that in order to figure out which plugin is causing the issue.
options:
- label: I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active.
- label: This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/).
- label: I can reproduce this bug consistently using the steps above.
validations:
required: true

View File

@ -1,20 +0,0 @@
---
name: "\U0001F47D External issues"
about: Please report WooCommerce Admin, WooCommerce Gutenberg Products Blocks or Action Scheduler issues directly to their respective repositories.
title: ''
labels: ''
assignees: ''
---
Please report issues for the following features directly to their respective repositories.
WooCommerce Admin: https://github.com/woocommerce/woocommerce-admin
WooCommerce Gutenberg Products Blocks: https://github.com/woocommerce/woocommerce-gutenberg-products-block
Action Scheduler: https://github.com/woocommerce/action-scheduler
WooCommerce REST API Docs: https://github.com/woocommerce/woocommerce-rest-api-docs
WooCommerce Code Reference: https://github.com/woocommerce/code-reference

View File

@ -0,0 +1,33 @@
name: ✨ Enhancement Request
description: If you have an idea to improve an existing feature in core or need something for development (such as a new hook) please let us know or submit a Pull Request!
title: "[Enhancement]: "
labels: ["type: enhancement"]
body:
- type: markdown
attributes:
value: |
### Thanks for contributing!
Please provide us with the information requested in this form.
Make sure to look through [existing `type: enhancement` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+enhancement%22) and [existing `votes needed` issues](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3A%22votes+needed%22) to see whether your idea is already being discussed.
Feel free to contribute to any existing issues.
Search tip: You can filter our issues using [our enhancement label](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3A%22type%3A+enhancement%22+).
Search tip: Make use of [GitHub's search syntax to refine your search](https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests).
- type: textarea
id: summary
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.

View File

@ -1,28 +0,0 @@
---
name: "❓ Support Question"
about: "If you have a question \U0001F4AC please see our docs or use our forums, helpdesk,
or Slack Community!"
title: ''
labels: ''
assignees: ''
---
We don't offer technical support on GitHub so we recommend using the following:
**Reading our documentation**
Usage docs can be found here: https://docs.woocommerce.com/
If you have a problem, you may want to start with the self help guide here: https://docs.woocommerce.com/document/woocommerce-self-service-guide/
**Technical support for premium extensions or if you're a WooCommerce.com customer**
Contact WooCommerce support by opening a ticket.
https://woocommerce.com/contact-us/
**For help with custom code**
WooCommerce Slack Community: https://woocommerce.com/community-slack/ in the `#developers` channel.
**General usage and development questions**
- WooCommerce Slack Community: https://woocommerce.com/community-slack/
- WordPress.org Forums: https://wordpress.org/support/plugin/woocommerce
- The Official WooCommerce Facebook Group https://www.facebook.com/groups/advanced.woocommerce/

View File

@ -1,58 +0,0 @@
---
name: "\U0001F41E Bug report"
about: Report a bug if something isn't working as expected in the core WooCommerce
plugin.
title: ''
labels: ''
assignees: ''
---
Please provide us with the information requested in this bug report. Without these details, we won't be able to fully evaluate this issue.
Bug reports lacking detail, or for any other reason than to report a bug, may be closed without action.
<!-- This template is for confirmed bugs only. If you have a support request or custom code related question please see our docs or use our forums, helpdesk, or Slack Community! https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=&template=3-Support.md&title= -->
<!-- Make sure to look through the existing issues to see whether your bug has already been submitted. Feel free to contribute to any existing issues. -->
<!-- Search tip: You can filter our issues using our component labels https://github.com/woocommerce/woocommerce/labels?q=component -->
<!-- Search tip: Make use of GitHub's search syntax to refine your search https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests -->
**Prerequisites (mark completed items with an [x]):**
- [ ] I have carried out troubleshooting steps and I believe I have found a bug.
- [ ] I have searched for similar bugs in both open and closed issues and cannot find a duplicate.
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
A clear and concise description of what actually happens. Please be as descriptive as possible;
**Steps to reproduce the bug (We need to be able to reproduce the bug in order to fix it.)**
Steps to reproduce the bug:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots**
If applicable, add screenshots to help explain your problem.
<!-- Please try testing your site for theme and plugins conflict. To do that deactivate all plugins except for WooCommerce and switch to a default WordPress theme or [Storefront](https://en-gb.wordpress.org/themes/storefront/). Then test again. If the issue is resolved with the default theme and all plugins deactivated, it means that one of your plugins or a theme is causing the issue. You will then need to enable it one by one and test every time you do that in order to figure out which plugin is causing the issue. -->
**Isolating the problem (mark completed items with an [x]):**
- [ ] I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active.
- [ ] This bug happens with a default WordPress theme active, or [Storefront](https://woocommerce.com/storefront/).
- [ ] I can reproduce this bug consistently using the steps above.
**WordPress Environment**
We use the [WooCommerce System Status Report](https://docs.woocommerce.com/document/understanding-the-woocommerce-system-status-report/) to help us evaluate the issue.
Without this report we won't be able to fully evaluate this issue.
<details>
```
The System Status Report is found in your WordPress admin under **WooCommerce > Status**.
Please select “Get system report”, then “Copy for support”, and then paste it here.
```
</details>

View File

@ -1,25 +0,0 @@
---
name: "✨ New Enhancement"
about: If you have an idea to improve an existing feature in core or need something
for development (such as a new hook) please let us know or submit a Pull Request!
title: ''
labels: ''
assignees: ''
---
<!-- Make sure to look through existing issues to see whether your idea is already being discussed. Feel free to contribute to any existing issues. -->
<!-- Search tip: You can filter issues using our enhancement label https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3Aenhancement -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,25 +0,0 @@
---
name: "\U0001F680 Feature request"
about: "Suggest a new feature \U0001F389 We'll consider building it if it receives
sufficient interest! \U0001F44D"
title: ''
labels: ''
assignees: ''
---
<!-- Make sure to look through existing issues to see whether your idea is already being discussed. Feel free to contribute to any existing issues. -->
<!-- Search tip: You can filter issues using our enhancement label https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+label%3Aenhancement -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,14 @@
blank_issues_enabled: true
contact_links:
- name: 🔒 Security issue
url: https://hackerone.com/automattic/
about: For security reasons, please report all security issues via HackerOne. If the issue is valid, a bug bounty will be paid out to you. Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released).
- name: ❓ Support Question
url: https://woocommerce.com/document/woocommerce-self-service-guide/
about: If you have a question please see our docs or use our forums, helpdesk, or Slack community!
- name: WooCommerce Admin
url: https://github.com/woocommerce/woocommerce-admin
about: Please report issues for WooCommerce Admin (such as Analytics and Onboarding) directly to it's repository.
- name: WooCommerce Blocks
url: https://github.com/woocommerce/woocommerce-gutenberg-products-block
about: Please report issues for WooCommerce Blocks directly to it's repository.

View File

@ -17,7 +17,7 @@ jobs:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Build the zip file
id: build
uses: woocommerce/action-build@v2
uses: woocommerce/action-build@trunk
- name: Unzip the file (prevents double zip problem)
run: unzip ${{ steps.build.outputs.zip_path }} -d zipfile
- name: Upload the zip file as an artifact

View File

@ -11,7 +11,7 @@ jobs:
uses: actions/checkout@v2
- name: Build
id: build
uses: woocommerce/action-build@v2
uses: woocommerce/action-build@trunk
- name: Upload release asset
uses: actions/upload-release-asset@v1
env:

View File

@ -59,8 +59,13 @@ jobs:
./vendor
key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }}
- name: Install PNPM and install dependencies
run: |
npm install -g pnpm
pnpm install
- name: Setup and install composer
run: composer install
run: pnpm nx composer-install woocommerce
- name: Add PHP8 Compatibility.
run: |
@ -71,11 +76,11 @@ jobs:
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
rm -rf ./vendor/phpunit/
composer dump-autoload
pnpm nx composer-dump-autoload woocommerce
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
run: pnpm nx test-unit woocommerce

70
.github/workflows/mirrors.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: Mirrors
on:
push:
branches: ['trunk', 'release/**']
jobs:
build:
name: Build WooCommerce zip
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build
id: build
uses: woocommerce/action-build@trunk
env:
BUILD_ENV: mirrors
- name: Upload PR zip
uses: actions/upload-artifact@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce
path: ${{ steps.build.outputs.zip_path }}
retention-days: 7
mirror:
name: Push to Mirror
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Create directories
run: |
mkdir -p tmp/woocommerce-build
mkdir -p monorepo
- name: Checkout monorepo
uses: actions/checkout@v2
with:
path: monorepo
- name: Download WooCommerce ZIP
uses: actions/download-artifact@v2
with:
name: woocommerce
path: tmp/woocommerce-build
- name: Extract and replace WooCommerce zip.
working-directory: tmp/woocommerce-build
run: |
mkdir -p woocommerce/woocommerce-production
unzip woocommerce.zip -d woocommerce/woocommerce-production
mv woocommerce/woocommerce-production/woocommerce/* woocommerce/woocommerce-production
rm -rf woocommerce/woocommerce-production/woocommerce
- name: Set up mirror
working-directory: tmp/woocommerce-build
run: |
touch mirrors.txt
echo "woocommerce/woocommerce-production" >> mirrors.txt
- name: Push to mirror
uses: Automattic/action-push-to-mirrors@v1
with:
source-directory: ${{ github.workspace }}/monorepo
token: ${{ secrets.API_TOKEN_GITHUB }}
username: matticbot
working-directory: ${{ github.workspace }}/tmp/woocommerce-build
timeout-minutes: 5 # 2021-01-18: Successful runs seem to take about half a minute.

View File

@ -11,6 +11,8 @@ jobs:
- name: Build
id: build
uses: woocommerce/action-build@trunk
env:
BUILD_ENV: e2e
- name: Upload PR zip
uses: actions/upload-artifact@v2
@ -41,8 +43,8 @@ jobs:
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
npm install -g pnpm
pnpm install
- name: Load docker images and start containers.
working-directory: package/woocommerce/plugins/woocommerce
@ -66,8 +68,8 @@ jobs:
- name: Install dependencies again
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
npm install -g pnpm
pnpm install
- name: Run tests command.
working-directory: package/woocommerce/plugins/woocommerce
@ -75,9 +77,60 @@ jobs:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.E2E_SLACK_CHANNEL }}
run: pnpx wc-e2e test:e2e
api-tests-run:
name: Runs API tests.
runs-on: ubuntu-18.04
needs: [build]
steps:
- name: Create dirs.
run: |
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
- name: Checkout code.
uses: actions/checkout@v2
with:
path: package/woocommerce
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Load docker images and start containers.
working-directory: package/woocommerce/plugins/woocommerce
run: pnpx wc-e2e docker:up
- name: Move current directory to code. We will install zip file in this dir later.
run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce
- name: Download WooCommerce ZIP.
uses: actions/download-artifact@v2
with:
name: woocommerce
path: tmp
- name: Extract and replace WooCommerce zip.
working-directory: tmp
run: |
unzip woocommerce.zip -d woocommerce
mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/
- name: Install dependencies again
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Run tests command.
working-directory: package/woocommerce/plugins/woocommerce
env:
BASE_URL: ${{ secrets.PR_E2E_TEST_URL }}
USER_KEY: ${{ secrets.PR_E2E_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.PR_E2E_TEST_ADMIN_PASSWORD }}
run: |
pnpx wc-e2e test:e2e
pnpx wc-api-tests test api
run: pnpx wc-api-tests test api

View File

@ -45,15 +45,20 @@ jobs:
./vendor
key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }}
- name: Install PNPM and install dependencies
run: |
npm install -g pnpm
pnpm install
- name: Setup and install composer
run: composer install
run: pnpm nx composer-install woocommerce
- name: Init DB and WP
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 latest
run: pnpm nx install-unit-test-db woocommerce
- name: Run unit tests with code coverage. Allow to fail.
run: |
RUN_CODE_COVERAGE=1 bash ./tests/bin/phpunit.sh
pnpm nx test-code-coverage woocommerce
exit 0
- name: Send code coverage to Codecov.

View File

@ -35,8 +35,13 @@ jobs:
./vendor
key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }}
- name: Install PNPM and install dependencies
run: |
npm install -g pnpm
pnpm install
- name: Setup and install composer
run: composer install
run: pnpm nx composer-install woocommerce
- name: Run code sniff
continue-on-error: true

View File

@ -13,10 +13,10 @@ jobs:
steps:
- name: Create dirs.
run: |
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
- name: Checkout code.
uses: actions/checkout@v2
@ -25,15 +25,48 @@ jobs:
- name: Install prerequisites.
working-directory: package/woocommerce/plugins/woocommerce
id: installation
run: |
npm install -g pnpm
pnpm install
composer install --no-dev
pnpm run build:assets
pnpm nx composer-install-no-dev woocommerce
pnpm nx build-assets woocommerce
pnpm install jest
- name: Run smoke test.
working-directory: package/woocommerce/plugins/woocommerce
if: always()
env:
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }}
SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }}
SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
WC_E2E_SCREENSHOTS: 1
E2E_RETEST: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
UPDATE_WC: 1
DEFAULT_TIMEOUT_OVERRIDE: 120000
run: |
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js
- name: Post Smoke tests results comment on PR
if: always()
uses: actions/github-script@v5
env:
TITLE: 'Smoke Test Results'
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }}
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const script = require( './package/woocommerce/packages/js/e2e-environment/bin/post-results-to-github-pr.js' )
await script({github, context})
- name: Run E2E tests.
working-directory: package/woocommerce/plugins/woocommerce
if: always()
env:
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }}
SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
@ -48,11 +81,22 @@ jobs:
UPDATE_WC: 1
DEFAULT_TIMEOUT_OVERRIDE: 120000
run: |
pnpx wc-e2e test:e2e plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpx wc-e2e test:e2e
- name: Post E2E tests results comment on PR
if: always()
uses: actions/github-script@v5
env:
TITLE: 'E2E Test Results'
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }}
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const script = require( './package/woocommerce/packages/js/e2e-environment/bin/post-results-to-github-pr.js' )
await script({github, context})
- name: Remove label from pull request.
if: "${{ contains(github.event.pull_request.labels.*.name, 'run: smoke tests') }}"
if: always() && ${{ contains( github.event.pull_request.labels.*.name, format('run{0} smoke tests', ':')) }}
uses: actions-ecosystem/action-remove-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -56,8 +56,11 @@ jobs:
./vendor
key: ${{ runner.os }}-${{ hashFiles('./composer.lock') }}
- name: Setup and install composer
run: composer install
- name: Install PNPM and install dependencies
run: |
npm install -g pnpm
pnpm install
pnpm nx composer-install woocommerce
- name: Add PHP8 Compatibility.
run: |
@ -75,4 +78,4 @@ jobs:
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
run: pnpm nx test-unit woocommerce

View File

@ -26,8 +26,8 @@ jobs:
run: |
npm install -g pnpm
pnpm install
composer install --no-dev
pnpm run build:assets
pnpm nx composer-install-no-dev woocommerce
pnpm nx build-assets woocommerce
pnpm install jest
- name: Run smoke test.
@ -49,6 +49,106 @@ jobs:
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
run: |
pnpx wc-e2e test:e2e plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpx wc-e2e test:e2e
pnpx wc-api-tests test api
build:
name: Build zip for PR
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build
id: build
uses: woocommerce/action-build@trunk
env:
BUILD_ENV: e2e
- name: Upload PR zip
uses: actions/upload-artifact@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce
path: ${{ steps.build.outputs.zip_path }}
retention-days: 7
test-plugins:
name: Smoke tests with ${{ matrix.plugin }} plugin installed
runs-on: ubuntu-18.04
needs: [build]
strategy:
fail-fast: false
matrix:
include:
- plugin: 'WooCommerce Payments'
repo: 'automattic/woocommerce-payments'
- plugin: 'WooCommerce PayPal Payments'
repo: 'woocommerce/woocommerce-paypal-payments'
- plugin: 'WooCommerce Shipping & Tax'
repo: 'woocommerce/woocommerce-services'
- plugin: 'WooCommerce Subscriptions'
repo: WC_SUBSCRIPTIONS_REPO
private: true
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
repo: 'Yoast/wordpress-seo'
- plugin: 'Contact Form 7'
repo: 'takayukister/contact-form-7'
steps:
- name: Create dirs.
run: |
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
- name: Checkout code.
uses: actions/checkout@v2
with:
path: package/woocommerce
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Load docker images and start containers.
working-directory: package/woocommerce/plugins/woocommerce
run: pnpx wc-e2e docker:up
- name: Move current directory to code. We will install zip file in this dir later.
run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce
- name: Download WooCommerce ZIP.
uses: actions/download-artifact@v2
with:
name: woocommerce
path: tmp
- name: Extract and replace WooCommerce zip.
working-directory: tmp
run: |
unzip woocommerce.zip -d woocommerce
mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/
- name: Install dependencies again
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Run tests command.
working-directory: package/woocommerce/plugins/woocommerce
env:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
GITHUB_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
PLUGIN_NAME: ${{ matrix.plugin }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: |
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js
pnpm nx test-e2e woocommerce

View File

@ -26,8 +26,8 @@ jobs:
run: |
npm install -g pnpm
pnpm install
composer install --no-dev
pnpm run build:assets
pnpm nx composer-install-no-dev woocommerce
pnpm nx build-assets woocommerce
pnpm install jest
- name: Run smoke test.
@ -50,7 +50,7 @@ jobs:
USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }}
run: |
pnpx wc-e2e test:e2e plugins/woocommerce/tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpx wc-e2e test:e2e
pnpx wc-api-tests test api
test-wp-version:
@ -73,16 +73,17 @@ jobs:
with:
path: package/woocommerce
- name: Run npm install.
working-directory: package/woocommerce/plugins/woocommerce
npm install -g pnpm
pnpm install
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Load docker images and start containers.
working-directory: package/woocommerce/plugins/woocommerce
env:
LATEST_WP_VERSION_MINUS: ${{ matrix.wp }}
run: pnpx wc-e2e docker:up
run: pnpm nx docker-up woocommerce
- name: Move current directory to code. We will install zip file in this dir later.
run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce
@ -103,4 +104,75 @@ jobs:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
run: pnpx wc-e2e test:e2e
run: pnpm nx test-e2e woocommerce
test-plugins:
name: Smoke tests with ${{ matrix.plugin }} plugin installed
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
include:
- plugin: 'WooCommerce Payments'
repo: 'automattic/woocommerce-payments'
- plugin: 'WooCommerce PayPal Payments'
repo: 'woocommerce/woocommerce-paypal-payments'
- plugin: 'WooCommerce Shipping & Tax'
repo: 'woocommerce/woocommerce-services'
- plugin: 'WooCommerce Subscriptions'
repo: WC_SUBSCRIPTIONS_REPO
private: true
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
repo: 'Yoast/wordpress-seo'
- plugin: 'Contact Form 7'
repo: 'takayukister/contact-form-7'
steps:
- name: Create dirs.
run: |
mkdir -p code/woocommerce
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
mkdir -p node_modules
- name: Checkout code.
uses: actions/checkout@v2
with:
path: package/woocommerce
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
- name: Load docker images and start containers.
working-directory: package/woocommerce/plugins/woocommerce
env:
LATEST_WP_VERSION_MINUS: ${{ matrix.wp }}
run: pnpm nx docker-up woocommerce
- name: Move current directory to code. We will install zip file in this dir later.
run: mv ./package/woocommerce/plugins/woocommerce/* ./code/woocommerce
- name: Download WooCommerce release zip
working-directory: tmp
run: |
ASSET_ID=$(jq ".release.assets[0].id" $GITHUB_EVENT_PATH)
curl https://api.github.com/repos/woocommerce/woocommerce/releases/assets/${ASSET_ID} -LJOH 'Accept: application/octet-stream'
unzip woocommerce.zip -d woocommerce
mv woocommerce/woocommerce/* ../package/woocommerce/plugins/woocommerce/
- name: Run tests command.
working-directory: package/woocommerce/plugins/woocommerce
env:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
GITHUB_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
PLUGIN_NAME: ${{ matrix.plugin }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: |
pnpx wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js
pnpm nx test-e2e woocommerce

46
.gitignore vendored
View File

@ -5,10 +5,28 @@ Thumbs.db
# IDE files
.idea
.vscode/
project.xml
project.properties
.project
.settings*
*.sublime-project
*.sublime-workspace
.sublimelinterrc
# Grunt
none
# Sass
.sass-cache/
# Logs
logs/
# Eslint Cache
.eslintcache
# Environment files
wp-cli.local.yml
.wp-env.override.json
yarn-error.log
npm-debug.log
.pnpm-debug.log
@ -22,6 +40,7 @@ npm-debug.log
build/
build-module/
build-style/
dist/
# Project files
node_modules/
@ -30,12 +49,27 @@ vendor/
# TypeScript files
tsconfig.tsbuildinfo
# Node Package Dependencies
package-lock.json
# wp-env config
.wp-env.override.json
# Unit tests
/tmp
packages/js/e2e-environment/config/default.json
packages/js/e2e-environment/docker/wp-cli/initialize.sh
packages/js/e2e-environment/build/
packages/js/e2e-environment/build-module/
tmp/
# Composer
vendor/
bin/composer/**/vendor/
lib/vendor/
contributors.md
contributors.html
# Yarn
yarn.lock
# Editors
nbproject/private/
# Test Results
test-results.json

2
.nvmrc
View File

@ -1 +1 @@
v12
v16

View File

@ -1,3 +1,3 @@
{
"extends": "stylelint-config-wordpress",
"extends": "@wordpress/stylelint-config",
}

View File

@ -34,9 +34,8 @@ The port # might be different depending on your `.wp-env.override.json` configur
Once you have WP-ENV container up, we need to run a few commands to start developing.
1. Run `pnpm install` to install npm modules.
2. Navigate to Core WooCommerce `cd plugins/woocommerce`.
3. Run `pnpm run build:core`
4. Run `composer install` to install PHP dependencies.
2. Run `pnpm nx build woocommerce` to build core.
3. Run `pnpm nx composer-install woocommerce` to install PHP dependencies.
If you don't have Composer available locally, run the following command. It runs the command in WP-ENV container.

View File

@ -1,5 +1,110 @@
== Changelog ==
= 6.0.0 2021-12-14 =
**WooCommerce**
* Localization - Improve internationalization and add regions for Chile. ([#30875](https://github.com/woocommerce/woocommerce/pull/30875))
* Localization - Add 'GB' back to countries that are recommended to use automated taxes. ([#31100](https://github.com/woocommerce/woocommerce/pull/31100))
* Enhancement - Improve the performance of the filtering by attributes using the new lookup table. ([#31212](https://github.com/woocommerce/woocommerce/pull/31212))
* Enhancement - Stop using options table to store rate limits. ([#30960](https://github.com/woocommerce/woocommerce/pull/30960))
* Enhancement - Support for dynamic price period in in-app marketplace product cards. ([#31026](https://github.com/woocommerce/woocommerce/pull/31026))
* Enhancement - warning to developers to avoid gotcha with shipping rates. ([#30958](https://github.com/woocommerce/woocommerce/pull/30958))
* Enhancement - Add woocommerce_product_options_shipping_product_data hook to product data metabox. ([#30876](https://github.com/woocommerce/woocommerce/pull/30876))
* Enhancement - Ensure empty arrays can be cached. ([#31077](https://github.com/woocommerce/woocommerce/pull/31077))
* Tweak - Remove the need to invoke LookupDataStore->show_feature() to use the product attributes lookup table. ([#31228](https://github.com/woocommerce/woocommerce/pull/31228))
* Tweak - Add new action hook woocommerce_after_variations_table after the product variations table (within the add-to-cart form). ([#29642](https://github.com/woocommerce/woocommerce/pull/29642))
* Tweak - Email password reset link instead of password for new customers. ([#31257](https://github.com/woocommerce/woocommerce/pull/31257))
* Tweak - Disable autocomplete on quantity input field to prevent stale values in Firefox. ([#31196](https://github.com/woocommerce/woocommerce/pull/31196))
* Tweak - Correct the Iraqi Dinar (IQD) symbol. ([#31070](https://github.com/woocommerce/woocommerce/pull/31070))
* Tweak - Add Product Reviews filter for review comment type to the WordPress comment page. ([#31004](https://github.com/woocommerce/woocommerce/pull/31004))
* Tweak - has_block_template method: Add apply_filters to the function which will enable third-party plugins to override the return value. ([#30997](https://github.com/woocommerce/woocommerce/pull/30997))
* Tweak - Clarify tooltip for when the on-hold email is sent. ([#30970](https://github.com/woocommerce/woocommerce/pull/30970))
* Tweak - Perform check for has product archive if current theme is an FSE theme, and not just if it has current_theme_supports( 'woocommerce' ); ([#31094](https://github.com/woocommerce/woocommerce/pull/31094))
* Tweak - Remove the absolute path to the currency-info.php from within locale-info.php. ([#30935](https://github.com/woocommerce/woocommerce/pull/30935))
* Tweak - Update track properties to follow correct format. ([#30899](https://github.com/woocommerce/woocommerce/pull/30899))
* Tweak - Merge Marketplace and My Subscriptions pages back into one Extensions page. ([#31085](https://github.com/woocommerce/woocommerce/pull/31085))
* Fix - Fixes an issue that prevented database update notices from being dismissed. ([#31075](https://github.com/woocommerce/woocommerce/pull/31075))
* Fix - Duplicate coupon usage count when order is created via admin/API and status is changed. ([#31147](https://github.com/woocommerce/woocommerce/pull/31147))
* Fix - Corrects the display of negative refund values within the order editor screen. ([#30957](https://github.com/woocommerce/woocommerce/pull/30957))
* Fix - Fix bug when creating REST API keys with very long descriptions. ([#30901](https://github.com/woocommerce/woocommerce/pull/30901))
* Fix - Fix products API orderby slug and include. ([#30873](https://github.com/woocommerce/woocommerce/pull/30873))
* Dev - Remove defunct AJAX events. ([#30931](https://github.com/woocommerce/woocommerce/pull/30931))
**WooCommerce Admin - 2.9.0 & 2.9.1 & 2.9.2 **
* Dev - Remove task status endpoint ( [#7841](https://github.com/woocommerce/woocommerce-admin/issues/7841) )
* Fix - Fix ordering and styling issue with WooCommerce Payments payment method promotion. ( [#7943](https://github.com/woocommerce/woocommerce-admin/issues/7943) )
* Fix - Fix ExPlat PHP client ( [#7926](https://github.com/woocommerce/woocommerce-admin/issues/7926) )
* Fix - Fix marketing extensions tracks ( [#7908](https://github.com/woocommerce/woocommerce-admin/issues/7908) )
* Fix - Fix shipping task completion status ( [#8031](https://github.com/woocommerce/woocommerce-admin/issues/8031) )
* Update - Increased number of possible items in Recommended Extensions list from 6 to 9 ( [#7887](https://github.com/woocommerce/woocommerce-admin/issues/7887) )
* Update - Reverts addition of Marketplace and My Subscriptions pages to the Marketplace menu. ( [#7902](https://github.com/woocommerce/woocommerce-admin/issues/7902) )
* Update - Add marketing extensions back to onboarding wizard ( [#7831](https://github.com/woocommerce/woocommerce-admin/issues/7831) )
* Update - Add profile notes. ( [#7861](https://github.com/woocommerce/woocommerce-admin/issues/7861) )
* Update - Change CTA text for personalize store task after completion ( [#7852](https://github.com/woocommerce/woocommerce-admin/issues/7852) )
* Update - Refactor data source poller for re-usability. ( [#7671](https://github.com/woocommerce/woocommerce-admin/issues/7671) )
* Update - Update WC Pay card to include in-person information ( [#7830](https://github.com/woocommerce/woocommerce-admin/issues/7830) )
* Update - Updating navigation link colors ( [#7833](https://github.com/woocommerce/woocommerce-admin/issues/7833) )
* Tweak - Use page title Extensions for Marketplace and My Subscriptions pages. ( [#7901](https://github.com/woocommerce/woocommerce-admin/issues/7901) )
* Tweak - Remove the Spinner component to prevent undesired page flickering. ( [#7886](https://github.com/woocommerce/woocommerce-admin/issues/7886) )
* Tweak - Add route and layout for unmatched path ( [#7503](https://github.com/woocommerce/woocommerce-admin/issues/7503) )
* Tweak - Avoid caching extended info ( [#7819](https://github.com/woocommerce/woocommerce-admin/issues/7819) )
* Tweak - Minor design update for Marketing task. ( [#7732](https://github.com/woocommerce/woocommerce-admin/issues/7732) )
* Fix - Do not clear `current` class from the entire page when updating wp-admin's menu. ( [#7773](https://github.com/woocommerce/woocommerce-admin/issues/7773) )
* Fix - Fix calendar not being dismissed when clicking outside. ( [#7714](https://github.com/woocommerce/woocommerce-admin/issues/7714) )
* Fix - fixed warnings when using AdvancedFilters component. ( [#7704](https://github.com/woocommerce/woocommerce-admin/issues/7704) )
* Fix - Fix Tasklist UI illustrations styling ( [#7858](https://github.com/woocommerce/woocommerce-admin/issues/7858) )
* Fix - Revert experiment task titles back to original ( [#7853](https://github.com/woocommerce/woocommerce-admin/issues/7853) )
* Fix - Ensure homescreen defaults to single column layout. ( [#7969](https://github.com/woocommerce/woocommerce-admin/issues/7969) )
* Fix: Fix shipping task not offering step 3. ( [#7985](https://github.com/woocommerce/woocommerce-admin/issues/7985) )
* Add - Add Avalara to tax task ( [#7874](https://github.com/woocommerce/woocommerce-admin/issues/7874) )
* Add - Add 2col expirement. ( [#7872](https://github.com/woocommerce/woocommerce-admin/issues/7872) )
* Add - Added two column experimental task list ( [#7669](https://github.com/woocommerce/woocommerce-admin/issues/7669) )
* Add - Add header cards for all tasks in Tasklist UI experiment ( [#7838](https://github.com/woocommerce/woocommerce-admin/issues/7838) )
* Add - Add onboarding task docs ( [#7762](https://github.com/woocommerce/woocommerce-admin/issues/7762) )
* Dev - Add method to check for install status ( [#7808](https://github.com/woocommerce/woocommerce-admin/issues/7808) )
* Dev - Refactor tax task into separate components
* Dev - Update the task list to use the new task list REST API ( [#7736](https://github.com/woocommerce/woocommerce-admin/issues/7736) )
* Performance - Only load default tasks during REST requests ( [#7904](https://github.com/woocommerce/woocommerce-admin/issues/7904) )
**WooCommerce Blocks - 6.2.0 & 6.3.0 & 6.3.1 & 6.3.2**
* Enhancement - Legacy Template Block: allow users to delete the block. ( [#5176](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5176) )
* Enhancement - Add placeholder text when modifying product search input in the editor. ( [#5122](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5122) )
* Enhancement - FSE: Add basic product archive block template. ( [#5049](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5049) )
* Enhancement - FSE: Add basic taxonomy block templates. ( [#5063](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5063) )
* Enhancement - FSE: Add single product block template. ( [#5054](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5054) )
* Enhancement - FSE: Remove the do_action( woocommerce_sidebar ); action from the LegacyTemplate.php block. ( [#5097](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5097) )
* Enhancement - Fix duplicate queries in product grids. ( [#5002](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5002) )
* Enhancement - FSE: Add abstract block legacy template for core PHP templates. ( [#4991](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4991) )
* Enhancement - FSE: Add render logic to BlockTemplateController. ( [#4984](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4984) )
* Enhancement - Improve accessibility by using self-explaining edit button titles. ( [#5113](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5113) )
* Enhancement - Improve readability of terms and condition text by not displaying the text justified. ( [#5120](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5120) )
* Enhancement - Improve rendering performance for Single Product block. ( [#5107](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5107) )
* Enhancement - Improve the product images placeholder display by adding a light gray border to it. ( [#4950](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4950) )
* Enhancement - Deprecate the __experimental_woocommerce_blocks_checkout_update_order_from_request action in favour of woocommerce_blocks_checkout_update_order_from_request. ( [#5015](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5015) )
* Enhancement - Deprecate the __experimental_woocommerce_blocks_checkout_update_order_meta action in favour of woocommerce_blocks_checkout_update_order_meta. ( [#5017](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5017) )
* Enhancement - Deprecate the __experimental_woocommerce_blocks_checkout_order_processed action in favour of woocommerce_blocks_checkout_order_processed. ( [#5014](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5014) )
* Enhancement - Cart v2: The cart block, like checkout block, now supports inner blocks that allow for greater customizability. ( [#4973](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4973) )
* Enhancement - BlockTemplateController: Adds the ability to load and manage block template files. ( [#4981](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4981) )
* Enhancement - Improve accessibility for the editor view of the Product search block. ( [#4905](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4905) )
* Fix - Removed WooCommerce block templates from appearing in the template dropdown for a page or post. ( [#5167](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5167) )
* Fix - Fix Country is required error on the Cart block when updating shipping address ( [#5129](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5129) )
* Fix - Fix state validation to compare state codes, and only validate if a country is given ( [#5132](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5132) )
* Fix - Make order note block removable ( [#5139](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5139) )
* Fix - Fix label alignment of the product search in the editor. ( [#5072](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5072) )
* Fix - Fix sale badge alignment on smaller screen. ( [#5061](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5061) )
* Fix - FSE: Fix missing is_custom property for WooCommerce block template objects. ( [#5067](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5067) )
* Fix - Replace incorrect with correct text domain. ( [#5020](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5020) )
* Fix - Scripts using wc-settings or script that depend on it would be enqueued in the footer if theyre enqueued in the header. ( [#5059](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5059) )
* Fix - Fix custom classname support for inner checkout blocks. ( [#4978](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4978) )
* Fix - Fix a bug in free orders and trial subscription products. ( [#4955](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4955) )
* Fix - Remove duplicate attributes in saved block HTML. ( [#4941](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4941) )
* Fix - Fix render error of Filter by Attribute block when no attribute is selected. ( [#4847](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4847) )
* Fix - Store API Ensure returned customer address state is valid. ( [#4844](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4844) )
* Fix - fatal error in certain WP 5.9 pre-release versions. ( [#5183](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5183) )
= 5.9.0 2021-11-09 =
**WooCommerce**

37
nx.json
View File

@ -1,20 +1,17 @@
{
"npmScope": "woocommerce",
"affected": {
"defaultBase": "trunk"
},
"implicitDependencies": {
"package.json": {
"dependencies": "*",
"devDependencies": "*"
},
".eslintrc.json": "*"
},
"extends": "@nrwl/workspace/presets/npm.json",
"npmScope": "woocommerce-monorepo",
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
"cacheableOperations": [
"build",
"test",
"lint",
"package",
"prepare"
]
}
}
},
@ -24,7 +21,21 @@
"target": "build",
"projects": "dependencies"
}
],
"prepare": [
{
"target": "prepare",
"projects": "dependencies"
}
],
"package": [
{
"target": "package",
"projects": "dependencies"
}
]
},
"projects": {}
"affected": {
"defaultBase": "trunk"
}
}

View File

@ -1,39 +1,46 @@
{
"name": "woocommerce-monorepo",
"title": "WooCommerce Monorepo",
"description": "Monorepo for the WooCommerce ecosystem",
"homepage": "https://woocommerce.com/",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
},
"author": "Automattic",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"devDependencies": {
"@nrwl/tao": "12.10.0",
"@nrwl/cli": "12.10.0",
"@nrwl/workspace": "12.10.0",
"@types/node": "14.14.33",
"@woocommerce/eslint-plugin": "^1.2.0",
"@wordpress/prettier-config": "^1.0.5",
"chalk": "^4.1.2",
"glob": "^7.2.0",
"jest": "^27.0.6",
"mkdirp": "^1.0.4",
"node-stream-zip": "^1.13.6",
"prettier": "npm:wp-prettier@2.2.1-beta-1",
"request": "^2.88.2",
"typescript": "4.2.4"
},
"dependencies": {
"@babel/core": "7.12.9",
"@wordpress/babel-plugin-import-jsx-pragma": "^3.1.0",
"@wordpress/babel-preset-default": "^6.3.3",
"lodash": "^4.17.21",
"wp-textdomain": "1.0.1"
}
"name": "woocommerce-monorepo",
"title": "WooCommerce Monorepo",
"description": "Monorepo for the WooCommerce ecosystem",
"homepage": "https://woocommerce.com/",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
},
"author": "Automattic",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"scripts": {
"preinstall": "npx only-allow pnpm"
},
"devDependencies": {
"@automattic/nx-composer": "^0.1.0",
"@nrwl/cli": "^13.3.4",
"@nrwl/linter": "^13.3.4",
"@nrwl/devkit": "^13.1.4",
"@nrwl/tao": "13.3.4",
"@nrwl/web": "^13.3.4",
"@nrwl/workspace": "^13.3.4",
"@types/node": "14.14.33",
"@woocommerce/eslint-plugin": "^1.3.0",
"@wordpress/prettier-config": "^1.1.1",
"chalk": "^4.1.2",
"glob": "^7.2.0",
"jest": "^27.3.1",
"mkdirp": "^1.0.4",
"node-stream-zip": "^1.15.0",
"prettier": "npm:wp-prettier@^2.2.1-beta-1",
"request": "^2.88.2",
"typescript": "4.2.4"
},
"dependencies": {
"@babel/core": "7.12.9",
"@wordpress/babel-plugin-import-jsx-pragma": "^3.1.0",
"@wordpress/babel-preset-default": "^6.4.1",
"lodash": "^4.17.21",
"wp-textdomain": "1.0.1"
}
}

View File

@ -1,8 +1,2 @@
# Node modules
node_modules/
# Environment
.env
# Collection output
collection.json

View File

@ -1,3 +1,10 @@
# Unreleased
## Added
- Shipping Zones API Tests
- Shipping Methods API Tests
- Complex Order API Tests
# 0.1.0
- Initial/beta release
@ -6,3 +13,5 @@
- Coupons API Tests
- Refunds API Tests
- Products API Tests
- CRUD tests for the Orders API
- Order Search API Tests

View File

@ -0,0 +1,7 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/<last-commit-hash-before-this-merge>/packages/js/api-core-tests/CHANGELOG.md).

View File

@ -31,7 +31,7 @@ cd "$SCRIPTPATH/$(dirname "$REALPATH")/.."
# Run scripts
case $1 in
'test')
jest --group=$2
jest --group=$2 --runInBand
TESTRESULT=$?
;;
'make:collection')

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/api-core-tests",
"description": "WooCommerce API core test",
"type": "library",
"license": "GPL-3.0-or-later",
"minimum-stability": "dev",
"require-dev": {
"automattic/jetpack-changelogger": "3.0.2"
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/PackageFormatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "NEXT_CHANGELOG.md"
}
}
}

1021
packages/js/api-core-tests/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,19 @@
const { order, getOrderExample } = require('./order');
const { coupon } = require('./coupon');
const { refund } = require('./refund');
const shared = require('./shared');
const { order, getOrderExample } = require( './order' );
const { coupon } = require( './coupon' );
const { refund } = require( './refund' );
const { getTaxRateExamples } = require( './tax-rate' );
const { getVariationExample } = require( './variation' );
const {
simpleProduct,
variableProduct,
variation,
virtualProduct,
groupedProduct,
externalProduct,
} = require( './products-crud' );
const { getShippingZoneExample } = require( './shipping-zone' );
const { getShippingMethodExample } = require( './shipping-method' );
const shared = require( './shared' );
module.exports = {
order,
@ -9,4 +21,14 @@ module.exports = {
coupon,
shared,
refund,
getTaxRateExamples,
getVariationExample,
simpleProduct,
variableProduct,
variation,
virtualProduct,
groupedProduct,
externalProduct,
getShippingZoneExample,
getShippingMethodExample,
};

View File

@ -0,0 +1,292 @@
/**
* Internal dependencies
*/
const {
postRequest,
deleteRequest,
getRequest,
putRequest,
} = require( '../utils/request' );
const productsTestSetup = require( './product-list' );
const { ordersApi } = require( '../endpoints/orders' );
const createCustomer = ( data ) => postRequest( 'customers', data );
const deleteCustomer = ( id ) => deleteRequest( `customers/${ id }`, true );
const createSampleData = async () => {
const testProductData = await productsTestSetup.createSampleData();
const orderedProducts = {
pocketHoodie: testProductData.simpleProducts.find(
( p ) => p.name === 'Hoodie with Pocket'
),
sunglasses: testProductData.simpleProducts.find(
( p ) => p.name === 'Sunglasses'
),
beanie: testProductData.simpleProducts.find(
( p ) => p.name === 'Beanie'
),
blueVneck: testProductData.variableProducts.vneckVariations.find(
( p ) => p.sku === 'woo-vneck-tee-blue'
),
pennant: testProductData.externalProducts[ 0 ],
};
const johnAddress = {
first_name: 'John',
last_name: 'Doe',
company: 'Automattic',
country: 'US',
address_1: '60 29th Street',
address_2: '#343',
city: 'San Francisco',
state: 'CA',
postcode: '94110',
phone: '123456789',
};
const tinaAddress = {
first_name: 'Tina',
last_name: 'Clark',
company: 'Automattic',
country: 'US',
address_1: 'Oxford Ave',
address_2: '',
city: 'Buffalo',
state: 'NY',
postcode: '14201',
phone: '123456789',
};
const guestShippingAddress = {
first_name: 'Ano',
last_name: 'Nymous',
company: '',
country: 'US',
address_1: '0 Incognito St',
address_2: '',
city: 'Erie',
state: 'PA',
postcode: '16515',
phone: '123456789',
};
const guestBillingAddress = {
first_name: 'Ben',
last_name: 'Efactor',
company: '',
country: 'US',
address_1: '200 W University Avenue',
address_2: '',
city: 'Gainesville',
state: 'FL',
postcode: '32601',
phone: '123456789',
email: 'ben.efactor@email.net',
};
const { body: john } = await createCustomer( {
first_name: 'John',
last_name: 'Doe',
username: 'john.doe',
email: 'john.doe@example.com',
billing: {
...johnAddress,
email: 'john.doe@example.com',
},
shipping: johnAddress,
} );
const { body: tina } = await createCustomer( {
first_name: 'Tina',
last_name: 'Clark',
username: 'tina.clark',
email: 'tina.clark@example.com',
billing: {
...tinaAddress,
email: 'tina.clark@example.com',
},
shipping: tinaAddress,
} );
const orderBaseData = {
payment_method: 'cod',
payment_method_title: 'Cash on Delivery',
status: 'processing',
set_paid: false,
currency: 'USD',
customer_id: 0,
};
const orders = [];
// Have "John" order all products.
Object.values( orderedProducts ).forEach( async ( product ) => {
const { body: order } = await ordersApi.create.order( {
...orderBaseData,
customer_id: john.id,
billing: {
...johnAddress,
email: 'john.doe@example.com',
},
shipping: johnAddress,
line_items: [
{
product_id: product.id,
quantity: 1,
},
],
} );
orders.push( order );
} );
// Have "Tina" order some sunglasses and make a child order.
// This somewhat resembles a subscription renewal, but we're just testing the `parent` field.
const { body: order2 } = await ordersApi.create.order( {
...orderBaseData,
status: 'completed',
set_paid: true,
customer_id: tina.id,
billing: {
...tinaAddress,
email: 'tina.clark@example.com',
},
shipping: tinaAddress,
line_items: [
{
product_id: orderedProducts.sunglasses.id,
quantity: 1,
},
],
} );
orders.push( order2 );
const { body: order3 } = await ordersApi.create.order( {
...orderBaseData,
parent_id: order2.id,
customer_id: tina.id,
billing: {
...tinaAddress,
email: 'tina.clark@example.com',
},
shipping: tinaAddress,
line_items: [
{
product_id: orderedProducts.sunglasses.id,
quantity: 1,
},
],
} );
orders.push( order3 );
// Guest order.
const { body: guestOrder } = await ordersApi.create.order( {
...orderBaseData,
billing: guestBillingAddress,
shipping: guestShippingAddress,
line_items: [
{
product_id: orderedProducts.pennant.id,
quantity: 2,
},
{
product_id: orderedProducts.beanie.id,
quantity: 1,
},
],
} );
// Create an order with all possible numerical fields (taxes, fees, refunds, etc).
const { body: taxSetting } = await getRequest(
'settings/general/woocommerce_calc_taxes'
);
await putRequest( 'settings/general/woocommerce_calc_taxes', {
value: 'yes',
} );
const { body: taxRate } = await postRequest( 'taxes', {
country: '*',
state: '*',
postcode: '*',
city: '*',
rate: '5.5000',
name: 'Tax',
rate: '5.5',
shipping: true,
} );
const { body: coupon } = await postRequest( 'coupons', {
code: 'save5',
amount: '5',
} );
const { body: order4 } = await ordersApi.create.order( {
...orderBaseData,
line_items: [
{
product_id: orderedProducts.blueVneck.id,
quantity: 1,
},
],
coupon_lines: [ { code: 'save5' } ],
shipping_lines: [
{
method_id: 'flat_rate',
total: '5.00',
},
],
fee_lines: [
{
total: '1.00',
name: 'Test Fee',
},
],
} );
await postRequest( `orders/${ order4.id }/refunds`, {
api_refund: false, // Prevent an actual refund request (fails with CoD),
line_items: [
{
id: order4.line_items[ 0 ].id,
quantity: 1,
refund_total: order4.line_items[ 0 ].total,
refund_tax: [
{
id: order4.line_items[ 0 ].taxes[ 0 ].id,
refund_total: order4.line_items[ 0 ].total_tax,
},
],
},
],
} );
orders.push( order4 );
return {
customers: { john, tina },
orders,
precisionOrder: order4,
hierarchicalOrders: {
parent: order2,
child: order3,
},
guestOrder,
testProductData,
};
};
const deleteSampleData = async ( sampleData ) => {
await productsTestSetup.deleteSampleData( sampleData.testProductData );
sampleData.orders
.concat( [ sampleData.guestOrder ] )
.forEach( async ( { id } ) => {
await ordersApi.delete.order( id, true );
} );
Object.values( sampleData.customers ).forEach( async ( { id } ) => {
await deleteCustomer( id );
} );
};
module.exports = {
createSampleData,
deleteSampleData,
};

View File

@ -54,8 +54,27 @@ const variableProduct = {
],
};
/**
* External product example
*/
const externalProduct = {
name: 'An External Product',
regular_price: '1.00',
type: 'external',
};
/**
* Grouped product example
*/
const groupedProduct = {
name: 'A Grouped Product',
type: 'grouped',
};
module.exports = {
simpleProduct,
virtualProduct,
variableProduct,
externalProduct,
groupedProduct,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
/**
* A basic refund.
*
@ -7,7 +6,7 @@
* https://woocommerce.github.io/woocommerce-rest-api-docs/#order-refund-properties
*
*/
const refund = {
const refund = {
api_refund: false,
amount: '1.00',
reason: 'Late delivery refund.',
@ -15,5 +14,5 @@
};
module.exports = {
refund: refund,
refund,
};

View File

@ -0,0 +1,27 @@
/**
* Constructs a shipping method based on the given `methodId` and `cost`.
*
* `methodId` should be one of the following:
* - `free_shipping`
* - `flat_rate`
* - `local_pickup`
*
* @returns shipping method object that can serve as a request payload for adding a shipping method to a shipping zone.
*/
const getShippingMethodExample = ( methodId, cost ) => {
const shippingMethodExample = {
method_id: methodId,
};
if ( cost !== undefined ) {
shippingMethodExample.settings = {
cost: cost,
};
}
return shippingMethodExample;
};
module.exports = {
getShippingMethodExample,
};

View File

@ -0,0 +1,24 @@
/**
* Default shipping zone object.
*
* For more details on shipping zone properties, see:
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zone-properties
*
*/
const shippingZone = {
name: 'US Domestic',
};
/**
* Constructs a default shipping zone object.
*
* @returns default shipping zone
*/
const getShippingZoneExample = () => {
return shippingZone;
};
module.exports = {
getShippingZoneExample,
};

View File

@ -0,0 +1,33 @@
/**
* A standard tax rate.
*
* For more details on the tax rate properties, see:
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rate-properties
*
*/
const standardTaxRate = {
name: 'Standard Rate',
rate: '10.0000',
class: 'standard',
};
const reducedTaxRate = {
name: 'Reduced Rate',
rate: '1.0000',
class: 'reduced-rate',
};
const zeroTaxRate = {
name: 'Zero Rate',
rate: '0.0000',
class: 'zero-rate',
};
const getTaxRateExamples = () => {
return { standardTaxRate, reducedTaxRate, zeroTaxRate };
};
module.exports = {
getTaxRateExamples,
};

View File

@ -0,0 +1,29 @@
/**
* A basic product variation.
*
* For more details on the product variation properties, see:
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variations
*
*/
const variation = {
regular_price: '1.00',
attributes: [
{
name: 'Size',
option: 'Large',
},
{
name: 'Colour',
option: 'Red',
},
],
};
const getVariationExample = () => {
return variation;
};
module.exports = {
getVariationExample,
};

View File

@ -1,11 +1,19 @@
const { ordersApi } = require('./orders');
const { couponsApi } = require('./coupons');
const { productsApi } = require('./products');
const { refundsApi } = require('./refunds');
const { ordersApi } = require( './orders' );
const { couponsApi } = require( './coupons' );
const { productsApi } = require( './products' );
const { refundsApi } = require( './refunds' );
const { taxRatesApi } = require( './tax-rates' );
const { variationsApi } = require( './variations' );
const { shippingZonesApi } = require( './shipping-zones' );
const { shippingMethodsApi } = require( './shipping-methods' );
module.exports = {
ordersApi,
couponsApi,
productsApi,
refundsApi,
taxRatesApi,
variationsApi,
shippingZonesApi,
shippingMethodsApi,
};

View File

@ -1,8 +1,13 @@
/**
* Internal dependencies
*/
const { getRequest, postRequest, putRequest, deleteRequest } = require('../utils/request');
const { getOrderExample, shared } = require('../data');
const {
getRequest,
postRequest,
putRequest,
deleteRequest,
} = require( '../utils/request' );
const { getOrderExample, shared } = require( '../data' );
/**
* WooCommerce Orders endpoints.
@ -24,14 +29,16 @@ const ordersApi = {
method: 'GET',
path: 'orders/<id>',
responseCode: 200,
order: async ( orderId ) => getRequest( `orders/${orderId}` ),
order: async ( orderId, ordersQuery = {} ) =>
getRequest( `orders/${ orderId }`, ordersQuery ),
},
listAll: {
name: 'List all orders',
method: 'GET',
path: 'orders',
responseCode: 200,
orders: async () => getRequest( 'orders' ),
orders: async ( ordersQuery = {} ) =>
getRequest( 'orders', ordersQuery ),
},
update: {
name: 'Update an order',
@ -39,7 +46,8 @@ const ordersApi = {
path: 'orders/<id>',
responseCode: 200,
payload: getOrderExample(),
order: async ( orderId, orderDetails ) => putRequest( `orders/${orderId}`, orderDetails ),
order: async ( orderId, orderDetails ) =>
putRequest( `orders/${ orderId }`, orderDetails ),
},
delete: {
name: 'Delete an order',
@ -47,9 +55,10 @@ const ordersApi = {
path: 'orders/<id>',
responseCode: 200,
payload: {
force: false
force: false,
},
order: async ( orderId, deletePermanently ) => deleteRequest( `orders/${orderId}`, deletePermanently ),
order: async ( orderId, deletePermanently ) =>
deleteRequest( `orders/${ orderId }`, deletePermanently ),
},
batch: {
name: 'Batch update orders',
@ -57,7 +66,8 @@ const ordersApi = {
path: 'orders/batch',
responseCode: 200,
payload: shared.getBatchPayloadExample( getOrderExample() ),
orders: async ( batchUpdatePayload ) => postRequest( `orders/batch`, batchUpdatePayload ),
orders: async ( batchUpdatePayload ) =>
postRequest( `orders/batch`, batchUpdatePayload ),
},
};

View File

@ -1,8 +1,7 @@
/**
* Internal dependencies
*/
const {
const {
getRequest,
postRequest,
deleteRequest,

View File

@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
const {
getRequest,
postRequest,
putRequest,
deleteRequest,
} = require( '../utils/request' );
/**
* WooCommerce Shipping method endpoints.
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-methods
*/
const shippingMethodsApi = {
name: 'Shipping methods',
create: {
name: 'Include a shipping method to a shipping zone',
method: 'POST',
path: 'shipping/zones/<id>/methods',
responseCode: 200,
shippingMethod: async ( shippingZoneId, shippingMethod ) =>
postRequest(
`shipping/zones/${ shippingZoneId }/methods`,
shippingMethod
),
},
retrieve: {
name: 'Retrieve a shipping method from a shipping zone',
method: 'GET',
path: 'shipping/zones/<zone_id>/methods/<id>',
responseCode: 200,
shippingMethod: async ( shippingZoneId, shippingMethodInstanceId ) =>
getRequest(
`shipping/zones/${ shippingZoneId }/methods/${ shippingMethodInstanceId }`
),
},
listAll: {
name: 'List all shipping methods from a shipping zone',
method: 'GET',
path: 'shipping/zones/<id>/methods',
responseCode: 200,
shippingMethods: async ( shippingZoneId, params = {} ) =>
getRequest( `shipping/zones/${ shippingZoneId }/methods`, params ),
},
update: {
name: 'Update a shipping method of a shipping zone',
method: 'PUT',
path: 'shipping/zones/<zone_id>/methods/<id>',
responseCode: 200,
shippingMethod: async (
shippingZoneId,
shippingMethodInstanceId,
updatedShippingMethod
) =>
putRequest(
`shipping/zones/${ shippingZoneId }/methods/${ shippingMethodInstanceId }`,
updatedShippingMethod
),
},
delete: {
name: 'Delete a shipping method from a shipping zone',
method: 'DELETE',
path: 'shipping/zones/<zone_id>/methods/<id>>',
responseCode: 200,
payload: {
force: false,
},
shippingMethod: async (
shippingZoneId,
shippingMethodInstanceId,
deletePermanently
) =>
deleteRequest(
`shipping/zones/${ shippingZoneId }/methods/${ shippingMethodInstanceId }`,
deletePermanently
),
},
};
module.exports = {
shippingMethodsApi,
};

View File

@ -0,0 +1,71 @@
/**
* Internal dependencies
*/
const {
getRequest,
postRequest,
putRequest,
deleteRequest,
} = require( '../utils/request' );
/**
* WooCommerce Shipping zone endpoints.
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zones
*/
const shippingZonesApi = {
name: 'Shipping zones',
create: {
name: 'Create a shipping zone',
method: 'POST',
path: 'shipping/zones',
responseCode: 201,
shippingZone: async ( shippingZone ) =>
postRequest( `shipping/zones`, shippingZone ),
},
retrieve: {
name: 'Retrieve a shipping zone',
method: 'GET',
path: 'shipping/zones/<id>',
responseCode: 200,
shippingZone: async ( shippingZoneId ) =>
getRequest( `shipping/zones/${ shippingZoneId }` ),
},
listAll: {
name: 'List all shipping zones',
method: 'GET',
path: 'shipping/zones',
responseCode: 200,
shippingZones: async ( params = {} ) =>
getRequest( `shipping/zones`, params ),
},
update: {
name: 'Update a shipping zone',
method: 'PUT',
path: 'shipping/zones/<id>',
responseCode: 200,
shippingZone: async ( shippingZoneId, updatedShippingZone ) =>
putRequest(
`shipping/zones/${ shippingZoneId }`,
updatedShippingZone
),
},
delete: {
name: 'Delete a shipping zone',
method: 'DELETE',
path: 'shipping/zones/<id>',
responseCode: 200,
payload: {
force: false,
},
shippingZone: async ( shippingZoneId, deletePermanently ) =>
deleteRequest(
`shipping/zones/${ shippingZoneId }`,
deletePermanently
),
},
};
module.exports = {
shippingZonesApi,
};

View File

@ -0,0 +1,73 @@
/**
* Internal dependencies
*/
const {
getRequest,
postRequest,
putRequest,
deleteRequest,
} = require( '../utils/request' );
const { getTaxRateExamples, shared } = require( '../data' );
/**
* WooCommerce Tax Rates endpoints.
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rates
*/
const taxRatesApi = {
name: 'Tax Rates',
create: {
name: 'Create a tax rate',
method: 'POST',
path: 'taxes',
responseCode: 201,
payload: getTaxRateExamples(),
taxRate: async ( taxRate ) => postRequest( 'taxes', taxRate ),
},
retrieve: {
name: 'Retrieve a tax rate',
method: 'GET',
path: 'taxes/<id>',
responseCode: 200,
taxRate: async ( taxRateId ) => taxes( `coupons/${ taxRateId }` ),
},
listAll: {
name: 'List all tax rates',
method: 'GET',
path: 'taxes',
responseCode: 200,
taxRates: async ( queryString = {} ) =>
getRequest( 'taxes', queryString ),
},
update: {
name: 'Update a tax rate',
method: 'PUT',
path: 'taxes/<id>',
responseCode: 200,
payload: getTaxRateExamples(),
taxRate: async ( taxRateId, taxRateDetails ) =>
putRequest( `taxes/${ taxRateId }`, taxRateDetails ),
},
delete: {
name: 'Delete a tax rate',
method: 'DELETE',
path: 'taxes/<id>',
responseCode: 200,
payload: {
force: false,
},
taxRate: async ( taxRateId, deletePermanently ) =>
deleteRequest( `taxes/${ taxRateId }`, deletePermanently ),
},
batch: {
name: 'Batch update tax rates',
method: 'POST',
path: 'taxes/batch',
responseCode: 200,
payload: shared.getBatchPayloadExample( getTaxRateExamples() ),
taxRates: async ( batchUpdatePayload ) =>
postRequest( `taxes/batch`, batchUpdatePayload ),
},
};
module.exports = { taxRatesApi };

View File

@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
const {
getRequest,
postRequest,
putRequest,
deleteRequest,
} = require( '../utils/request' );
const { getVariationExample, shared } = require( '../data' );
/**
* WooCommerce Product Variation endpoints.
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variations
*/
const variationsApi = {
name: 'Product variations',
create: {
name: 'Create a product variation',
method: 'POST',
path: 'products/<product_id>/variations',
responseCode: 201,
payload: getVariationExample(),
variation: async ( productId, variation ) =>
postRequest( `products/${ productId }/variations`, variation ),
},
retrieve: {
name: 'Retrieve a product variation',
method: 'GET',
path: 'products/<product_id>/variations/<id>',
responseCode: 200,
variation: async ( productId, variationId ) =>
`products/${ productId }/variations/${ variationId }`,
},
listAll: {
name: 'List all product variations',
method: 'GET',
path: 'products/<product_id>/variations',
responseCode: 200,
variations: async ( productId, queryString = {} ) =>
getRequest( `products/${ productId }/variations`, queryString ),
},
update: {
name: 'Update a product variation',
method: 'PUT',
path: 'products/<product_id>/variations/<id>',
responseCode: 200,
payload: getVariationExample(),
variation: async ( productId, variationId, variationDetails ) =>
putRequest(
`products/${ productId }/variations/${ variationId }`,
taxRateDetails
),
},
delete: {
name: 'Delete a product variation',
method: 'DELETE',
path: 'products/<product_id>/variations/<id>',
responseCode: 200,
payload: {
force: false,
},
variation: async ( productId, variationId, deletePermanently ) =>
deleteRequest(
`products/${ productId }/variations/${ variationId }`,
deletePermanently
),
},
batch: {
name: 'Batch update product variations',
method: 'POST',
path: 'products/<product_id>/variations/batch',
responseCode: 200,
payload: shared.getBatchPayloadExample( getVariationExample() ),
variations: async ( batchUpdatePayload ) =>
postRequest(
`products/${ productId }/variations/${ variationId }`,
batchUpdatePayload
),
},
};
module.exports = { variationsApi };

View File

@ -4,6 +4,7 @@
"description": "API tests for WooCommerce",
"main": "index.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
"test": "jest",
"test:api": "jest --group=api",
"test:hello": "jest --group=hello",
@ -25,6 +26,9 @@
"postman-collection": "^4.1.0",
"supertest": "^6.1.4"
},
"publishConfig": {
"access": "public"
},
"bin": {
"wc-api-tests": "bin/wc-api-tests.sh"
}

View File

@ -0,0 +1,38 @@
{
"root": "packages/js/api-core-tests/",
"sourceRoot": "packages/js/api-core-tests",
"projectType": "library",
"targets": {
"changelog": {
"executor": "./tools/executors/changelogger:changelog",
"options": {
"action": "add",
"cwd": "packages/js/api-core-tests"
}
},
"test": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test"
}
},
"test-hello": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test:hello"
}
},
"make-collection": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "make:collection"
}
},
"test-api": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test:api"
}
}
}
}

View File

@ -0,0 +1,329 @@
const { couponsApi } = require( '../../endpoints/coupons' );
const { ordersApi } = require( '../../endpoints/orders' );
const { coupon, order } = require( '../../data' );
/**
* Tests for the WooCommerce Coupons API.
*
* @group api
* @group coupons
*
*/
describe( 'Coupons API tests', () => {
let couponId;
it( 'can create a coupon', async () => {
const testCoupon = {
...coupon,
code: `${ coupon.code }-${ Date.now() }`,
};
const response = await couponsApi.create.coupon( testCoupon );
expect( response.statusCode ).toEqual( couponsApi.create.responseCode );
expect( response.body.id ).toBeDefined();
couponId = response.body.id;
// Validate the created coupon object has the correct code, amount, and discount type
expect( response.body ).toEqual(
expect.objectContaining( {
code: testCoupon.code,
amount: Number( coupon.amount ).toFixed( 2 ),
discount_type: coupon.discount_type,
} )
);
} );
it( 'can retrieve a coupon', async () => {
const response = await couponsApi.retrieve.coupon( couponId );
expect( response.statusCode ).toEqual(
couponsApi.retrieve.responseCode
);
expect( response.body.id ).toEqual( couponId );
} );
it( 'can update a coupon', async () => {
const updatedCouponDetails = {
description: '10% off storewide',
maximum_amount: '500.00',
usage_limit_per_user: 1,
free_shipping: true,
};
const response = await couponsApi.update.coupon(
couponId,
updatedCouponDetails
);
expect( response.statusCode ).toEqual( couponsApi.update.responseCode );
expect( response.body ).toEqual(
expect.objectContaining( updatedCouponDetails )
);
} );
it( 'can permanently delete a coupon', async () => {
const response = await couponsApi.delete.coupon( couponId, true );
expect( response.statusCode ).toEqual( couponsApi.delete.responseCode );
const getCouponResponse = await couponsApi.retrieve.coupon( couponId );
expect( getCouponResponse.statusCode ).toEqual( 404 );
} );
describe( 'Batch update coupons', () => {
/**
* Coupons to be created, updated, and deleted.
*/
const expectedCoupons = [
{
code: `batchcoupon-${ Date.now() }`,
discount_type: 'percent',
amount: '10',
free_shipping: false,
},
{
code: `batchcoupon-${ Date.now() + 1 }`,
discount_type: 'percent',
amount: '20',
},
];
it( 'can batch create coupons', async () => {
// Batch create 2 new coupons.
const batchCreatePayload = {
create: expectedCoupons,
};
const batchCreateResponse = await couponsApi.batch.coupons(
batchCreatePayload
);
expect( batchCreateResponse.status ).toEqual(
couponsApi.batch.responseCode
);
// Verify that the 2 new coupons were created
const actualCoupons = batchCreateResponse.body.create;
expect( actualCoupons ).toHaveLength( expectedCoupons.length );
for ( let i = 0; i < actualCoupons.length; i++ ) {
const { id, code } = actualCoupons[ i ];
const expectedCouponCode = expectedCoupons[ i ].code;
expect( id ).toBeDefined();
expect( code ).toEqual( expectedCouponCode );
// Save the coupon id
expectedCoupons[ i ].id = id;
}
} );
it( 'can batch update coupons', async () => {
// Update the 1st coupon to free shipping.
// Update the amount of the 2nd coupon to 25.
const batchUpdatePayload = {
update: [
{
id: expectedCoupons[ 0 ].id,
free_shipping: true,
},
{
id: expectedCoupons[ 1 ].id,
amount: '25.00',
},
],
};
const batchUpdateResponse = await couponsApi.batch.coupons(
batchUpdatePayload
);
// Verify the response code and the number of coupons that were updated.
const updatedCoupons = batchUpdateResponse.body.update;
expect( batchUpdateResponse.status ).toEqual(
couponsApi.batch.responseCode
);
expect( updatedCoupons ).toHaveLength( expectedCoupons.length );
// Verify that the 1st coupon was updated to free shipping.
expect( updatedCoupons[ 0 ].id ).toEqual( expectedCoupons[ 0 ].id );
expect( updatedCoupons[ 0 ].free_shipping ).toEqual( true );
// Verify that the amount of the 2nd coupon was updated to 25.
expect( updatedCoupons[ 1 ].id ).toEqual( expectedCoupons[ 1 ].id );
expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' );
} );
it( 'can batch delete coupons', async () => {
// Batch delete the 2 coupons.
const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id );
const batchDeletePayload = {
delete: couponIdsToDelete,
};
const batchDeleteResponse = await couponsApi.batch.coupons(
batchDeletePayload
);
// Verify that the response shows the 2 coupons.
const deletedCouponIds = batchDeleteResponse.body.delete.map(
( { id } ) => id
);
expect( batchDeleteResponse.status ).toEqual(
couponsApi.batch.responseCode
);
expect( deletedCouponIds ).toEqual( couponIdsToDelete );
// Verify that the 2 deleted coupons cannot be retrieved.
for ( const couponId of couponIdsToDelete ) {
const { status } = await couponsApi.retrieve.coupon( couponId );
expect( status ).toEqual( 404 );
}
} );
} );
describe( 'List coupons', () => {
const allCoupons = [
{
...coupon,
code: `listcoupons-01-${ Date.now() }`,
description: `description-01-${ Date.now() }`,
},
{
...coupon,
code: `listcoupons-02-${ Date.now() }`,
description: `description-02-${ Date.now() }`,
},
{
...coupon,
code: `listcoupons-03-${ Date.now() }`,
description: `description-03-${ Date.now() }`,
},
];
beforeAll( async () => {
// Create list of coupons for testing.
const response = await couponsApi.batch.coupons( {
create: allCoupons,
} );
const actualCreatedCoupons = response.body.create;
// Save their coupon ID's
for ( const coupon of allCoupons ) {
const { id } = actualCreatedCoupons.find(
( { code } ) => coupon.code === code
);
coupon.id = id;
}
} );
afterAll( async () => {
// Clean up created coupons
const batchDeletePayload = {
delete: allCoupons.map( ( { id } ) => id ),
};
await couponsApi.batch.coupons( batchDeletePayload );
} );
it( 'can list all coupons by default', async () => {
const response = await couponsApi.listAll.coupons();
const listedCoupons = response.body;
const actualCouponIdsList = listedCoupons.map( ( { id } ) => id );
const expectedCouponIdsList = allCoupons.map( ( { id } ) => id );
expect( response.status ).toEqual(
couponsApi.listAll.responseCode
);
expect( actualCouponIdsList ).toEqual(
expect.arrayContaining( expectedCouponIdsList )
);
} );
it( 'can limit result set to matching code', async () => {
const matchingCoupon = allCoupons[ 1 ];
const payload = { code: matchingCoupon.code };
const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect( status ).toEqual( couponsApi.listAll.responseCode );
expect( body ).toHaveLength( 1 );
expect( body[ 0 ].id ).toEqual( matchingCoupon.id );
} );
it( 'can paginate results', async () => {
const pageSize = 2;
const payload = {
page: 1,
per_page: pageSize,
};
const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect( status ).toEqual( couponsApi.listAll.responseCode );
expect( body ).toHaveLength( pageSize );
} );
it( 'can limit results to matching string', async () => {
// Search by description
const matchingCoupon = allCoupons[ 2 ];
const matchingString = matchingCoupon.description;
const payload = {
search: matchingString,
};
const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect( status ).toEqual( couponsApi.listAll.responseCode );
expect( body ).toHaveLength( 1 );
expect( body[ 0 ].id ).toEqual( matchingCoupon.id );
} );
} );
describe( 'Add coupon to order', () => {
const testCoupon = {
code: `coupon-${ Date.now() }`,
discount_type: 'percent',
amount: '10',
};
let orderId;
beforeAll( async () => {
// Create a coupon
const createCouponResponse = await couponsApi.create.coupon(
testCoupon
);
testCoupon.id = createCouponResponse.body.id;
} );
// Clean up created coupon and order
afterAll( async () => {
await couponsApi.delete.coupon( testCoupon.id, true );
await ordersApi.delete.order( orderId, true );
} );
it( 'can add coupon to an order', async () => {
const orderWithCoupon = {
...order,
coupon_lines: [ { code: testCoupon.code } ],
};
const { status, body } = await ordersApi.create.order(
orderWithCoupon
);
orderId = body.id;
expect( status ).toEqual( ordersApi.create.responseCode );
expect( body.coupon_lines ).toHaveLength( 1 );
expect( body.coupon_lines[ 0 ].code ).toEqual( testCoupon.code );
// Test that the coupon meta data exists.
// See: https://github.com/woocommerce/woocommerce/issues/28166.
expect( body.coupon_lines[ 0 ].meta_data ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
key: 'coupon_data',
value: expect.objectContaining( {
code: testCoupon.code,
} ),
} ),
] )
);
} );
} );
} );

View File

@ -0,0 +1,208 @@
const {
taxRatesApi,
productsApi,
ordersApi,
variationsApi,
} = require( '../../endpoints' );
const {
getOrderExample,
getTaxRateExamples,
getVariationExample,
simpleProduct: defaultSimpleProduct,
variableProduct: defaultVariableProduct,
groupedProduct: defaultGroupedProduct,
externalProduct: defaultExternalProduct,
} = require( '../../data' );
/**
* Simple product with Standard tax rate
*/
const simpleProduct = {
...defaultSimpleProduct,
regular_price: '10.00',
tax_class: 'standard',
};
/**
* Variable product with 1 variation with Reduced tax rate
*/
const variableProduct = {
...defaultVariableProduct,
tax_class: 'reduced-rate',
};
const variation = {
...getVariationExample(),
regular_price: '20.00',
tax_class: 'reduced-rate',
};
/**
* External product with Zero rate tax
*/
const externalProduct = {
...defaultExternalProduct,
regular_price: '400.00',
tax_class: 'zero-rate',
};
/**
* Grouped product
*/
const groupedProduct = defaultGroupedProduct;
/**
* Tax rates for each tax class
*/
const { standardTaxRate, reducedTaxRate, zeroTaxRate } = getTaxRateExamples();
/**
* Delete all pre-existing tax rates.
*/
const deletePreExistingTaxRates = async () => {
const { body } = await taxRatesApi.listAll.taxRates( {
_fields: 'id',
} );
if ( Array.isArray( body ) && body.length > 0 ) {
const ids = body.map( ( { id } ) => id );
await taxRatesApi.batch.taxRates( { delete: ids } );
}
};
/**
* Create a tax rate for each tax class, and save their ID's.
*/
const createTaxRates = async () => {
const taxRates = [ standardTaxRate, reducedTaxRate, zeroTaxRate ];
for ( const taxRate of taxRates ) {
const { body } = await taxRatesApi.create.taxRate( taxRate );
taxRate.id = body.id;
}
};
/**
* Create simple, variable, grouped, and external products.
*/
const createProducts = async () => {
// Create a simple product
const { body: createdSimpleProduct } = await productsApi.create.product(
simpleProduct
);
simpleProduct.id = createdSimpleProduct.id;
// Create a variable product with 1 variation
const { body: createdVariableProduct } = await productsApi.create.product(
variableProduct
);
variableProduct.id = createdVariableProduct.id;
await variationsApi.create.variation( variableProduct.id, variation );
// Create a grouped product using the simple product created earlier.
groupedProduct.grouped_products = [ simpleProduct.id ];
const { body: createdGroupedProduct } = await productsApi.create.product(
groupedProduct
);
groupedProduct.id = createdGroupedProduct.id;
// Create an external product
const { body: createdExternalProduct } = await productsApi.create.product(
externalProduct
);
externalProduct.id = createdExternalProduct.id;
};
/**
* The complex order to be created.
*/
const order = {
...getOrderExample(),
shipping_lines: [],
fee_lines: [],
coupon_lines: [],
line_items: [],
};
/**
* Expected totals
*/
const expectedOrderTotal = '442.20';
const expectedTaxTotal = '2.20';
const expectedSimpleProductTaxTotal = '1.00';
const expectedVariableProductTaxTotal = '0.20';
const expectedExternalProductTaxTotal = '0.00';
/**
*
* Test for adding a complex order with different product types and tax classes.
*
* @group api
* @group orders
*
*/
describe( 'Orders API test', () => {
beforeAll( async () => {
await deletePreExistingTaxRates();
await createTaxRates();
await createProducts();
// Add line items to the order
order.line_items = [
{ product_id: simpleProduct.id },
{ product_id: variableProduct.id },
{ product_id: externalProduct.id },
{ product_id: groupedProduct.id },
];
} );
afterAll( async () => {
// Delete order
await ordersApi.delete.order( order.id, true );
// Delete products
await productsApi.batch.products( {
delete: [
simpleProduct.id,
variableProduct.id,
externalProduct.id,
groupedProduct.id,
],
} );
// Delete tax rates
await taxRatesApi.batch.taxRates( {
delete: [ standardTaxRate.id, zeroTaxRate.id, reducedTaxRate.id ],
} );
} );
it( 'can add complex order', async () => {
// Create the complex order and save its ID.
const { status, body } = await ordersApi.create.order( order );
order.id = body.id;
expect( status ).toEqual( ordersApi.create.responseCode );
// Verify order and tax totals
expect( body.total ).toEqual( expectedOrderTotal );
expect( body.total_tax ).toEqual( expectedTaxTotal );
// Verify total tax of each product line item
const expectedTaxTotalsPerLineItem = [
[ simpleProduct, expectedSimpleProductTaxTotal ],
[ variableProduct, expectedVariableProductTaxTotal ],
[ groupedProduct, expectedSimpleProductTaxTotal ],
[ externalProduct, expectedExternalProductTaxTotal ],
];
for ( const [
product,
expectedLineTaxTotal,
] of expectedTaxTotalsPerLineItem ) {
const { total_tax: actualLineTaxTotal } = body.line_items.find(
( { product_id } ) => product_id === product.id
);
expect( actualLineTaxTotal ).toEqual( expectedLineTaxTotal );
}
} );
} );

View File

@ -0,0 +1,86 @@
const { ordersApi } = require( '../../endpoints' );
const { getOrderExample, shared } = require( '../../data' );
/**
* Order to be searched
*/
const order = {
...getOrderExample(),
shipping: {
...shared.customerShipping,
company: 'Murphy LLC',
phone: '6146524353',
},
shipping_lines: [],
fee_lines: [],
coupon_lines: [],
};
/**
* Search parameters to be used.
* The following scenarios are not covered in this test suite because they're already covered in the `List all orders > search` test in `orders.test.js`
* ```
* can search by billing address 1
* can search by shipping address 1
* can search by billing last name
* can search by billing email
* can search by item name
* ```
*/
const searchParams = [
[ 'orderId', 'orderId' ],
[ 'billing first name', order.billing.first_name ],
[ 'billing company name', order.billing.company ],
[ 'billing address 2', order.billing.address_2 ],
[ 'billing city name', order.billing.city ],
[ 'billing post code', order.billing.postcode ],
[ 'billing phone', order.billing.phone ],
[ 'billing state', order.billing.state ],
[ 'shipping first name', order.shipping.first_name ],
[ 'shipping last name', order.shipping.last_name ],
[ 'shipping address 2', order.shipping.address_2 ],
[ 'shipping city', order.shipping.city ],
[ 'shipping post code', order.shipping.postcode ],
[ 'shipping state', order.shipping.state ],
];
/**
* Tests for the WooCommerce Order Search API.
*
* @group api
* @group orders
*
*/
describe( 'Order Search API tests', () => {
beforeAll( async () => {
// Create an order and save its ID
const { body } = await ordersApi.create.order( order );
order.id = body.id;
} );
afterAll( async () => {
// Cleanup: Delete the order
await ordersApi.delete.order( order.id, true );
} );
it.each( searchParams )( 'can search by %s', async ( title, param ) => {
const searchValue = param === 'orderId' ? order.id : param;
const { status, body } = await ordersApi.listAll.orders( {
search: searchValue,
} );
expect( status ).toEqual( ordersApi.listAll.responseCode );
expect( body ).toHaveLength( 1 );
expect( body[ 0 ].id ).toEqual( order.id );
} );
it( 'can return an empty result set when no matches were found', async () => {
const { status, body } = await ordersApi.listAll.orders( {
search: 'Chauncey Smith Kunde',
} );
expect( status ).toEqual( ordersApi.listAll.responseCode );
expect( body ).toEqual( [] );
} );
} );

View File

@ -0,0 +1,215 @@
const { ordersApi, productsApi } = require( '../../endpoints' );
const { order } = require( '../../data' );
/**
* Billing properties to update.
*/
const updatedCustomerBilling = {
first_name: 'Jane',
last_name: 'Doe',
company: 'Automattic',
country: 'US',
address_1: '123 Market Street',
address_2: 'Suite 500',
city: 'Austin',
state: 'TX',
postcode: '73301',
phone: '123456789',
email: 'jane.doe@example.com',
};
/**
* Shipping properties to update.
*/
const updatedCustomerShipping = {
first_name: 'Mike',
last_name: 'Anderson',
company: 'Automattic',
country: 'US',
address_1: '123 Ocean Ave',
address_2: '',
city: 'New York',
state: 'NY',
postcode: '10013',
phone: '123456789',
};
/**
* Data tables to be used for testing the 'Create an order' API.
*/
const statusesDataTable = [
'pending',
'processing',
'on-hold',
'completed',
'cancelled',
'refunded',
'failed',
];
/**
* A simple product that will be added to an order.
*/
const simpleProduct = {
name: 'Incredible Plastic Table',
regular_price: '48',
};
/**
* Tests for the WooCommerce Orders API.
*
* @group api
* @group orders
*
*/
describe( 'Orders API tests: CRUD', () => {
let orderId;
describe( 'Create an order', () => {
it( 'can create a pending order by default', async () => {
// Create an order that has a null status
const requestPayload = {
...order,
status: null,
};
const { body, status } = await ordersApi.create.order(
requestPayload
);
// Save the order ID. It will be used by the retrieve, update, and delete tests.
orderId = body.id;
// Verify that the order status is 'pending'
expect( status ).toEqual( ordersApi.create.responseCode );
expect( typeof body.id ).toEqual( 'number' );
expect( body.status ).toEqual( 'pending' );
} );
it.each( statusesDataTable )(
"can create an order with status '%s'",
async ( expectedStatus ) => {
const requestPayload = {
...order,
status: expectedStatus,
};
const { status, body } = await ordersApi.create.order(
requestPayload
);
expect( status ).toEqual( ordersApi.create.responseCode );
expect( typeof body.id ).toEqual( 'number' );
expect( body.status ).toEqual( expectedStatus );
// Cleanup: Delete this order
await ordersApi.delete.order( body.id, true );
}
);
} );
describe( 'Retrieve an order', () => {
it( 'can retrieve an order', async () => {
const response = await ordersApi.retrieve.order( orderId );
expect( response.status ).toEqual(
ordersApi.retrieve.responseCode
);
expect( response.body.id ).toEqual( orderId );
} );
} );
describe( 'Update an order', () => {
beforeAll( async () => {
// Create the product and save its id
const { body } = await productsApi.create.product( simpleProduct );
simpleProduct.id = body.id;
} );
afterAll( async () => {
// Delete the created product
await productsApi.delete.product( simpleProduct.id, true );
} );
it.each( statusesDataTable )(
"can update status of an order to '%s'",
async ( expectedOrderStatus ) => {
const requestPayload = {
status: expectedOrderStatus,
};
const { status, body } = await ordersApi.update.order(
orderId,
requestPayload
);
expect( status ).toEqual( ordersApi.update.responseCode );
expect( body.id ).toEqual( orderId );
expect( body.status ).toEqual( expectedOrderStatus );
}
);
it( 'can add shipping and billing contacts to an order', async () => {
// Update the billing and shipping fields on the order
order.billing = updatedCustomerBilling;
order.shipping = updatedCustomerShipping;
const response = await ordersApi.update.order( orderId, order );
expect( response.status ).toEqual( ordersApi.update.responseCode );
expect( response.body.billing ).toEqual( updatedCustomerBilling );
expect( response.body.shipping ).toEqual( updatedCustomerShipping );
} );
it( 'can add a product to an order', async () => {
// Add the product to the order
const requestPayload = {
line_items: [ { product_id: simpleProduct.id } ],
};
const { body, status } = await ordersApi.update.order(
orderId,
requestPayload
);
// Verify that the added product has the correct values
expect( status ).toEqual( ordersApi.update.responseCode );
expect( body.line_items ).toHaveLength( 1 );
expect( body.line_items[ 0 ].product_id ).toEqual(
simpleProduct.id
);
expect( body.line_items[ 0 ].name ).toEqual( simpleProduct.name );
} );
it( 'can pay for an order', async () => {
// Setup: Set order status to 'pending'
await ordersApi.update.order( orderId, {
status: 'pending',
} );
// Pay for the order by setting `set_paid` to true
const updateRequestPayload = {
set_paid: true,
};
const { status, body } = await ordersApi.update.order(
orderId,
updateRequestPayload
);
expect( status ).toEqual( ordersApi.update.responseCode );
expect( body.id ).toEqual( orderId );
// Validate that the status of the order was automatically set to 'processing'
expect( body.status ).toEqual( 'processing' );
// Validate that the date_paid and date_paid_gmt properties are no longer null
expect( body.date_paid ).not.toBeNull();
expect( body.date_paid_gmt ).not.toBeNull();
} );
} );
describe( 'Delete an order', () => {
it( 'can permanently delete an order', async () => {
// Delete the order.
const response = await ordersApi.delete.order( orderId, true );
expect( response.status ).toEqual( ordersApi.delete.responseCode );
// Verify that the order can no longer be retrieved.
const getOrderResponse = await ordersApi.retrieve.order( orderId );
expect( getOrderResponse.status ).toEqual( 404 );
} );
} );
} );

View File

@ -1,10 +1,11 @@
const { ordersApi } = require('../../endpoints/orders');
const { order } = require('../../data');
const { ordersApi } = require( '../../endpoints/orders' );
const { order } = require( '../../data' );
const { createSampleData, deleteSampleData } = require( '../../data/orders' );
/**
* Billing properties to update.
*/
const updatedCustomerBilling = {
const updatedCustomerBilling = {
first_name: 'Jane',
last_name: 'Doe',
company: 'Automattic',
@ -41,43 +42,523 @@ const updatedCustomerShipping = {
* @group orders
*
*/
describe('Orders API tests', () => {
let orderId;
describe( 'Orders API tests', () => {
let orderId, sampleData;
it('can create an order', async () => {
beforeAll( async () => {
sampleData = await createSampleData();
}, 100000 );
afterAll( async () => {
await deleteSampleData( sampleData );
}, 10000 );
it( 'can create an order', async () => {
const response = await ordersApi.create.order( order );
expect( response.status ).toEqual( ordersApi.create.responseCode );
expect( response.body.id ).toBeDefined();
orderId = response.body.id;
// Validate the data type and verify the order is in a pending state
expect( typeof response.body.status ).toBe('string');
expect( response.body.status ).toEqual('pending');
});
expect( typeof response.body.status ).toBe( 'string' );
expect( response.body.status ).toEqual( 'pending' );
} );
it('can retrieve an order', async () => {
it( 'can retrieve an order', async () => {
const response = await ordersApi.retrieve.order( orderId );
expect( response.status ).toEqual( ordersApi.retrieve.responseCode );
expect( response.body.id ).toEqual( orderId );
});
} );
it('can add shipping and billing contacts to an order', async () => {
it( 'can add shipping and billing contacts to an order', async () => {
// Update the billing and shipping fields on the order
order.billing = updatedCustomerBilling;
order.shipping = updatedCustomerShipping;
const response = await ordersApi.update.order( orderId, order );
expect( response.status).toEqual( ordersApi.update.responseCode );
expect( response.status ).toEqual( ordersApi.update.responseCode );
expect( response.body.billing ).toEqual( updatedCustomerBilling );
expect( response.body.shipping ).toEqual( updatedCustomerShipping );
});
} );
it('can permanently delete an order', async () => {
it( 'can permanently delete an order', async () => {
const response = await ordersApi.delete.order( orderId, true );
expect( response.status ).toEqual( ordersApi.delete.responseCode );
const getOrderResponse = await ordersApi.retrieve.order( orderId );
expect( getOrderResponse.status ).toEqual( 404 );
});
});
} );
describe( 'List all orders', () => {
const ORDERS_COUNT = 10;
it( 'pagination', async () => {
const pageSize = 4;
const page1 = await ordersApi.listAll.orders( {
per_page: pageSize,
} );
const page2 = await ordersApi.listAll.orders( {
per_page: pageSize,
page: 2,
} );
expect( page1.statusCode ).toEqual( 200 );
expect( page2.statusCode ).toEqual( 200 );
// Verify total page count.
expect( page1.headers[ 'x-wp-total' ] ).toEqual(
ORDERS_COUNT.toString()
);
expect( page1.headers[ 'x-wp-totalpages' ] ).toEqual( '3' );
// Verify we get pageSize'd arrays.
expect( Array.isArray( page1.body ) ).toBe( true );
expect( Array.isArray( page2.body ) ).toBe( true );
expect( page1.body ).toHaveLength( pageSize );
expect( page2.body ).toHaveLength( pageSize );
// Ensure all of the order IDs are unique (no page overlap).
const allOrderIds = page1.body
.concat( page2.body )
.reduce( ( acc, { id } ) => {
acc[ id ] = 1;
return acc;
}, {} );
expect( Object.keys( allOrderIds ) ).toHaveLength( pageSize * 2 );
// Verify that offset takes precedent over page number.
const page2Offset = await ordersApi.listAll.orders( {
per_page: pageSize,
page: 2,
offset: pageSize + 1,
} );
// The offset pushes the result set 1 order past the start of page 2.
expect( page2Offset.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( { id: page2.body[ 0 ].id } ),
] )
);
expect( page2Offset.body[ 0 ].id ).toEqual( page2.body[ 1 ].id );
// Verify the last page only has 1 order as we expect.
const lastPage = await ordersApi.listAll.orders( {
per_page: pageSize,
page: 3,
} );
expect( Array.isArray( lastPage.body ) ).toBe( true );
expect( lastPage.body ).toHaveLength( 2 );
// Verify a page outside the total page count is empty.
const page6 = await ordersApi.listAll.orders( {
per_page: pageSize,
page: 6,
} );
expect( Array.isArray( page6.body ) ).toBe( true );
expect( page6.body ).toHaveLength( 0 );
} );
it( 'inclusion / exclusion', async () => {
const allOrders = await ordersApi.listAll.orders( {
per_page: 10,
} );
expect( allOrders.statusCode ).toEqual( 200 );
const allOrdersIds = allOrders.body.map( ( order ) => order.id );
expect( allOrdersIds ).toHaveLength( ORDERS_COUNT );
const ordersToFilter = [
allOrdersIds[ 0 ],
allOrdersIds[ 2 ],
allOrdersIds[ 4 ],
allOrdersIds[ 7 ],
];
const included = await ordersApi.listAll.orders( {
per_page: 20,
include: ordersToFilter.join( ',' ),
} );
expect( included.statusCode ).toEqual( 200 );
expect( included.body ).toHaveLength( ordersToFilter.length );
expect( included.body ).toEqual(
expect.arrayContaining(
ordersToFilter.map( ( id ) =>
expect.objectContaining( { id } )
)
)
);
const excluded = await ordersApi.listAll.orders( {
per_page: 20,
exclude: ordersToFilter.join( ',' ),
} );
expect( excluded.statusCode ).toEqual( 200 );
expect( excluded.body ).toHaveLength(
ORDERS_COUNT - ordersToFilter.length
);
expect( excluded.body ).toEqual(
expect.not.arrayContaining(
ordersToFilter.map( ( id ) =>
expect.objectContaining( { id } )
)
)
);
} );
it( 'parent', async () => {
const result1 = await ordersApi.listAll.orders( {
parent: sampleData.hierarchicalOrders.parent.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[ 0 ].id ).toBe(
sampleData.hierarchicalOrders.child.id
);
const result2 = await ordersApi.listAll.orders( {
parent_exclude: sampleData.hierarchicalOrders.parent.id,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( {
id: sampleData.hierarchicalOrders.child.id,
} ),
] )
);
} );
it( 'status', async () => {
const result1 = await ordersApi.listAll.orders( {
status: 'completed',
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 2 );
expect( result1.body ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
status: 'completed',
customer_id: 0,
line_items: expect.arrayContaining( [
expect.objectContaining( {
name: 'Single',
quantity: 2,
} ),
expect.objectContaining( {
name: 'Beanie with Logo',
quantity: 3,
} ),
expect.objectContaining( {
name: 'T-Shirt',
quantity: 1,
} ),
] ),
} ),
expect.objectContaining( {
status: 'completed',
customer_id: sampleData.customers.tina.id,
line_items: expect.arrayContaining( [
expect.objectContaining( {
name: 'Sunglasses',
quantity: 1,
} ),
] ),
} ),
] )
);
const result2 = await ordersApi.listAll.orders( {
status: 'processing',
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 8 );
expect( result2.body ).toEqual(
expect.not.arrayContaining(
result1.body.map( ( { id } ) =>
expect.objectContaining( { id } )
)
)
);
} );
it( 'customer', async () => {
const result1 = await ordersApi.listAll.orders( {
customer: sampleData.customers.john.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 5 );
result1.body.forEach( ( order ) =>
expect( order ).toEqual(
expect.objectContaining( {
customer_id: sampleData.customers.john.id,
} )
)
);
const result2 = await ordersApi.listAll.orders( {
customer: 0,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 3 );
result2.body.forEach( ( order ) =>
expect( order ).toEqual(
expect.objectContaining( {
customer_id: 0,
} )
)
);
} );
it( 'product', async () => {
const beanie = sampleData.testProductData.simpleProducts.find(
( p ) => p.name === 'Beanie'
);
const result1 = await ordersApi.listAll.orders( {
product: beanie.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 2 );
result1.body.forEach( ( order ) =>
expect( order ).toEqual(
expect.objectContaining( {
line_items: expect.arrayContaining( [
expect.objectContaining( {
name: 'Beanie',
} ),
] ),
} )
)
);
} );
// NOTE: This does not verify the `taxes` array nested in line items.
// While the precision parameter doesn't affect those values, after some
// discussion it seems `dp` may not be supported in v4 of the API.
it( 'dp (precision)', async () => {
const expectPrecisionToMatch = ( value, dp ) => {
expect( value ).toEqual(
Number.parseFloat( value ).toFixed( dp )
);
};
const verifyOrderPrecision = ( order, dp ) => {
expectPrecisionToMatch( order[ 'discount_total' ], dp );
expectPrecisionToMatch( order[ 'discount_tax' ], dp );
expectPrecisionToMatch( order[ 'shipping_total' ], dp );
expectPrecisionToMatch( order[ 'shipping_tax' ], dp );
expectPrecisionToMatch( order[ 'cart_tax' ], dp );
expectPrecisionToMatch( order[ 'total' ], dp );
expectPrecisionToMatch( order[ 'total_tax' ], dp );
order[ 'line_items' ].forEach( ( lineItem ) => {
expectPrecisionToMatch( lineItem[ 'total' ], dp );
expectPrecisionToMatch( lineItem[ 'total_tax' ], dp );
} );
order[ 'tax_lines' ].forEach( ( taxLine ) => {
expectPrecisionToMatch( taxLine[ 'tax_total' ], dp );
expectPrecisionToMatch(
taxLine[ 'shipping_tax_total' ],
dp
);
} );
order[ 'shipping_lines' ].forEach( ( shippingLine ) => {
expectPrecisionToMatch( shippingLine[ 'total' ], dp );
expectPrecisionToMatch( shippingLine[ 'total_tax' ], dp );
} );
order[ 'fee_lines' ].forEach( ( feeLine ) => {
expectPrecisionToMatch( feeLine[ 'total' ], dp );
expectPrecisionToMatch( feeLine[ 'total_tax' ], dp );
} );
order[ 'refunds' ].forEach( ( refund ) => {
expectPrecisionToMatch( refund[ 'total' ], dp );
} );
};
const result1 = await ordersApi.retrieve.order(
sampleData.precisionOrder.id,
{
dp: 1,
}
);
expect( result1.statusCode ).toEqual( 200 );
verifyOrderPrecision( result1.body, 1 );
const result2 = await ordersApi.retrieve.order(
sampleData.precisionOrder.id,
{
dp: 3,
}
);
expect( result2.statusCode ).toEqual( 200 );
verifyOrderPrecision( result2.body, 3 );
const result3 = await ordersApi.retrieve.order(
sampleData.precisionOrder.id
);
expect( result3.statusCode ).toEqual( 200 );
verifyOrderPrecision( result3.body, 2 ); // The default value for 'dp' is 2.
} );
it( 'search', async () => {
// By default, 'search' looks in:
// - _billing_address_index
// - _shipping_address_index
// - _billing_last_name
// - _billing_email
// - order_item_name
// Test billing email.
const result1 = await ordersApi.listAll.orders( {
search: 'example.com',
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 7 );
result1.body.forEach( ( order ) =>
expect( order.billing.email ).toContain( 'example.com' )
);
// Test billing address.
const result2 = await ordersApi.listAll.orders( {
search: 'gainesville', // Intentionally lowercase.
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[ 0 ].id ).toEqual( sampleData.guestOrder.id );
// Test shipping address.
const result3 = await ordersApi.listAll.orders( {
search: 'Incognito',
} );
expect( result3.statusCode ).toEqual( 200 );
expect( result3.body ).toHaveLength( 1 );
expect( result3.body[ 0 ].id ).toEqual( sampleData.guestOrder.id );
// Test billing last name.
const result4 = await ordersApi.listAll.orders( {
search: 'Doe',
} );
expect( result4.statusCode ).toEqual( 200 );
expect( result4.body ).toHaveLength( 5 );
result4.body.forEach( ( order ) =>
expect( order.billing.last_name ).toEqual( 'Doe' )
);
// Test order item name.
const result5 = await ordersApi.listAll.orders( {
search: 'Pennant',
} );
expect( result5.statusCode ).toEqual( 200 );
expect( result5.body ).toHaveLength( 2 );
result5.body.forEach( ( order ) =>
expect( order ).toEqual(
expect.objectContaining( {
line_items: expect.arrayContaining( [
expect.objectContaining( {
name: 'WordPress Pennant',
} ),
] ),
} )
)
);
} );
describe( 'orderby', () => {
// The orders endpoint `orderby` parameter uses WP_Query, so our tests won't
// include slug and title, since they are programmatically generated.
it( 'default', async () => {
// Default = date desc.
const result = await ordersApi.listAll.orders();
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in descending order.
let lastDate = Date.now();
result.body.forEach( ( { date_created } ) => {
const created = Date.parse( date_created + '.000Z' );
expect( lastDate ).toBeGreaterThanOrEqual( created );
lastDate = created;
} );
} );
it( 'date', async () => {
const result = await ordersApi.listAll.orders( {
order: 'asc',
orderby: 'date',
} );
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in ascending order.
let lastDate = 0;
result.body.forEach( ( { date_created } ) => {
const created = Date.parse( date_created + '.000Z' );
expect( created ).toBeGreaterThanOrEqual( lastDate );
lastDate = created;
} );
} );
it( 'id', async () => {
const result1 = await ordersApi.listAll.orders( {
order: 'asc',
orderby: 'id',
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
let lastId = 0;
result1.body.forEach( ( { id } ) => {
expect( id ).toBeGreaterThan( lastId );
lastId = id;
} );
const result2 = await ordersApi.listAll.orders( {
order: 'desc',
orderby: 'id',
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
lastId = Number.MAX_SAFE_INTEGER;
result2.body.forEach( ( { id } ) => {
expect( lastId ).toBeGreaterThan( id );
lastId = id;
} );
} );
it( 'include', async () => {
const includeIds = [
sampleData.precisionOrder.id,
sampleData.hierarchicalOrders.parent.id,
sampleData.guestOrder.id,
];
const result1 = await ordersApi.listAll.orders( {
order: 'asc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result1.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
const result2 = await ordersApi.listAll.orders( {
order: 'desc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result2.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
} );
} );
} );
} );

View File

@ -1,777 +0,0 @@
/**
* Internal dependencies
*/
const { createSampleData, deleteSampleData } = require( '../../data/products' );
const { productsApi } = require('../../endpoints/products');
/**
* Tests for the WooCommerce Products API.
*
* @group api
* @group products
*
*/
describe( 'Products API tests', () => {
const PRODUCTS_COUNT = 20;
let sampleData;
beforeAll( async () => {
sampleData = await createSampleData();
}, 10000 );
afterAll( async () => {
await deleteSampleData( sampleData );
}, 10000 );
describe( 'List all products', () => {
it( 'defaults', async () => {
const result = await productsApi.listAll.products();
expect( result.statusCode ).toEqual( 200 );
expect( result.headers['x-wp-total'] ).toEqual( PRODUCTS_COUNT.toString() );
expect( result.headers['x-wp-totalpages'] ).toEqual( '2' );
} );
it( 'pagination', async () => {
const pageSize = 6;
const page1 = await productsApi.listAll.products( {
per_page: pageSize,
} );
const page2 = await productsApi.listAll.products( {
per_page: pageSize,
page: 2,
} );
expect( page1.statusCode ).toEqual( 200 );
expect( page2.statusCode ).toEqual( 200 );
// Verify total page count.
expect( page1.headers['x-wp-total'] ).toEqual( PRODUCTS_COUNT.toString() );
expect( page1.headers['x-wp-totalpages'] ).toEqual( '4' );
// Verify we get pageSize'd arrays.
expect( Array.isArray( page1.body ) ).toBe( true );
expect( Array.isArray( page2.body ) ).toBe( true );
expect( page1.body ).toHaveLength( pageSize );
expect( page2.body ).toHaveLength( pageSize );
// Ensure all of the product IDs are unique (no page overlap).
const allProductIds = page1.body.concat( page2.body ).reduce( ( acc, product ) => {
acc[ product.id ] = 1;
return acc;
}, {} );
expect( Object.keys( allProductIds ) ).toHaveLength( pageSize * 2 );
// Verify that offset takes precedent over page number.
const page2Offset = await productsApi.listAll.products( {
per_page: pageSize,
page: 2,
offset: pageSize + 1,
} );
// The offset pushes the result set 1 product past the start of page 2.
expect( page2Offset.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( { id: page2.body[0].id } )
] )
);
expect( page2Offset.body[0].id ).toEqual( page2.body[1].id );
// Verify the last page only has 2 products as we expect.
const lastPage = await productsApi.listAll.products( {
per_page: pageSize,
page: 4,
} );
expect( Array.isArray( lastPage.body ) ).toBe( true );
expect( lastPage.body ).toHaveLength( 2 );
// Verify a page outside the total page count is empty.
const page6 = await productsApi.listAll.products( {
per_page: pageSize,
page: 6,
} );
expect( Array.isArray( page6.body ) ).toBe( true );
expect( page6.body ).toHaveLength( 0 );
} );
it( 'search', async () => {
// Match in the short description.
const result1 = await productsApi.listAll.products( {
search: 'external'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'WordPress Pennant' );
// Match in the product name.
const result2 = await productsApi.listAll.products( {
search: 'pocket'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'Hoodie with Pocket' );
} );
it( 'inclusion / exclusion', async () => {
const allProducts = await productsApi.listAll.products( {
per_page: 20,
} );
expect( allProducts.statusCode ).toEqual( 200 );
const allProductIds = allProducts.body.map( product => product.id );
expect( allProductIds ).toHaveLength( PRODUCTS_COUNT );
const productsToFilter = [
allProductIds[2],
allProductIds[4],
allProductIds[7],
allProductIds[13],
];
const included = await productsApi.listAll.products( {
per_page: 20,
include: productsToFilter.join( ',' ),
} );
expect( included.statusCode ).toEqual( 200 );
expect( included.body ).toHaveLength( productsToFilter.length );
expect( included.body ).toEqual(
expect.arrayContaining(
productsToFilter.map( id => expect.objectContaining( { id } ) )
)
);
const excluded = await productsApi.listAll.products( {
per_page: 20,
exclude: productsToFilter.join( ',' ),
} );
expect( excluded.statusCode ).toEqual( 200 );
expect( excluded.body ).toHaveLength( PRODUCTS_COUNT - productsToFilter.length );
expect( excluded.body ).toEqual(
expect.not.arrayContaining(
productsToFilter.map( id => expect.objectContaining( { id } ) )
)
);
} );
it( 'slug', async () => {
// Match by slug.
const result1 = await productsApi.listAll.products( {
slug: 't-shirt-with-logo'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].slug ).toBe( 't-shirt-with-logo' );
// No matches
const result2 = await productsApi.listAll.products( {
slug: 'no-product-with-this-slug'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'sku', async () => {
// Match by SKU.
const result1 = await productsApi.listAll.products( {
sku: 'woo-sunglasses'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].sku ).toBe( 'woo-sunglasses' );
// No matches
const result2 = await productsApi.listAll.products( {
sku: 'no-product-with-this-sku'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'type', async () => {
const result1 = await productsApi.listAll.products( {
type: 'simple'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.headers['x-wp-total'] ).toEqual( '16' );
const result2 = await productsApi.listAll.products( {
type: 'external'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'WordPress Pennant' );
const result3 = await productsApi.listAll.products( {
type: 'variable'
} );
expect( result3.statusCode ).toEqual( 200 );
expect( result3.body ).toHaveLength( 2 );
const result4 = await productsApi.listAll.products( {
type: 'grouped'
} );
expect( result4.statusCode ).toEqual( 200 );
expect( result4.body ).toHaveLength( 1 );
expect( result4.body[0].name ).toBe( 'Logo Collection' );
} );
it( 'featured', async () => {
const featured = [
expect.objectContaining( { name: 'Hoodie with Zipper' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Sunglasses' } ),
expect.objectContaining( { name: 'Cap' } ),
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
];
const result1 = await productsApi.listAll.products( {
featured: true,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( featured.length );
expect( result1.body ).toEqual( expect.arrayContaining( featured ) );
const result2 = await productsApi.listAll.products( {
featured: false,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( featured ) );
} );
it( 'categories', async () => {
const accessory = [
expect.objectContaining( { name: 'Beanie' } ),
]
const hoodies = [
expect.objectContaining( { name: 'Hoodie with Zipper' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Hoodie with Logo' } ),
expect.objectContaining( { name: 'Hoodie' } ),
];
// Verify that subcategories are included.
const result1 = await productsApi.listAll.products( {
per_page: 20,
category: sampleData.categories.clothing.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toEqual( expect.arrayContaining( accessory ) );
expect( result1.body ).toEqual( expect.arrayContaining( hoodies ) );
// Verify sibling categories are not.
const result2 = await productsApi.listAll.products( {
category: sampleData.categories.hoodies.id,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( accessory ) );
expect( result2.body ).toEqual( expect.arrayContaining( hoodies ) );
} );
it( 'on sale', async () => {
const onSale = [
expect.objectContaining( { name: 'Beanie with Logo' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Single' } ),
expect.objectContaining( { name: 'Cap' } ),
expect.objectContaining( { name: 'Belt' } ),
expect.objectContaining( { name: 'Beanie' } ),
expect.objectContaining( { name: 'Hoodie' } ),
];
const result1 = await productsApi.listAll.products( {
on_sale: true,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( onSale.length );
expect( result1.body ).toEqual( expect.arrayContaining( onSale ) );
const result2 = await productsApi.listAll.products( {
on_sale: false,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( onSale ) );
} );
it( 'price', async () => {
const result1 = await productsApi.listAll.products( {
min_price: 21,
max_price: 28,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Long Sleeve Tee' );
expect( result1.body[0].price ).toBe( '25' );
const result2 = await productsApi.listAll.products( {
max_price: 5,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 1 );
expect( result2.body[0].name ).toBe( 'Single' );
expect( result2.body[0].price ).toBe( '2' );
const result3 = await productsApi.listAll.products( {
min_price: 5,
order: 'asc',
orderby: 'price',
} );
expect( result3.statusCode ).toEqual( 200 );
expect( result3.body ).toEqual(
expect.not.arrayContaining( [
expect.objectContaining( { name: 'Single' } )
] )
);
} );
it( 'before / after', async () => {
const before = [
expect.objectContaining( { name: 'Album' } ),
expect.objectContaining( { name: 'Single' } ),
expect.objectContaining( { name: 'T-Shirt with Logo' } ),
expect.objectContaining( { name: 'Beanie with Logo' } ),
];
const after = [
expect.objectContaining( { name: 'Hoodie' } ),
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
expect.objectContaining( { name: 'Parent Product' } ),
expect.objectContaining( { name: 'Child Product' } ),
];
const result1 = await productsApi.listAll.products( {
before: '2021-09-05T15:50:19',
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( before.length );
expect( result1.body ).toEqual( expect.arrayContaining( before ) );
const result2 = await productsApi.listAll.products( {
after: '2021-09-18T15:50:18',
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( before ) );
expect( result2.body ).toHaveLength( after.length );
expect( result2.body ).toEqual( expect.arrayContaining( after ) );
} );
it( 'attributes', async () => {
const red = sampleData.attributes.colors.find( term => term.name === 'Red' );
const redProducts = [
expect.objectContaining( { name: 'V-Neck T-Shirt' } ),
expect.objectContaining( { name: 'Hoodie' } ),
expect.objectContaining( { name: 'Beanie' } ),
expect.objectContaining( { name: 'Beanie with Logo' } ),
];
const result = await productsApi.listAll.products( {
attribute: 'pa_color',
attribute_term: red.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( redProducts.length );
expect( result.body ).toEqual( expect.arrayContaining( redProducts ) );
} );
it( 'status', async () => {
const result1 = await productsApi.listAll.products( {
status: 'pending'
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Polo' );
const result2 = await productsApi.listAll.products( {
status: 'draft'
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( 0 );
} );
it( 'shipping class', async () => {
const result = await productsApi.listAll.products( {
shipping_class: sampleData.shippingClasses.freight.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'Long Sleeve Tee' );
} );
it( 'tax class', async () => {
const result = await productsApi.listAll.products( {
tax_class: 'reduced-rate',
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'Sunglasses' );
} );
it( 'stock status', async () => {
const result = await productsApi.listAll.products( {
stock_status: 'onbackorder',
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( 1 );
expect( result.body[0].name ).toBe( 'T-Shirt' );
} );
it( 'tags', async () => {
const coolProducts = [
expect.objectContaining( { name: 'Sunglasses' } ),
expect.objectContaining( { name: 'Hoodie with Pocket' } ),
expect.objectContaining( { name: 'Beanie' } ),
];
const result = await productsApi.listAll.products( {
tag: sampleData.tags.cool.id,
} );
expect( result.statusCode ).toEqual( 200 );
expect( result.body ).toHaveLength( coolProducts.length );
expect( result.body ).toEqual( expect.arrayContaining( coolProducts ) );
} );
it( 'parent', async () => {
const result1 = await productsApi.listAll.products( {
parent: sampleData.hierarchicalProducts.parent.id,
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( 1 );
expect( result1.body[0].name ).toBe( 'Child Product' );
const result2 = await productsApi.listAll.products( {
parent_exclude: sampleData.hierarchicalProducts.parent.id,
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toEqual( expect.not.arrayContaining( [
expect.objectContaining( { name: 'Child Product' } ),
] ) );
} );
describe( 'orderby', () => {
const productNamesAsc = [
'Album',
'Beanie',
'Beanie with Logo',
'Belt',
'Cap',
'Child Product',
'Hoodie',
'Hoodie with Logo',
'Hoodie with Pocket',
'Hoodie with Zipper',
'Logo Collection',
'Long Sleeve Tee',
'Parent Product',
'Polo',
'Single',
'Sunglasses',
'T-Shirt',
'T-Shirt with Logo',
'V-Neck T-Shirt',
'WordPress Pennant',
];
const productNamesDesc = [ ...productNamesAsc ].reverse();
const productNamesByRatingAsc = [
'Sunglasses',
'Cap',
'T-Shirt',
];
const productNamesByRatingDesc = [ ...productNamesByRatingAsc ].reverse();
const productNamesByPopularityDesc = [
'Beanie with Logo',
'Single',
'T-Shirt',
];
const productNamesByPopularityAsc = [ ...productNamesByPopularityDesc ].reverse();
it( 'default', async () => {
// Default = date desc.
const result = await productsApi.listAll.products();
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in descending order.
let lastDate = Date.now();
result.body.forEach( ( { date_created_gmt } ) => {
const created = Date.parse( date_created_gmt + '.000Z' );
expect( lastDate ).toBeGreaterThan( created );
lastDate = created;
} );
} );
it( 'date', async () => {
const result = await productsApi.listAll.products( {
order: 'asc',
orderby: 'date',
} );
expect( result.statusCode ).toEqual( 200 );
// Verify all dates are in ascending order.
let lastDate = 0;
result.body.forEach( ( { date_created_gmt } ) => {
const created = Date.parse( date_created_gmt + '.000Z' );
expect( created ).toBeGreaterThan( lastDate );
lastDate = created;
} );
} );
it( 'id', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'id',
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
let lastId = 0;
result1.body.forEach( ( { id } ) => {
expect( id ).toBeGreaterThan( lastId );
lastId = id;
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'id',
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
lastId = Number.MAX_SAFE_INTEGER;
result2.body.forEach( ( { id } ) => {
expect( lastId ).toBeGreaterThan( id );
lastId = id;
} );
} );
it( 'title', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'title',
per_page: productNamesAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesAsc[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'title',
per_page: productNamesDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesDesc[ idx ] );
} );
} );
it( 'slug', async () => {
const productNamesBySlugAsc = [
'Polo', // The Polo isn't published so it has an empty slug.
...productNamesAsc.filter( p => p !== 'Polo' ),
];
const productNamesBySlugDesc = [ ...productNamesBySlugAsc ].reverse();
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'slug',
per_page: productNamesBySlugAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesBySlugAsc[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'slug',
per_page: productNamesBySlugDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesBySlugDesc[ idx ] );
} );
} );
it( 'price', async () => {
const productNamesMinPriceAsc = [
'Parent Product',
'Child Product',
'Single',
'WordPress Pennant',
'Album',
'V-Neck T-Shirt',
'Cap',
'Beanie with Logo',
'T-Shirt with Logo',
'Beanie',
'T-Shirt',
'Logo Collection',
'Polo',
'Long Sleeve Tee',
'Hoodie with Pocket',
'Hoodie',
'Hoodie with Zipper',
'Hoodie with Logo',
'Belt',
'Sunglasses',
];
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'price',
per_page: productNamesMinPriceAsc.length
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( productNamesMinPriceAsc.length );
// Verify all results are in ascending order.
// The query uses the min price calculated in the product meta lookup table,
// so we can't just check the price property of the response.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesMinPriceAsc[ idx ] );
} );
const productNamesMaxPriceDesc = [
'Sunglasses',
'Belt',
'Hoodie',
'Logo Collection',
'Hoodie with Logo',
'Hoodie with Zipper',
'Hoodie with Pocket',
'Long Sleeve Tee',
'V-Neck T-Shirt',
'Polo',
'T-Shirt',
'Beanie',
'T-Shirt with Logo',
'Beanie with Logo',
'Cap',
'Album',
'WordPress Pennant',
'Single',
'Child Product',
'Parent Product',
];
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'price',
per_page: productNamesMaxPriceDesc.length
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( productNamesMaxPriceDesc.length );
// Verify all results are in descending order.
// The query uses the max price calculated in the product meta lookup table,
// so we can't just check the price property of the response.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesMaxPriceDesc[ idx ] );
} );
} );
// This case will remain skipped until orderby include is fixed.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it( 'include', async () => {
const includeIds = [
sampleData.groupedProducts[ 0 ].id,
sampleData.simpleProducts[ 3 ].id,
sampleData.hierarchicalProducts.parent.id,
];
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result1.statusCode ).toEqual( 200 );
expect( result1.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result1.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'include',
include: includeIds.join( ',' ),
} );
expect( result2.statusCode ).toEqual( 200 );
expect( result2.body ).toHaveLength( includeIds.length );
// Verify all results are in proper order.
result2.body.forEach( ( { id }, idx ) => {
expect( id ).toBe( includeIds[ idx ] );
} );
} );
it( 'rating (desc)', async () => {
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'rating',
per_page: productNamesByRatingDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByRatingDesc[ idx ] );
} );
} );
// This case will remain skipped until ratings can be sorted ascending.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'rating (asc)', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'rating',
per_page: productNamesByRatingAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByRatingAsc[ idx ] );
} );
} );
it( 'popularity (desc)', async () => {
const result2 = await productsApi.listAll.products( {
order: 'desc',
orderby: 'popularity',
per_page: productNamesByPopularityDesc.length,
} );
expect( result2.statusCode ).toEqual( 200 );
// Verify all results are in descending order.
result2.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByPopularityDesc[ idx ] );
} );
} );
// This case will remain skipped until popularity can be sorted ascending.
// See: https://github.com/woocommerce/woocommerce/issues/30354#issuecomment-925955099.
it.skip( 'popularity (asc)', async () => {
const result1 = await productsApi.listAll.products( {
order: 'asc',
orderby: 'popularity',
per_page: productNamesByPopularityAsc.length,
} );
expect( result1.statusCode ).toEqual( 200 );
// Verify all results are in ascending order.
result1.body.forEach( ( { name }, idx ) => {
expect( name ).toBe( productNamesByPopularityAsc[ idx ] );
} );
} );
} );
} );
} );

View File

@ -0,0 +1,57 @@
const { shippingMethodsApi } = require( '../../endpoints' );
const { getShippingMethodExample } = require( '../../data' );
/**
* Shipping zone id for "Locations not covered by your other zones".
*/
const shippingZoneId = 0;
/**
* Data table for shipping methods.
*/
const shippingMethods = [
[ 'Flat rate', 'flat_rate', '10' ],
[ 'Free shipping', 'free_shipping', undefined ],
[ 'Local pickup', 'local_pickup', '30' ],
];
/**
* Tests for the WooCommerce Shipping methods API.
*
* @group api
* @group shipping-methods
*
*/
describe( 'Shipping methods API tests', () => {
it.each( shippingMethods )(
"can add a '%s' shipping method",
async ( methodTitle, methodId, cost ) => {
const shippingMethod = getShippingMethodExample( methodId, cost );
const {
status,
body,
} = await shippingMethodsApi.create.shippingMethod(
shippingZoneId,
shippingMethod
);
expect( status ).toEqual( shippingMethodsApi.create.responseCode );
expect( typeof body.id ).toEqual( 'number' );
expect( body.method_id ).toEqual( methodId );
expect( body.method_title ).toEqual( methodTitle );
expect( body.enabled ).toEqual( true );
if ( [ 'flat_rate', 'local_pickup' ].includes( methodId ) ) {
expect( body.settings.cost.value ).toEqual( cost );
}
// Cleanup: Delete the shipping method
await shippingMethodsApi.delete.shippingMethod(
shippingZoneId,
body.id,
true
);
}
);
} );

View File

@ -0,0 +1,124 @@
const { shippingZonesApi } = require( '../../endpoints' );
const { getShippingZoneExample } = require( '../../data' );
/**
* Shipping zone to be created, retrieved, updated, and deleted by the tests.
*/
const shippingZone = getShippingZoneExample();
/**
* Tests for the WooCommerce Shipping zones API.
*
* @group api
* @group shipping-zones
*
*/
describe( 'Shipping zones API tests', () => {
it( 'cannot delete the default shipping zone "Locations not covered by your other zones"', async () => {
// Delete all pre-existing shipping zones
const { body } = await shippingZonesApi.listAll.shippingZones( {
_fields: 'id',
} );
const ids = body.map( ( { id } ) => id );
for ( const id of ids ) {
await shippingZonesApi.delete.shippingZone( id, true );
}
// Verify that the default shipping zone remains
const {
body: remainingZones,
} = await shippingZonesApi.listAll.shippingZones( {
_fields: 'id',
} );
expect( remainingZones ).toHaveLength( 1 );
expect( remainingZones[ 0 ].id ).toEqual( 0 );
} );
it( 'cannot update the default shipping zone', async () => {
const newZoneDetails = {
name: 'Default shipping zone',
};
const { status, body } = await shippingZonesApi.update.shippingZone(
0,
newZoneDetails
);
expect( status ).toEqual( 403 );
expect( body.code ).toEqual(
'woocommerce_rest_shipping_zone_invalid_zone'
);
expect( body.message ).toEqual(
'The "locations not covered by your other zones" zone cannot be updated.'
);
} );
it( 'can create a shipping zone', async () => {
const { status, body } = await shippingZonesApi.create.shippingZone(
shippingZone
);
expect( status ).toEqual( shippingZonesApi.create.responseCode );
expect( typeof body.id ).toEqual( 'number' );
expect( body.name ).toEqual( shippingZone.name );
// Save the shipping zone ID. It will be used by other tests.
shippingZone.id = body.id;
} );
it( 'can retrieve a shipping zone', async () => {
const { status, body } = await shippingZonesApi.retrieve.shippingZone(
shippingZone.id
);
expect( status ).toEqual( shippingZonesApi.retrieve.responseCode );
expect( body.id ).toEqual( shippingZone.id );
} );
it( 'can list all shipping zones', async () => {
const param = {
_fields: 'id',
};
const { status, body } = await shippingZonesApi.listAll.shippingZones(
param
);
expect( body ).toHaveLength( 2 ); // the test shipping zone, and the default 'Locations not covered by your other zones'
expect( status ).toEqual( shippingZonesApi.listAll.responseCode );
expect( body ).toEqual(
expect.arrayContaining( [ { id: shippingZone.id } ] )
);
} );
it( 'can update a shipping zone', async () => {
const updatedShippingZone = {
name: 'United States (Domestic)',
};
const { status, body } = await shippingZonesApi.update.shippingZone(
shippingZone.id,
updatedShippingZone
);
expect( status ).toEqual( shippingZonesApi.retrieve.responseCode );
expect( body.id ).toEqual( shippingZone.id );
expect( body.name ).toEqual( updatedShippingZone.name );
} );
it( 'can delete a shipping zone', async () => {
const { status, body } = await shippingZonesApi.delete.shippingZone(
shippingZone.id,
true
);
expect( status ).toEqual( shippingZonesApi.delete.responseCode );
expect( body.id ).toEqual( shippingZone.id );
// Verify that the shipping zone can no longer be retrieved
const {
status: retrieveStatus,
} = await shippingZonesApi.retrieve.shippingZone( shippingZone.id );
expect( retrieveStatus ).toEqual( 404 );
} );
} );

View File

@ -1,12 +1,9 @@
module.exports = {
parser: '@typescript-eslint/parser',
env: {
'jest/globals': true
'jest/globals': true,
},
ignorePatterns: [
'dist/',
'node_modules/'
],
ignorePatterns: [ 'dist/', 'node_modules/' ],
rules: {
'no-unused-vars': 'off',
'no-dupe-class-members': 'off',
@ -14,18 +11,11 @@ module.exports = {
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 2,
},
plugins: [
'@typescript-eslint'
],
extends: [
'plugin:@wordpress/eslint-plugin/recommended-with-formatting'
],
plugins: [ '@typescript-eslint/eslint-plugin' ],
extends: [ 'plugin:@wordpress/eslint-plugin/recommended-with-formatting' ],
overrides: [
{
files: [
'**/*.js',
'**/*.ts'
],
files: [ '**/*.js', '**/*.ts' ],
settings: {
jsdoc: {
mode: 'typescript',
@ -33,13 +23,10 @@ module.exports = {
},
},
{
files: [
'**/*.spec.ts',
'**/*.test.ts'
],
files: [ '**/*.spec.ts', '**/*.test.ts' ],
rules: {
'no-console': 'off',
}
}
]
}
},
},
],
};

View File

@ -1,18 +0,0 @@
# Editors
project.xml
project.properties
/nbproject/private/
.buildpath
.project
.settings*
.idea
.vscode
*.sublime-project
*.sublime-workspace
.sublimelinterrc
# Build Artifacts
/node_modules/
/dist/
tsconfig.tsbuildinfo

View File

@ -1,5 +1,9 @@
# Unreleased
## Added
- Added low stock threshold field for products
# 0.2.0
## Added

View File

@ -0,0 +1,7 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/<last-commit-hash-before-this-merge>/packages/js/api/CHANGELOG.md).

View File

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/api",
"description": "WooCommerce API",
"type": "library",
"license": "GPL-3.0-or-later",
"minimum-stability": "dev",
"require-dev": {
"automattic/jetpack-changelogger": "3.0.2"
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/PackageFormatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "NEXT_CHANGELOG.md"
}
}
}

1021
packages/js/api/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
],
"sideEffects": false,
"scripts": {
"preinstall": "npx only-allow pnpm",
"clean": "rm -rf ./dist ./tsconfig.tsbuildinfo",
"compile": "tsc -b",
"build": "pnpm run clean && npm run compile",
@ -34,20 +35,21 @@
"test": "jest"
},
"dependencies": {
"axios": "0.21.2",
"axios": "^0.24.0",
"create-hmac": "1.1.7",
"oauth-1.0a": "2.2.6"
},
"devDependencies": {
"@types/create-hmac": "1.1.0",
"@types/jest": "25.2.1",
"@types/moxios": "^0.4.9",
"@types/jest": "^27.0.2",
"@types/node": "13.13.5",
"jest": "^25.1.0",
"jest-mock-extended": "^1.0.10",
"moxios": "0.4.0",
"@typescript-eslint/eslint-plugin": "^5.3.1",
"@typescript-eslint/parser": "^5.3.1",
"axios-mock-adapter": "^1.20.0",
"eslint": "^8.2.0",
"jest": "^27.3.1",
"ts-jest": "25.5.0",
"typescript": "3.9.7"
"typescript": "^4.4.4"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,50 @@
{
"root": "packages/js/api/",
"sourceRoot": "packages/js/api/src",
"projectType": "library",
"targets": {
"changelog": {
"executor": "./tools/executors/changelogger:changelog",
"options": {
"action": "add",
"cwd": "packages/js/api"
}
},
"build": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "build"
}
},
"clean": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "clean"
}
},
"compile": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "compile"
}
},
"prepare": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "prepare"
}
},
"lint": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "lint"
}
},
"test": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test"
}
}
}
}

View File

@ -101,7 +101,7 @@ describe( 'ModelRepository', () => {
const created = await repository.create( { parent: 'yes' }, { childName: 'test' } );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { childName: 'test' } );
expect( callback ).toHaveBeenCalledWith( { parent: 'yes' }, { childName: 'test' } );
} );
it( 'should throw error on create without callback', () => {

View File

@ -1,52 +1,54 @@
import { mocked } from 'ts-jest/utils'
import { ModelTransformerTransformation } from '../model-transformer-transformation';
import { ModelTransformer } from '../../model-transformer';
import { mock, MockProxy } from 'jest-mock-extended';
import { DummyModel } from '../../../__test_data__/dummy-model';
jest.mock( '../../model-transformer' );
describe( 'ModelTransformerTransformation', () => {
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
let propertyTransformer: ModelTransformer< any >;
let transformation: ModelTransformerTransformation< any >;
beforeEach( () => {
mockTransformer = mock< ModelTransformer< any > >();
propertyTransformer = new ModelTransformer( [] );
transformation = new ModelTransformerTransformation< DummyModel >(
'test',
DummyModel,
mockTransformer,
propertyTransformer,
);
} );
it( 'should execute child transformer', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
mocked( propertyTransformer.toModel ).mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { toModel: 'Test' } } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
expect( propertyTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
mocked( propertyTransformer.fromModel ).mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { fromModel: 'Test' } } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
expect( propertyTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
} );
it( 'should execute child transformer on array', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
mocked( propertyTransformer.toModel ).mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { toModel: 'Test' }, { toModel: 'Test' } ] } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test2' );
expect( propertyTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
expect( propertyTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test2' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
mocked( propertyTransformer.fromModel ).mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { fromModel: 'Test' }, { fromModel: 'Test' } ] } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test2' );
expect( propertyTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
expect( propertyTransformer.fromModel ).toHaveBeenCalledWith( 'Test2' );
} );
} );

View File

@ -1,56 +1,57 @@
import * as moxios from 'moxios';
import MockAdapter from 'axios-mock-adapter';
import { AxiosClient } from '../axios-client';
import { HTTPResponse } from '../../http-client';
import { AxiosInterceptor } from '../axios-interceptor';
import { mock } from 'jest-mock-extended';
import axios from 'axios';
class DummyInterceptor extends AxiosInterceptor {
public start = jest.fn();
public stop = jest.fn();
}
describe( 'AxiosClient', () => {
let httpClient: AxiosClient;
beforeEach( () => {
moxios.install();
} );
afterEach( () => {
moxios.uninstall();
} );
it( 'should transform to HTTPResponse', async () => {
const adapter = new MockAdapter( axios );
httpClient = new AxiosClient( { baseURL: 'http://test.test' } );
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
adapter
.onGet( '/test' )
.reply(
200,
{ test: 'value' },
{ 'content-type': 'application/json' }
);
const response = await httpClient.get( '/test' );
adapter.restore();
expect( response ).toBeInstanceOf( HTTPResponse );
expect( response ).toHaveProperty( 'statusCode', 200 );
expect( response ).toHaveProperty( 'headers', { 'content-type': 'application/json' } );
expect( response ).toHaveProperty( 'headers', {
'content-type': 'application/json',
} );
expect( response ).toHaveProperty( 'data', { test: 'value' } );
} );
it( 'should start extra interceptors', async () => {
const interceptor = mock< AxiosInterceptor >();
const interceptor = new DummyInterceptor();
httpClient = new AxiosClient(
{ baseURL: 'http://test.test' },
[ interceptor ],
);
const adapter = new MockAdapter( axios );
moxios.stubRequest( '/test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
httpClient = new AxiosClient( { baseURL: 'http://test.test' }, [
interceptor,
] );
adapter.onGet( '/test' ).reply( 200, { test: 'value' } );
await httpClient.get( '/test' );
adapter.restore();
expect( interceptor.start ).toHaveBeenCalled();
} );
} );

View File

@ -1,5 +1,5 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import MockAdapter from 'axios-mock-adapter';
import { AxiosInterceptor } from '../axios-interceptor';
class TestInterceptor extends AxiosInterceptor {}
@ -7,10 +7,11 @@ class TestInterceptor extends AxiosInterceptor {}
describe( 'AxiosInterceptor', () => {
let interceptors: TestInterceptor[];
let axiosInstance: AxiosInstance;
let adapter: MockAdapter;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
adapter = new MockAdapter( axiosInstance );
interceptors = [];
} );
@ -18,11 +19,11 @@ describe( 'AxiosInterceptor', () => {
for ( const interceptor of interceptors ) {
interceptor.stop( axiosInstance );
}
moxios.uninstall( axiosInstance );
adapter.restore();
} );
it( 'should not break interceptor chaining for success', async () => {
moxios.stubRequest( 'http://test.test', { status: 200 } );
adapter.onGet( 'http://test.test' ).reply( 200 );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );
@ -37,7 +38,7 @@ describe( 'AxiosInterceptor', () => {
} );
it( 'should not break interceptor chaining for errors', async () => {
moxios.stubRequest( 'http://test.test', { status: 401 } );
adapter.onGet( 'http://test.test' ).reply( 401 );
interceptors.push( new TestInterceptor() );
interceptors.push( new TestInterceptor() );

View File

@ -1,14 +1,15 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import MockAdapter from 'axios-mock-adapter';
import { AxiosOAuthInterceptor } from '../axios-oauth-interceptor';
describe( 'AxiosOAuthInterceptor', () => {
let apiAuthInterceptor: AxiosOAuthInterceptor;
let axiosInstance: AxiosInstance;
let adapter: MockAdapter;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
adapter = new MockAdapter( axiosInstance );
apiAuthInterceptor = new AxiosOAuthInterceptor(
'consumer_key',
'consumer_secret',
@ -18,66 +19,40 @@ describe( 'AxiosOAuthInterceptor', () => {
afterEach( () => {
apiAuthInterceptor.stop( axiosInstance );
moxios.uninstall( axiosInstance );
} );
it( 'should not run unless started', async () => {
moxios.stubRequest( 'https://api.test', { status: 200 } );
apiAuthInterceptor.stop( axiosInstance );
await axiosInstance.get( 'https://api.test' );
let request = moxios.requests.mostRecent();
expect( request.headers ).not.toHaveProperty( 'Authorization' );
apiAuthInterceptor.start( axiosInstance );
await axiosInstance.get( 'https://api.test' );
request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
adapter.restore();
} );
it( 'should use basic auth for HTTPS', async () => {
moxios.stubRequest( 'https://api.test', { status: 200 } );
await axiosInstance.get( 'https://api.test' );
adapter.onGet( 'https://api.test' ).reply( 200 );
const response = await axiosInstance.get( 'https://api.test' );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toBe(
'Basic ' +
Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ),
);
expect( response.config.auth ).not.toBeNull();
expect( response.config.auth!.username ).toBe( 'consumer_key' );
expect( response.config.auth!.password ).toBe( 'consumer_secret' );
} );
it( 'should use OAuth 1.0a for HTTP', async () => {
moxios.stubRequest( 'http://api.test', { status: 200 } );
await axiosInstance.get( 'http://api.test' );
const request = moxios.requests.mostRecent();
adapter.onGet( 'http://api.test' ).reply( 200 );
const response = await axiosInstance.get( 'http://api.test' );
// We're going to assume that the oauth-1.0a package added the signature data correctly so we will
// focus on ensuring that the header looks roughly correct given what we readily know.
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toMatch(
expect( response.config.headers! ).toHaveProperty( 'Authorization' );
expect( response.config.headers!.Authorization ).toMatch(
/^OAuth oauth_consumer_key="consumer_key".*oauth_signature_method="HMAC-SHA256".*oauth_version="1.0"/,
);
} );
it( 'should work with base URL', async () => {
moxios.stubRequest( '/test', { status: 200 } );
await axiosInstance.request( {
adapter.onGet( 'https://api.test/test' ).reply( 200 );
const response = await axiosInstance.request( {
method: 'GET',
baseURL: 'https://api.test/',
url: '/test',
} );
const request = moxios.requests.mostRecent();
expect( request.headers ).toHaveProperty( 'Authorization' );
expect( request.headers.Authorization ).toBe(
'Basic ' +
Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ),
);
expect( response.config.auth ).not.toBeNull();
expect( response.config.auth!.username ).toBe( 'consumer_key' );
expect( response.config.auth!.password ).toBe( 'consumer_secret' );
} );
} );

View File

@ -1,31 +1,30 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import MockAdapter from 'axios-mock-adapter';
import { AxiosResponseInterceptor } from '../axios-response-interceptor';
describe( 'AxiosResponseInterceptor', () => {
let apiResponseInterceptor: AxiosResponseInterceptor;
let axiosInstance: AxiosInstance;
let adapter: MockAdapter;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
adapter = new MockAdapter( axiosInstance );
apiResponseInterceptor = new AxiosResponseInterceptor();
apiResponseInterceptor.start( axiosInstance );
} );
afterEach( () => {
apiResponseInterceptor.stop( axiosInstance );
moxios.uninstall();
adapter.restore();
} );
it( 'should transform responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
adapter.onGet( 'http://test.test' ).reply(
200,
{ test: 'value' },
{ 'content-type': 'application/json' }
);
const response = await axiosInstance.get( 'http://test.test' );
@ -41,13 +40,11 @@ describe( 'AxiosResponseInterceptor', () => {
} );
it( 'should transform error responses into an HTTPResponse', async () => {
moxios.stubRequest( 'http://test.test', {
status: 404,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { code: 'error_code', message: 'value' } ),
} );
adapter.onGet( 'http://test.test' ).reply(
404,
{ code: 'error_code', message: 'value' },
{ 'content-type': 'application/json' }
);
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject( {
statusCode: 404,
@ -62,7 +59,7 @@ describe( 'AxiosResponseInterceptor', () => {
} );
it( 'should bubble non-response errors', async () => {
moxios.stubTimeout( 'http://test.test' );
adapter.onGet( 'http://test.test' ).timeout();
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject(
new Error( 'timeout of 0ms exceeded' ),

View File

@ -1,31 +1,33 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import MockAdapter from 'axios-mock-adapter';
import { AxiosURLToQueryInterceptor } from '../axios-url-to-query-interceptor';
describe( 'AxiosURLToQueryInterceptor', () => {
let urlToQueryInterceptor: AxiosURLToQueryInterceptor;
let axiosInstance: AxiosInstance;
let adapter: MockAdapter;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
adapter = new MockAdapter( axiosInstance );
urlToQueryInterceptor = new AxiosURLToQueryInterceptor( 'test' );
urlToQueryInterceptor.start( axiosInstance );
} );
afterEach( () => {
urlToQueryInterceptor.stop( axiosInstance );
moxios.uninstall();
adapter.restore();
} );
it( 'should put path in query string', async () => {
moxios.stubRequest( 'http://test.test/?test=%2Ftest%2Froute', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
adapter.onGet(
'http://test.test/',
{ params: { test: '/test/route' } }
).reply(
200,
{ test: 'value' },
{ 'content-type': 'application/json' }
);
const response = await axiosInstance.get( 'http://test.test/test/route' );

View File

@ -50,7 +50,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
username: this.oauth.consumer.key,
password: this.oauth.consumer.secret,
};
} else {
} else if ( request.headers ) {
request.headers.Authorization = this.oauth.toHeader(
this.oauth.authorize( {
url,

View File

@ -34,7 +34,7 @@ abstract class AbstractProductInventory extends Model {
*
* @type {StockStatus}
*/
public readonly stockStatus: StockStatus = ''
public readonly stockStatus: StockStatus = '';
/**
* The status of backordering for a product.
@ -56,6 +56,13 @@ abstract class AbstractProductInventory extends Model {
* @type {boolean}
*/
public readonly isOnBackorder: boolean = false;
/**
* Indicates the threshold for when the low stock notification will be sent to the merchant.
*
* @type {number}
*/
public readonly lowStockThreshold: number = -1;
}
export interface IProductInventory extends AbstractProductInventory {}

View File

@ -53,7 +53,7 @@ export type ProductGroupedUpdateParams = 'groupedProducts';
* Properties related to tracking inventory.
*/
export type ProductInventoryUpdateParams = 'backorderStatus' | 'canBackorder' | 'trackInventory'
| 'onePerOrder' | 'remainingStock';
| 'onePerOrder' | 'remainingStock' | 'lowStockThreshold';
/**
* Properties related to sales tax.

View File

@ -130,10 +130,11 @@ export class SimpleProduct extends AbstractProduct implements
public readonly onePerOrder: boolean = false;
public readonly trackInventory: boolean = false;
public readonly remainingStock: number = -1;
public readonly stockStatus: StockStatus = ''
public readonly stockStatus: StockStatus = '';
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
public readonly lowStockThreshold: number = -1;
/**
* @see ./abstracts/price.ts

View File

@ -115,10 +115,11 @@ export class VariableProduct extends AbstractProduct implements
public readonly onePerOrder: boolean = false;
public readonly trackInventory: boolean = false;
public readonly remainingStock: number = -1;
public readonly stockStatus: StockStatus = ''
public readonly stockStatus: StockStatus = '';
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
public readonly lowStockThreshold: number = -1;
/**
* @see ./abstracts/sales-tax.ts

View File

@ -118,6 +118,7 @@ export class ProductVariation extends AbstractProductData implements
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
public readonly lowStockThreshold: number = -1;
/**
* @see ./abstracts/price.ts

View File

@ -1,4 +1,4 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { mocked } from 'ts-jest/utils';
import { HTTPClient, HTTPResponse } from '../../../http';
import { ModelTransformer, ModelRepositoryParams } from '../../../framework';
import { DummyModel } from '../../../__test_data__/dummy-model';
@ -26,17 +26,25 @@ class DummyChildModel extends Model {
}
type DummyChildParams = ModelRepositoryParams< DummyChildModel, { parent: string }, { childSearch: string }, 'childName' >
jest.mock( '../../../framework/model-transformer' );
describe( 'Shared REST Functions', () => {
let mockClient: MockProxy< HTTPClient >;
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
let mockClient: HTTPClient;
let mockTransformer: ModelTransformer< any >;
beforeEach( () => {
mockClient = mock< HTTPClient >();
mockTransformer = mock< ModelTransformer< any > >();
mockClient = {
get: jest.fn(),
post: jest.fn(),
patch: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
};
mockTransformer = new ModelTransformer( [] );
} );
it( 'restList', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
mocked( mockClient.get ).mockResolvedValue( new HTTPResponse(
200,
{},
[
@ -50,7 +58,7 @@ describe( 'Shared REST Functions', () => {
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restList< DummyModelParams >( () => 'test-url', DummyModel, mockClient, mockTransformer );
@ -65,7 +73,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restListChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
mocked( mockClient.get ).mockResolvedValue( new HTTPResponse(
200,
{},
[
@ -79,7 +87,7 @@ describe( 'Shared REST Functions', () => {
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restListChild< DummyChildParams >(
( parent ) => 'test-url-' + parent.parent,
@ -99,7 +107,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restCreate', async () => {
mockClient.post.mockResolvedValue( new HTTPResponse(
mocked( mockClient.post ).mockResolvedValue( new HTTPResponse(
200,
{},
{
@ -107,8 +115,8 @@ describe( 'Shared REST Functions', () => {
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
mocked( mockTransformer.fromModel ).mockReturnValue( { name: 'From-Test' } );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restCreate< DummyModelParams >(
( properties ) => 'test-url-' + properties.name,
@ -126,7 +134,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restRead', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
mocked( mockClient.get ).mockResolvedValue( new HTTPResponse(
200,
{},
{
@ -134,7 +142,7 @@ describe( 'Shared REST Functions', () => {
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restRead< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
@ -146,7 +154,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restReadChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
mocked( mockClient.get ).mockResolvedValue( new HTTPResponse(
200,
{},
{
@ -154,7 +162,7 @@ describe( 'Shared REST Functions', () => {
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restReadChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
@ -171,7 +179,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restUpdate', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
mocked( mockClient.patch ).mockResolvedValue( new HTTPResponse(
200,
{},
{
@ -179,8 +187,8 @@ describe( 'Shared REST Functions', () => {
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
mocked( mockTransformer.fromModel ).mockReturnValue( { name: 'From-Test' } );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restUpdate< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
@ -193,7 +201,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restUpdateChildren', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
mocked( mockClient.patch ).mockResolvedValue( new HTTPResponse(
200,
{},
{
@ -201,8 +209,8 @@ describe( 'Shared REST Functions', () => {
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
mocked( mockTransformer.fromModel ).mockReturnValue( { name: 'From-Test' } );
mocked( mockTransformer.toModel ).mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restUpdateChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
@ -220,7 +228,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restDelete', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
mocked( mockClient.delete ).mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDelete< DummyModelParams >( ( id ) => 'test-url-' + id, mockClient );
@ -231,7 +239,7 @@ describe( 'Shared REST Functions', () => {
} );
it( 'restDeleteChildren', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
mocked( mockClient.delete ).mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDeleteChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,

View File

@ -345,6 +345,7 @@ export function createProductInventoryTransformation(): ModelTransformation[] {
onePerOrder: PropertyType.Boolean,
stockStatus: PropertyType.String,
backOrderStatus: PropertyType.String,
lowStockThreshold: PropertyType.Integer,
},
),
new KeyChangeTransformation< IProductInventory >(
@ -356,6 +357,7 @@ export function createProductInventoryTransformation(): ModelTransformation[] {
backorderStatus: 'backorders',
canBackorder: 'backorders_allowed',
isOnBackorder: 'backordered',
lowStockThreshold: 'low_stock_amount',
},
),
];

View File

@ -1,13 +1,14 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { UpdatesSettings } from '../../models';
import { SettingService } from '../setting-service';
describe( 'SettingService', () => {
let repository: MockProxy< UpdatesSettings >;
let repository: UpdatesSettings;
let service: SettingService;
beforeEach( () => {
repository = mock< UpdatesSettings >();
repository = {
update: jest.fn(),
};
service = new SettingService( repository );
} );

View File

@ -1,8 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"types": [ "node", "jest", "axios", "moxios", "create-hmac" ],
"rootDir": "src",
"types": [ "node", "jest", "axios", "create-hmac" ],
"rootDir": "src",
"outDir": "dist",
"target": "es5"
},

1
packages/js/e2e-core-tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/test-data/sample_products.csv

View File

@ -1,8 +1,19 @@
# Unreleased
## Fixed
- Moved `merchant.login()` out of `beforeAll()` block and into test body for retried runs.
## Added
- This package is now transpiled with Babel, which allows the usage of modern, yet compatible JS code.
- A `specs/data` folder to store page element data.
- Tests to verify that different top-level menu and their associated sub-menus load successfully.
- Test scaffolding via `npx wc-e2e install @woocommerce/e2e-core-tests`
## Changed
- New coupon test deletes the coupon instead of trashing it.
- A copy of sample_data.csv is included in the package.
# 0.1.6

View File

@ -0,0 +1,7 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/<last-commit-hash-before-this-merge>/packages/js/e2e-core-tests/CHANGELOG.md).

View File

@ -20,6 +20,18 @@ Follow [E2E setup instructions](https://github.com/woocommerce/woocommerce/blob/
### Setting up core tests
#### Version 0.2.0 or newer
Version 0.2.0 added a test installer that will populate the `tests/e2e/specs` folder with test scripts for all the current core test suite. It also creates sample configuration files including all the configuration data needed to run the core tests.
- Install the e2e-environment `npm install @woocommerce/e2e-environment --save-dev`
- Run the installer `npx wc-e2e install @woocommerce/e2e-core-tests`
- Merge the sample configuration files:
- `tests/e2e/docker/woocommerce.e2e-core-tests.sh` => `initialize.sh`
- `tests/e2e/config/default-woocommerce.e2e-core-tests.json` => `default.json`
#### Version 0.1.X or other test runner
- Create the folder `tests/e2e/specs` in your repository if it does not exist.
- To add a core test to your test suite, create a new `.test.js` file within `tests/e2e/specs` . Example code to run all the shopper tests:
```js
@ -72,6 +84,7 @@ The functions to access the core tests are:
- `runAnalyticsPageLoadsTest` - Merchant can load and see all pages in Analytics
- `runImportProductsTest` - Merchant can import products via CSV file
- `runInitiateWccomConnectionTest` - Merchant can initiate connection to WooCommerce.com
- `runAdminPageLoadTests` - Merchant can load pages from the WP Admin sidebar
### Shopper
@ -103,7 +116,7 @@ The functions to access the core tests are:
## Contributing a new test
- In your branch create a new `example-test-name.test.js` under the `tests/e2e/core-tests/specs` folder.
- In your branch create a new `example-test-name.test.js` under the appropriate folder in the [`specs`](specs) directory.
- Jest does not allow its global functions to be accessed outside the jest environment. To allow the test code to be published in a package import any jest global functions used in your test
```js
const {
@ -129,7 +142,7 @@ const runExampleTestName = () => {
module.exports = runExampleTestName;
```
- Add your test to `tests/e2e/core-tests/specs/index.js`
- Add your test to [`specs/index.js`](specs/index.js)
```js
const runExampleTestName = require( './grouping/example-test-name.test' );
// ...

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
#
# Copy the WooCommerce sample data file to the package
#
PACKAGEPATH=$(dirname $(dirname "$0"))
cp -v $PACKAGEPATH/../../../plugins/woocommerce/sample-data/sample_products.csv $PACKAGEPATH/test-data

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/e2e-core-tests",
"description": "WooCommerce end to end core tests",
"type": "library",
"license": "GPL-3.0-or-later",
"minimum-stability": "dev",
"require-dev": {
"automattic/jetpack-changelogger": "3.0.2"
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/PackageFormatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "NEXT_CHANGELOG.md"
}
}
}

1021
packages/js/e2e-core-tests/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ const getCoreTestsRoot = () => {
return {
appRoot: coreTestsPath,
packageRoot: moduleDir,
coreTestsRoot: __dirname
};
};

View File

@ -0,0 +1,195 @@
{
"url": "http://localhost:8084/",
"users": {
"admin": {
"username": "admin",
"password": "password"
},
"customer": {
"username": "customer",
"password": "password"
}
},
"products": {
"simple": {
"name": "Simple product"
},
"variable": {
"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
},
{
"id": 0,
"name": "Logo",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Woo",
"WordPress"
],
"sortOrder": 0
}
]
},
"variations": [
{
"regularPrice": "19.99",
"attributes": [
{
"name": "Size",
"option": "Large"
},
{
"name": "Colour",
"option": "Red"
}
]
},
{
"regularPrice": "18.99",
"attributes": [
{
"name": "Size",
"option": "Medium"
},
{
"name": "Colour",
"option": "Green"
}
]
},
{
"regularPrice": "17.99",
"attributes": [
{
"name": "Size",
"option": "Small"
},
{
"name": "Colour",
"option": "Blue"
}
]
}
],
"grouped": {
"name": "Grouped Product with Three Children",
"groupedProducts": [
{
"name": "Base Unit",
"regularPrice": "29.99"
},
{
"name": "Add-on A",
"regularPrice": "11.95"
},
{
"name": "Add-on B",
"regularPrice": "18.97"
}
]
},
"external": {
"name": "External product",
"regularPrice": "24.99",
"buttonText": "Buy now",
"externalUrl": "https://wordpress.org/plugins/woocommerce"
}
},
"coupons": {
"percentage": {
"code": "20percent",
"discountType": "percent",
"amount": "20.00"
}
},
"addresses": {
"admin": {
"store": {
"email": "admin@woocommercecoree2etestsuite.com",
"firstname": "John",
"lastname": "Doe",
"company": "Automattic",
"country": "United States (US)",
"addressfirstline": "addr 1",
"addresssecondline": "addr 2",
"countryandstate": "United States (US) — California",
"city": "San Francisco",
"state": "CA",
"postcode": "94107"
}
},
"customer": {
"billing": {
"firstname": "John",
"lastname": "Doe",
"company": "Automattic",
"country": "United States (US)",
"addressfirstline": "addr 1",
"addresssecondline": "addr 2",
"city": "San Francisco",
"state": "CA",
"postcode": "94107",
"phone": "123456789",
"email": "john.doe@example.com"
},
"shipping": {
"firstname": "John",
"lastname": "Doe",
"company": "Automattic",
"country": "United States (US)",
"addressfirstline": "addr 1",
"addresssecondline": "addr 2",
"city": "San Francisco",
"state": "CA",
"postcode": "94107"
}
}
},
"orders": {
"basicPaidOrder": {
"paymentMethod": "cod",
"status": "processing",
"billing": {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}
}
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
testSpecs: 'installFiles/scaffold-tests.json',
defaultJson: 'installFiles/default-test-config.json',
initializeSh: 'installFiles/initialize.sh.default',
};

Some files were not shown because too many files have changed in this diff Show More