Merge branch 'trunk' into add/api-t-order-search

This commit is contained in:
Rodel 2021-11-19 22:38:12 +08:00
commit ca5124e1d1
155 changed files with 15278 additions and 4748 deletions

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

@ -28,8 +28,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.

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.

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.
@ -82,7 +82,7 @@ jobs:
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 +103,4 @@ 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

14
.gitignore vendored
View File

@ -5,10 +5,19 @@ Thumbs.db
# IDE files
.idea
.vscode/
project.xml
project.properties
.project
.settings*
*.sublime-project
*.sublime-workspace
.sublimelinterrc
# Eslint Cache
.eslintcache
# Environment files
wp-cli.local.yml
.wp-env.override.json
yarn-error.log
npm-debug.log
.pnpm-debug.log
@ -22,6 +31,7 @@ npm-debug.log
build/
build-module/
build-style/
dist/
# Project files
node_modules/
@ -37,5 +47,3 @@ tsconfig.tsbuildinfo
/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/

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,45 @@
{
"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": "latest",
"@nrwl/linter": "^13.1.4",
"@nrwl/tao": "latest",
"@nrwl/web": "^13.1.4",
"@nrwl/workspace": "latest",
"@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

@ -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",
"Add",
"Update",
"Dev",
"Tweak",
"Performance",
"Enhancement"
],
"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

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

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.',

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,7 +29,8 @@ 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',

View File

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

View File

@ -0,0 +1,31 @@
{
"root": "packages/js/api-core-tests/",
"sourceRoot": "packages/js/api-core-tests",
"projectType": "library",
"targets": {
"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

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

@ -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 +1,2 @@
# 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

@ -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",
"Add",
"Update",
"Dev",
"Tweak",
"Performance",
"Enhancement"
],
"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

@ -34,20 +34,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,43 @@
{
"root": "packages/js/api/",
"sourceRoot": "packages/js/api/src",
"projectType": "library",
"targets": {
"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

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

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

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

@ -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",
"Add",
"Update",
"Dev",
"Tweak",
"Performance",
"Enhancement"
],
"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

@ -0,0 +1,5 @@
{
"root": "packages/js/e2e-core-tests/",
"sourceRoot": "packages/js/e2e-core-tests",
"projectType": "library"
}

View File

@ -7,23 +7,19 @@ const { HTTPClientFactory, Coupon } = require( '@woocommerce/api' );
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const { it, describe, beforeAll } = require( '@jest/globals' );
/**
* Create the default coupon and tests interactions with it via the API.
*/
const runCouponApiTest = () => {
describe('REST API > Coupon', () => {
describe( 'REST API > Coupon', () => {
let client;
let percentageCoupon;
let coupon;
let repository;
beforeAll(async () => {
beforeAll( async () => {
percentageCoupon = config.get( 'coupons.percentage' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
@ -34,15 +30,17 @@ const runCouponApiTest = () => {
.create();
} );
it('can create a coupon', async () => {
it( 'can create a coupon', async () => {
repository = Coupon.restRepository( client );
// Check properties of the coupon in the create coupon response.
coupon = await repository.create( percentageCoupon );
expect( coupon ).toEqual( expect.objectContaining( percentageCoupon ) );
});
expect( coupon ).toEqual(
expect.objectContaining( percentageCoupon )
);
} );
it('can retrieve a coupon', async () => {
it( 'can retrieve a coupon', async () => {
const couponProperties = {
id: coupon.id,
code: percentageCoupon.code,
@ -51,12 +49,16 @@ const runCouponApiTest = () => {
};
// Read coupon directly from API to compare.
const response = await client.get( `/wc/v3/coupons/${coupon.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( couponProperties ) );
});
const response = await client.get(
`/wc/v3/coupons/${ coupon.id }`
);
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( couponProperties )
);
} );
it('can update a coupon', async () => {
it( 'can update a coupon', async () => {
const updatedCouponProperties = {
amount: '75.00',
discount_type: 'fixed_cart',
@ -66,19 +68,23 @@ const runCouponApiTest = () => {
await repository.update( coupon.id, updatedCouponProperties );
// Check the coupon response for the updated values.
const response = await client.get( `/wc/v3/coupons/${coupon.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( updatedCouponProperties ) );
});
const response = await client.get(
`/wc/v3/coupons/${ coupon.id }`
);
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( updatedCouponProperties )
);
} );
it('can delete a coupon', async () => {
it( 'can delete a coupon', async () => {
// Delete the coupon
const status = await repository.delete( coupon.id );
// If the delete is successful, the response comes back truthy
expect( status ).toBeTruthy();
});
});
} );
} );
};
module.exports = runCouponApiTest;

View File

@ -7,24 +7,20 @@ const { HTTPClientFactory, ExternalProduct } = require( '@woocommerce/api' );
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const { it, describe, beforeAll } = require( '@jest/globals' );
/**
* Create an external product and retrieve via the API.
*/
const runExternalProductAPITest = () => {
// @todo: add a call to ensure pretty permalinks are enabled once settings api is in use.
describe('REST API > External Product', () => {
describe( 'REST API > External Product', () => {
let client;
let defaultExternalProduct;
let product;
let repository;
beforeAll(async () => {
beforeAll( async () => {
defaultExternalProduct = config.get( 'products.external' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
@ -35,15 +31,17 @@ const runExternalProductAPITest = () => {
.create();
} );
it('can create an external product', async () => {
it( 'can create an external product', async () => {
repository = ExternalProduct.restRepository( client );
// Check properties of product in the create product response.
product = await repository.create( defaultExternalProduct );
expect( product ).toEqual( expect.objectContaining( defaultExternalProduct ) );
});
expect( product ).toEqual(
expect.objectContaining( defaultExternalProduct )
);
} );
it('can retrieve a raw external product', async () => {
it( 'can retrieve a raw external product', async () => {
const rawProperties = {
id: product.id,
button_text: defaultExternalProduct.buttonText,
@ -52,12 +50,16 @@ const runExternalProductAPITest = () => {
};
// Read product directly from api.
const response = await client.get( `/wc/v3/products/${product.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( rawProperties ) );
});
const response = await client.get(
`/wc/v3/products/${ product.id }`
);
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( rawProperties )
);
} );
it('can retrieve a transformed external product', async () => {
it( 'can retrieve a transformed external product', async () => {
const transformedProperties = {
...defaultExternalProduct,
id: product.id,
@ -66,14 +68,16 @@ const runExternalProductAPITest = () => {
// Read product via the repository.
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( transformedProperties ) );
});
expect( transformed ).toEqual(
expect.objectContaining( transformedProperties )
);
} );
it('can delete an external product', async () => {
it( 'can delete an external product', async () => {
const status = repository.delete( product.id );
expect( status ).toBeTruthy();
});
});
} );
} );
};
module.exports = runExternalProductAPITest;

View File

@ -1,32 +1,32 @@
/**
* Internal dependencies
*/
const { HTTPClientFactory, GroupedProduct, SimpleProduct } = require( '@woocommerce/api' );
const {
HTTPClientFactory,
GroupedProduct,
SimpleProduct,
} = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const { it, describe, beforeAll } = require( '@jest/globals' );
/**
* Create an external product and retrieve via the API.
*/
const runGroupedProductAPITest = () => {
// @todo: add a call to ensure pretty permalinks are enabled once settings api is in use.
describe('REST API > Grouped Product', () => {
describe( 'REST API > Grouped Product', () => {
let client;
let defaultGroupedProduct;
let baseGroupedProduct;
let product;
let groupedProducts = [];
const groupedProducts = [];
let repository;
beforeAll(async () => {
beforeAll( async () => {
defaultGroupedProduct = config.get( 'products.grouped' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
@ -38,13 +38,19 @@ const runGroupedProductAPITest = () => {
// Create the simple products to be grouped first.
repository = SimpleProduct.restRepository( client );
for ( let c = 0; c < defaultGroupedProduct.groupedProducts.length; c++ ) {
product = await repository.create( defaultGroupedProduct.groupedProducts[ c ] );
for (
let c = 0;
c < defaultGroupedProduct.groupedProducts.length;
c++
) {
product = await repository.create(
defaultGroupedProduct.groupedProducts[ c ]
);
groupedProducts.push( product.id );
}
});
} );
it('can create a grouped product', async () => {
it( 'can create a grouped product', async () => {
baseGroupedProduct = {
...defaultGroupedProduct,
groupedProducts,
@ -53,38 +59,46 @@ const runGroupedProductAPITest = () => {
// Check properties of product in the create product response.
product = await repository.create( baseGroupedProduct );
expect( product ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
expect( product ).toEqual(
expect.objectContaining( baseGroupedProduct )
);
} );
it('can retrieve a raw grouped product', async () => {
let rawProperties = {
it( 'can retrieve a raw grouped product', async () => {
const rawProperties = {
id: product.id,
grouped_products: baseGroupedProduct.groupedProducts,
...defaultGroupedProduct,
};
delete rawProperties['groupedProducts'];
delete rawProperties.groupedProducts;
// Read product directly from api.
const response = await client.get( `/wc/v3/products/${product.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( rawProperties ) );
});
const response = await client.get(
`/wc/v3/products/${ product.id }`
);
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( rawProperties )
);
} );
it('can retrieve a transformed grouped product', async () => {
it( 'can retrieve a transformed grouped product', async () => {
// Read product via the repository.
const transformed = await repository.read( product.id );
expect( transformed ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
expect( transformed ).toEqual(
expect.objectContaining( baseGroupedProduct )
);
} );
it('can delete a grouped product', async () => {
it( 'can delete a grouped product', async () => {
const status = repository.delete( product.id );
expect( status ).toBeTruthy();
// Delete the simple "child" products.
groupedProducts.forEach( ( productId ) => {
repository.delete( productId );
});
});
});
} );
} );
} );
};
module.exports = runGroupedProductAPITest;

View File

@ -1,28 +1,24 @@
/**
* Internal dependencies
*/
const { HTTPClientFactory, Order } = require( '@woocommerce/api' );
const { HTTPClientFactory, Order } = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* External dependencies
*/
const config = require( 'config' );
const { it, describe, beforeAll } = require( '@jest/globals' );
/**
/**
* Creates an order and tests interactions with it via the API.
*/
const runOrderApiTest = () => {
describe('REST API > Order', () => {
describe( 'REST API > Order', () => {
let client;
let order;
let repository;
beforeAll(async () => {
beforeAll( async () => {
order = config.get( 'orders.basicPaidOrder' );
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
@ -33,15 +29,15 @@ const runOrderApiTest = () => {
.create();
} );
it('can create an order', async () => {
it( 'can create an order', async () => {
repository = Order.restRepository( client );
// Check properties of the order in the create order response.
order = await repository.create( order );
expect( order ).toEqual( expect.objectContaining( order ) );
});
} );
it('can retrieve an order', async () => {
it( 'can retrieve an order', async () => {
const orderProperties = {
id: order.id,
payment_method: order.paymentMethod,
@ -49,12 +45,14 @@ const runOrderApiTest = () => {
};
// Read order directly from API to compare.
const response = await client.get( `/wc/v3/orders/${order.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( orderProperties ) );
});
const response = await client.get( `/wc/v3/orders/${ order.id }` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( orderProperties )
);
} );
it('can update an order', async () => {
it( 'can update an order', async () => {
const updatedOrderProperties = {
payment_method: 'bacs',
status: 'completed',
@ -63,19 +61,21 @@ const runOrderApiTest = () => {
await repository.update( order.id, updatedOrderProperties );
// Check the order response for the updated values.
const response = await client.get( `/wc/v3/orders/${order.id}` );
expect( response.status ).toBe( 200 );
expect( response.data ).toEqual( expect.objectContaining( updatedOrderProperties ) );
});
const response = await client.get( `/wc/v3/orders/${ order.id }` );
expect( response.statusCode ).toBe( 200 );
expect( response.data ).toEqual(
expect.objectContaining( updatedOrderProperties )
);
} );
it('can delete an order', async () => {
it( 'can delete an order', async () => {
// Delete the order
const status = await repository.delete( order.id );
// If the delete is successful, the response comes back truthy
expect( status ).toBeTruthy();
});
});
} );
} );
};
module.exports = runOrderApiTest;

View File

@ -7,11 +7,7 @@ const { HTTPClientFactory } = require( '@woocommerce/api' );
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const { it, describe, beforeAll } = require( '@jest/globals' );
/**
* Create the default coupon and tests interactions with it via the API.
@ -20,7 +16,7 @@ const runTelemetryAPITest = () => {
describe( 'REST API > Telemetry', () => {
let client;
beforeAll(async () => {
beforeAll( async () => {
const admin = config.get( 'users.admin' );
const url = config.get( 'url' );
@ -30,29 +26,26 @@ const runTelemetryAPITest = () => {
.create();
} );
it.each([
null,
{},
{ platform: 'ios' },
{ version: '1.1' },
])( 'errors for invalid request body - %p', async data => {
const response = await client
.post( `/wc-telemetry/tracker`, data )
.catch( err => {
expect( err.response.status ).toBe( 400 );
} );
it.each( [ null, {}, { platform: 'ios' }, { version: '1.1' } ] )(
'errors for invalid request body - %p',
async ( data ) => {
const response = await client
.post( `/wc-telemetry/tracker`, data )
.catch( ( err ) => {
expect( err.statusCode ).toBe( 400 );
} );
expect( response ).toBeUndefined();
} );
expect( response ).toBeUndefined();
}
);
it( 'returns 200 with correct fields', async () => {
const response = await client
.post( `/wc-telemetry/tracker`, {
platform: 'ios',
version: '1.0',
})
const response = await client.post( `/wc-telemetry/tracker`, {
platform: 'ios',
version: '1.0',
} );
expect( response.status ).toBe( 200 );
expect( response.statusCode ).toBe( 200 );
} );
} );
};

View File

@ -1,83 +0,0 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const {
merchant,
completeOnboardingWizard,
withRestApi,
addShippingZoneAndMethod,
IS_RETEST_MODE,
} = require( '@woocommerce/e2e-utils' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
} = require( '@jest/globals' );
const shippingZoneNameUS = config.get( 'addresses.customer.shipping.country' );
const runOnboardingFlowTest = () => {
describe('Store owner can go through store Onboarding', () => {
if ( IS_RETEST_MODE ) {
it('can reset onboarding to default settings', async () => {
await withRestApi.resetOnboarding();
});
it('can reset shipping zones to default settings', async () => {
await withRestApi.deleteAllShippingZones();
});
it('can reset to default settings', async () => {
await withRestApi.resetSettingsGroupToDefault('general');
await withRestApi.resetSettingsGroupToDefault('products');
await withRestApi.resetSettingsGroupToDefault('tax');
});
}
it('can start and complete onboarding when visiting the site for the first time.', async () => {
await completeOnboardingWizard();
});
});
};
const runTaskListTest = () => {
describe('Store owner can go through setup Task List', () => {
it('can setup shipping', async () => {
await page.evaluate(() => {
document.querySelector('.woocommerce-list__item-title').scrollIntoView();
});
// Query for all tasks on the list
const taskListItems = await page.$$('.woocommerce-list__item-title');
expect(taskListItems.length).toBeInRange( 5, 6 );
// Work around for https://github.com/woocommerce/woocommerce-admin/issues/6761
if ( taskListItems.length == 6 ) {
// Click on "Set up shipping" task to move to the next step
const [ setupTaskListItem ] = await page.$x( '//div[contains(text(),"Set up shipping")]' );
await setupTaskListItem.click();
// Wait for "Proceed" button to become active
await page.waitForSelector('button.is-primary:not(:disabled)');
await page.waitFor(3000);
// Click on "Proceed" button to save shipping settings
await page.click('button.is-primary');
await page.waitFor(3000);
} else {
await merchant.openNewShipping();
await addShippingZoneAndMethod(shippingZoneNameUS);
}
});
});
};
module.exports = {
runOnboardingFlowTest,
runTaskListTest,
};

View File

@ -5,9 +5,8 @@ const {
merchant,
clickTab,
AdminEdit,
factories,
withRestApi,
} = require( '@woocommerce/e2e-utils' );
const { Coupon } = require( '@woocommerce/api' );
/**
* External dependencies
@ -50,8 +49,7 @@ const runCreateCouponTest = () => {
// Delete the coupon
const couponId = await adminEdit.getId();
if ( couponId ) {
const repository = Coupon.restRepository( factories.api.withDefaultPermalinks );
await repository.delete( couponId );
await withRestApi.deleteCoupon( couponId );
}
});
});

View File

@ -21,7 +21,13 @@ const runInitiateWccomConnectionTest = () => {
});
it.skip('can initiate WCCOM connection', async () => {
await merchant.openHelper();
await merchant.openExtensions();
// Click on a tab to choose WooCommerce Subscriptions extension
await Promise.all( [
expect( page ).toClick( 'a.nav-tab', { text: "WooCommerce.com Subscriptions" } ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Click on Connect button to initiate a WCCOM connection
await Promise.all([

View File

@ -4,9 +4,10 @@
const {
merchant,
uiUnblocked,
withRestApi,
AdminEdit,
} = require('@woocommerce/e2e-utils');
const config = require('config');
} = require( '@woocommerce/e2e-utils' );
const config = require( 'config' );
const {
HTTPClientFactory,
VariableProduct,
@ -14,22 +15,34 @@ const {
SimpleProduct,
ProductVariation,
ExternalProduct
} = require('@woocommerce/api');
} = require( '@woocommerce/api' );
const taxClasses = [
{
name: 'Tax Class Simple',
},
{
name: 'Tax Class Variable',
},
{
name: 'Tax Class External',
},
];
const taxRates = [
{
name: 'Tax Rate Simple',
rate: '10',
rate: '10.0000',
class: 'tax-class-simple'
},
{
name: 'Tax Rate Variable',
rate: '20',
rate: '20.0000',
class: 'tax-class-variable'
},
{
name: 'Tax Rate External',
rate: '30',
rate: '30.0000',
class: 'tax-class-external'
}
];
@ -43,60 +56,22 @@ const initProducts = async () => {
const httpClient = HTTPClientFactory.build(apiUrl)
.withBasicAuth(adminUsername, adminPassword)
.create();
const taxClassesPath = '/wc/v3/taxes/classes';
const taxClasses = [
{
name: 'Tax Class Simple',
slug: 'tax-class-simple-698962'
},
{
name: 'Tax Class Variable',
slug: 'tax-class-variable-790238'
},
{
name: 'Tax Class External',
slug: 'tax-class-external-991321'
}
];
// Enable taxes in settings
const enableTaxes = async () => {
const path = '/wc/v3/settings/general/woocommerce_calc_taxes';
const data = {
value: 'yes'
};
await httpClient.put(path, data);
};
await enableTaxes();
// Initialize tax classes
const initTaxClasses = async () => {
for (const classToBeAdded of taxClasses) {
await httpClient.post(taxClassesPath, classToBeAdded);
}
};
await initTaxClasses();
// Initialize tax rates
const initTaxRates = async () => {
const path = '/wc/v3/taxes';
for (const rateToBeAdded of taxRates) {
await httpClient.post(path, rateToBeAdded);
}
};
await initTaxRates();
await withRestApi.updateSettingOption( 'general', 'woocommerce_calc_taxes', { value: 'yes' } );
await withRestApi.addTaxClasses( taxClasses );
await withRestApi.addTaxRates( taxRates );
// Initialization functions per product type
const initSimpleProduct = async () => {
const repo = SimpleProduct.restRepository(httpClient);
const repo = SimpleProduct.restRepository( httpClient );
const simpleProduct = {
name: 'Simple Product 273722',
regularPrice: '100',
taxClass: 'Tax Class Simple'
};
return await repo.create(simpleProduct);
return await repo.create( simpleProduct );
};
const initVariableProduct = async () => {
const variations = [
{
@ -264,12 +239,12 @@ const runCreateOrderTest = () => {
}
// Verify that the names of each tax class were shown
for (const { name } of taxRates) {
for (const taxRate of taxRates) {
await expect(page).toMatchElement('th.line_tax', {
text: name
text: taxRate.name
});
await expect(page).toMatchElement('.wc-order-totals td.label', {
text: name
text: taxRate.name
});
}

View File

@ -9,32 +9,12 @@ const {
uiUnblocked,
evalAndClick,
createOrder,
clickAndWaitForSelector,
} = require( '@woocommerce/e2e-utils' );
const { waitForSelector } = require( '@woocommerce/e2e-environment' );
const config = require( 'config' );
const simpleProductPrice = config.has('products.simple.price') ? config.get('products.simple.price') : '9.99';
/**
* Evaluate and click a button selector then wait for a result selector.
* This is a work around for what appears to be intermittent delays in handling confirm dialogs.
*
* @param buttonSelector
* @param resultSelector
* @returns {Promise<void>}
*/
const clickAndWaitForSelector = async ( buttonSelector, resultSelector ) => {
await evalAndClick( buttonSelector );
await waitForSelector(
page,
resultSelector,
{
timeout: 5000
}
);
};
const runRefundOrderTest = () => {
describe('WooCommerce Orders > Refund an order', () => {
let productId;

View File

@ -51,7 +51,7 @@ const updateCustomerBilling = async () => {
search: 'Jane',
role: 'all',
} );
if ( ! customers.data | ! customers.data.length ) {
if ( ! customers.data || ! customers.data.length ) {
return;
}

View File

@ -45,7 +45,7 @@ const runOrderStatusFiltersTest = () => {
});
// Create the orders using the API
await withRestApi.batchCreateOrders(orders);
await withRestApi.batchCreateOrders( orders, false );
// Next, let's login and navigate to the Orders page
await merchant.login();

View File

@ -107,9 +107,9 @@ const runImportProductsTest = () => {
afterAll(async () => {
// Delete imported products
await withRestApi.deleteAllProducts();
await withRestApi.deleteAllProductAttributes();
await withRestApi.deleteAllProductCategories();
await withRestApi.deleteAllProductTags();
await withRestApi.deleteAllProductAttributes( false );
await withRestApi.deleteAllProductCategories( false );
await withRestApi.deleteAllProductTags( false );
});
it( 'should show error message if you go without providing CSV file', async () => {

View File

@ -14,7 +14,7 @@ const runAddShippingClassesTest = () => {
});
afterAll(async () => {
await withRestApi.deleteAllShippingClasses();
await withRestApi.deleteAllShippingClasses( false );
});
it('can add shipping classes', async () => {

View File

@ -35,7 +35,7 @@ const runAddNewShippingZoneTest = () => {
beforeAll(async () => {
productId = await createSimpleProduct();
await withRestApi.deleteAllShippingZones();
await withRestApi.deleteAllShippingZones( false );
await merchant.login();
});

View File

@ -42,19 +42,19 @@ const runCartCalculateShippingTest = () => {
firstProductId = await createSimpleProduct(firstProductName);
secondProductId = await createSimpleProduct(secondProductName, secondProductPrice);
await withRestApi.resetSettingsGroupToDefault( 'general' );
await withRestApi.resetSettingsGroupToDefault( 'general', false );
// Add a new shipping zone Germany with Free shipping
await withRestApi.addShippingZoneAndMethod(shippingZoneNameDE, shippingCountryDE, ' ', 'free_shipping');
await withRestApi.addShippingZoneAndMethod(shippingZoneNameDE, shippingCountryDE, ' ', 'free_shipping', '', [], false );
// Add a new shipping zone for France with Flat rate & Local pickup
await withRestApi.addShippingZoneAndMethod(shippingZoneNameFR, shippingCountryFR, ' ', 'flat_rate', '5', ['local_pickup']);
await withRestApi.addShippingZoneAndMethod(shippingZoneNameFR, shippingCountryFR, ' ', 'flat_rate', '5', ['local_pickup'], false );
await shopper.emptyCart();
});
afterAll(async () => {
await withRestApi.deleteAllShippingZones();
await withRestApi.deleteAllShippingZones( false );
});
it('allows customer to calculate Free Shipping if in Germany', async () => {

View File

@ -9,6 +9,7 @@ const {
applyCoupon,
removeCoupon,
} = require( '@woocommerce/e2e-utils' );
const { getCouponId, getCouponsTable } = require( '../utils/coupons' );
/**
* External dependencies
@ -19,36 +20,12 @@ const {
beforeAll,
} = require( '@jest/globals' );
const couponsTable = [
['fixed cart', { text: '$5.00' }, { text: '$4.99' }],
['percentage', { text: '$4.99' }, { text: '$5.00' }],
['fixed product', { text: '$5.00' }, { text: '$4.99' }]
];
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
const getCoupon = (couponType) => {
switch (couponType) {
case 'fixed cart':
return couponFixedCart;
case 'percentage':
return couponPercentage;
case 'fixed product':
return couponFixedProduct;
}
};
const runCartApplyCouponsTest = () => {
describe('Cart applying coupons', () => {
let productId;
beforeAll(async () => {
productId = await createSimpleProduct();
couponFixedCart = await createCoupon();
couponPercentage = await createCoupon('50', 'Percentage discount');
couponFixedProduct = await createCoupon('5', 'Fixed product discount');
await shopper.emptyCart();
await shopper.goToShop();
await shopper.addToCartFromShopPage( productId );
@ -56,8 +33,8 @@ const runCartApplyCouponsTest = () => {
await shopper.goToCart();
});
it.each(couponsTable)('allows cart to apply %s coupon', async (couponType, cartDiscount, orderTotal) => {
const coupon = getCoupon(couponType);
it.each( getCouponsTable() )( 'allows cart to apply %s coupon', async ( couponType, cartDiscount, orderTotal ) => {
const coupon = await getCouponId( couponType );
await applyCoupon(coupon);
await expect(page).toMatchElement('.woocommerce-message', { text: 'Coupon code applied successfully.' });
@ -69,9 +46,10 @@ const runCartApplyCouponsTest = () => {
});
it('prevents cart applying same coupon twice', async () => {
await applyCoupon(couponFixedCart);
const couponId = await getCouponId( 'fixed cart' );
await applyCoupon( couponId );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await applyCoupon(couponFixedCart);
await applyCoupon( couponId );
// Verify only one discount applied
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
@ -79,7 +57,7 @@ const runCartApplyCouponsTest = () => {
});
it('allows cart to apply multiple coupons', async () => {
await applyCoupon(couponFixedProduct);
await applyCoupon( await getCouponId( 'fixed product' ) );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
@ -88,8 +66,8 @@ const runCartApplyCouponsTest = () => {
});
it('restores cart total when coupons are removed', async () => {
await removeCoupon(couponFixedCart);
await removeCoupon(couponFixedProduct);
await removeCoupon( await getCouponId( 'fixed cart' ) );
await removeCoupon( await getCouponId( 'fixed product' ) );
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
});
});

View File

@ -28,9 +28,9 @@ const runCartPageTest = () => {
beforeAll(async () => {
productId = await createSimpleProduct();
await withRestApi.resetSettingsGroupToDefault('general');
await withRestApi.resetSettingsGroupToDefault('products');
await withRestApi.resetSettingsGroupToDefault('tax');
await withRestApi.resetSettingsGroupToDefault( 'general', false );
await withRestApi.resetSettingsGroupToDefault( 'products', false );
await withRestApi.resetSettingsGroupToDefault( 'tax', false );
});
it('should display no item in the cart', async () => {

View File

@ -10,6 +10,7 @@ const {
removeCoupon,
waitForSelectorWithoutThrow,
} = require( '@woocommerce/e2e-utils' );
const { getCouponId, getCouponsTable } = require( '../utils/coupons' );
/**
* External dependencies
@ -20,26 +21,6 @@ const {
beforeAll,
} = require( '@jest/globals' );
const couponsTable = [
['fixed cart', { text: '$5.00' }, { text: '$4.99' }],
['percentage', { text: '$4.99' }, { text: '$5.00' }],
['fixed product', { text: '$5.00' }, { text: '$4.99' }]
];
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
const getCoupon = (couponType) => {
switch (couponType) {
case 'fixed cart':
return couponFixedCart;
case 'percentage':
return couponPercentage;
case 'fixed product':
return couponFixedProduct;
}
};
const runCheckoutApplyCouponsTest = () => {
describe('Checkout coupons', () => {
@ -47,9 +28,6 @@ const runCheckoutApplyCouponsTest = () => {
beforeAll(async () => {
productId = await createSimpleProduct();
couponFixedCart = await createCoupon();
couponPercentage = await createCoupon('50', 'Percentage discount');
couponFixedProduct = await createCoupon('5', 'Fixed product discount');
await shopper.emptyCart();
await shopper.goToShop();
await waitForSelectorWithoutThrow( '.add_to_cart_button' );
@ -58,8 +36,8 @@ const runCheckoutApplyCouponsTest = () => {
await shopper.goToCheckout();
});
it.each(couponsTable)('allows checkout to apply %s coupon', async (couponType, cartDiscount, orderTotal) => {
const coupon = getCoupon(couponType);
it.each( getCouponsTable() )( 'allows checkout to apply %s coupon', async ( couponType, cartDiscount, orderTotal ) => {
const coupon = await getCouponId( couponType );
await applyCoupon(coupon);
await expect(page).toMatchElement('.woocommerce-message', { text: 'Coupon code applied successfully.' });
@ -73,9 +51,10 @@ const runCheckoutApplyCouponsTest = () => {
});
it('prevents checkout applying same coupon twice', async () => {
await applyCoupon(couponFixedCart);
const couponId = await getCouponId( 'fixed cart' );
await applyCoupon( couponId );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
await applyCoupon(couponFixedCart);
await applyCoupon( couponId );
// Verify only one discount applied
// This is a work around for Puppeteer inconsistently finding 'Coupon code already applied'
await expect(page).toMatchElement('.cart-discount .amount', {text: '$5.00'});
@ -83,7 +62,7 @@ const runCheckoutApplyCouponsTest = () => {
});
it('allows checkout to apply multiple coupons', async () => {
await applyCoupon(couponFixedProduct);
await applyCoupon( await getCouponId( 'fixed product' ) );
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon code applied successfully.'});
// Verify discount applied and order total
@ -92,8 +71,8 @@ const runCheckoutApplyCouponsTest = () => {
});
it('restores checkout total when coupons are removed', async () => {
await removeCoupon(couponFixedCart);
await removeCoupon(couponFixedProduct);
await removeCoupon( await getCouponId( 'fixed cart' ) );
await removeCoupon( await getCouponId( 'fixed product' ) );
await expect(page).toMatchElement('.order-total .amount', {text: '$9.99'});
});
});

View File

@ -39,7 +39,7 @@ const runCheckoutCreateAccountTest = () => {
await settingsPageSaveChanges();
// Set free shipping within California
await addShippingZoneAndMethod('Free Shipping CA', 'state:US:CA', ' ', 'free_shipping');
await addShippingZoneAndMethod('Free Shipping CA', 'state:US:CA', ' ', 'free_shipping' );
await merchant.logout();

View File

@ -26,12 +26,12 @@ const runCheckoutPageTest = () => {
describe('Checkout page', () => {
beforeAll(async () => {
productId = await createSimpleProduct();
await withRestApi.resetSettingsGroupToDefault('general');
await withRestApi.resetSettingsGroupToDefault('products');
await withRestApi.resetSettingsGroupToDefault('tax');
await withRestApi.resetSettingsGroupToDefault( 'general', false );
await withRestApi.resetSettingsGroupToDefault( 'products', false );
await withRestApi.resetSettingsGroupToDefault( 'tax', false );
// Set free shipping within California
await withRestApi.addShippingZoneAndMethod('Free Shipping CA', 'state:US:CA', '', 'free_shipping');
await withRestApi.addShippingZoneAndMethod('Free Shipping CA', 'state:US:CA', '', 'free_shipping', '', [], false );
// Set base location with state CA.
await withRestApi.updateSettingOption( 'general', 'woocommerce_default_country', { value: 'US:CA' } );
@ -44,14 +44,14 @@ const runCheckoutPageTest = () => {
// Tax calculation should have been enabled by another test - no-op
// Enable BACS payment method
await withRestApi.updatePaymentGateway( 'bacs', { enabled: true } );
await withRestApi.updatePaymentGateway( 'bacs', { enabled: true }, false );
// Enable COD payment method
await withRestApi.updatePaymentGateway( 'cod', { enabled: true } );
await withRestApi.updatePaymentGateway( 'cod', { enabled: true }, false );
});
afterAll(async () => {
await withRestApi.deleteAllShippingZones();
await withRestApi.deleteAllShippingZones( false );
});
it('should display cart items in order review', async () => {

View File

@ -0,0 +1,49 @@
/**
* Internal dependencies
*/
const { createCoupon } = require( '@woocommerce/e2e-utils' );
const couponsTable = [
[ 'fixed cart', { text: '$5.00' }, { text: '$4.99' } ],
[ 'percentage', { text: '$4.99' }, { text: '$5.00' } ],
[ 'fixed product', { text: '$5.00' }, { text: '$4.99' } ]
];
let couponFixedCart;
let couponPercentage;
let couponFixedProduct;
/**
* Get a test coupon Id. Create the coupon if it does not exist.
*
* @param {string} couponType Coupon type.
* @return {string} Coupon code.
*/
const getCouponId = async ( couponType ) => {
switch ( couponType ) {
case 'fixed cart':
if ( ! couponFixedCart ) {
couponFixedCart = await createCoupon();
}
return couponFixedCart;
case 'percentage':
if ( ! couponPercentage ) {
couponPercentage = await createCoupon( '50', 'Percentage discount' );
}
return couponPercentage;
case 'fixed product':
if ( ! couponFixedProduct ) {
couponFixedProduct = await createCoupon( '5', 'Fixed product discount' );
}
return couponFixedProduct;
}
};
const getCouponsTable = () => {
return couponsTable;
};
module.exports = {
getCouponsTable,
getCouponId,
};

View File

@ -4,6 +4,7 @@
- Added quotes around `WORDPRESS_TITLE` value in .env file to address issue with docker compose 2 "key cannot contain a space" error.
- Added `LATEST_WP_VERSION_MINUS` that allows setting a number to subtract from the current WordPress version for the WordPress Docker image.
- Support for PHP_VERSION, MARIADB_VERSION environment variables for built in container initialization
## Fixed

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-environment/CHANGELOG.md).

View File

@ -17,22 +17,30 @@ if [[ $1 ]]; then
export WORDPRESS_VERSION=$(./bin/get-previous-version.js $WORDPRESS_VERSION $LATEST_WP_VERSION_MINUS 2> /dev/null)
fi
if ! [[ $TRAVIS_PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
TRAVIS_PHP_VERSION=$(./bin/get-latest-docker-tag.js php 7 2> /dev/null)
fi
if [[ $TRAVIS_PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_PHP_VERSION=$TRAVIS_PHP_VERSION
if [[ $PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_PHP_VERSION=$PHP_VERSION
else
export DC_PHP_VERSION="7.4.22"
if ! [[ $TRAVIS_PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
TRAVIS_PHP_VERSION=$(./bin/get-latest-docker-tag.js php 7 2> /dev/null)
fi
if [[ $TRAVIS_PHP_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_PHP_VERSION=$TRAVIS_PHP_VERSION
else
export DC_PHP_VERSION="7.4.25"
fi
fi
if ! [[ $TRAVIS_MARIADB_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
TRAVIS_MARIADB_VERSION=$(./bin/get-latest-docker-tag.js mariadb 10 2> /dev/null)
fi
if [[ $TRAVIS_MARIADB_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_MARIADB_VERSION=$TRAVIS_MARIADB_VERSION
if [[ $MARIADB_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_MARIADB_VERSION=$MARIADB_VERSION
else
export DC_MARIADB_VERSION="10.6.4"
if ! [[ $TRAVIS_MARIADB_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
TRAVIS_MARIADB_VERSION=$(./bin/get-latest-docker-tag.js mariadb 10 2> /dev/null)
fi
if [[ $TRAVIS_MARIADB_VERSION =~ ^[0-9]+\.[0-9]+ ]]; then
export DC_MARIADB_VERSION=$TRAVIS_MARIADB_VERSION
else
export DC_MARIADB_VERSION="10.6.5"
fi
fi
if [[ $1 == 'up' ]]; then

View File

@ -126,9 +126,17 @@ The built in container defaults to mapping the root folder of the repository to
Since the introduction of the WooCommerce Monorepo, a `WC_CORE_PATH` environment variable maps to Core WooCommerce at `plugins/woocommerce`. It can also be overriden in a similar fashion.
### Specifying Server Software versions
The built-in container supports these variables for use locally and in CI environments:
- `WP_VERSION` - WordPress (default `latest`)
- `PHP_VERSION` - PHP (default `latest`)
- `MARIADB_VERSION` - MariaDB (default `latest`)
### Travis CI Supported Versions
Travis CI uses environment variables to allow control of some software versions in the testing environment. The built in container supports these variables:
Travis CI uses environment variables to allow control of some software versions in the testing environment. The built-in container supports these variables:
- `WP_VERSION` - WordPress (default `latest`)
- `TRAVIS_PHP_VERSION` - PHP (default `latest`)

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/e2e-environment",
"description": "WooCommerce end to end testing environment",
"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",
"Add",
"Update",
"Dev",
"Tweak",
"Performance",
"Enhancement"
],
"changelog": "NEXT_CHANGELOG.md"
}
}
}

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

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,11 @@
echo "Initializing WooCommerce E2E"
wp plugin activate woocommerce
# This is a workaround to accommodate different directory names.
wp plugin activate --all
wp plugin deactivate akismet
wp plugin deactivate hello
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com \
--user_pass=password \

View File

@ -41,6 +41,7 @@
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.12.7",
"@wordpress/eslint-plugin": "7.3.0",
"eslint": "^8.1.0",
"ndb": "^1.1.5",
"semver": "^7.3.2"
},
@ -58,7 +59,8 @@
"docker:ssh": "docker exec -it $(node utils/get-app-name.js)_wordpress-www /bin/bash",
"test:e2e": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js",
"test:e2e-debug": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev --debug",
"test:e2e-dev": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev"
"test:e2e-dev": "bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev",
"lint": "eslint src"
},
"bin": {
"wc-e2e": "bin/wc-e2e.sh"

View File

@ -0,0 +1,79 @@
{
"root": "packages/js/e2e-environment/",
"sourceRoot": "packages/js/e2e-environment/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "build"
}
},
"lint": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "lint"
}
},
"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"
}
},
"docker-up": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "docker:up"
}
},
"docker-down": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "docker:down"
}
},
"docker-clear-all": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "docker:clear-all"
}
},
"docker-ssh": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "docker:ssh"
}
},
"test-e2e": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test:e2e"
}
},
"test-e2e-debug": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test:e2e-debug"
}
},
"test-e2e-dev": {
"executor": "@nrwl/workspace:run-script",
"options": {
"script": "test:e2e-dev"
}
}
}
}

View File

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

View File

@ -7,6 +7,13 @@
- Update `shopper.addToCartFromShopPage()` and `.removeFromCart()` to accept product Id or Title
- Added `deleteAllProductAttributes()`, `deleteAllProductCategories()`, and `deleteAllProductTags()` to clean up meta data added when products are imported
- Added `withRestApi.createProductCategory()` that creates a product category and returns the ID
- `deleteAllProductAttributes()`, `deleteAllProductCategories()`, and `deleteAllProductTags()` to clean up meta data added when products are imported
- `withRestApi.createProductCategory()` that creates a product category and returns the ID
- `withRestApi.deleteCoupon()` that deletes a single coupon
- `withRestApi.addTaxClasses()` that adds an array of tax classes if they do not exist
- `withRestApi.addTaxRates()` that adds an array of tax rates if they do not exist
- `clickAndWaitForSelector( buttonSelector, resultSelector, timeout )` to click a button and wait for response
- Optional parameter `testResponse` to `withRestApi` functions that contain an `expect()`
# 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-utils/CHANGELOG.md).

View File

@ -142,22 +142,26 @@ Please note: if you're using a non-SSL environment (such as a Docker container f
| Function | Parameters | Description |
|----------|------------|-------------|
| `resetOnboarding` | | Reset onboarding settings |
| `addShippingZoneAndMethod` | `zoneName`, `zoneLocation`, `zipCode`, `zoneMethod`, `cost`, `additionalZoneMethods`, `testResponse` | Adds a shipping zone along with a shipping method |
| `batchCreateOrders` | `orders`, `testResponse` | Create a batch of orders using the "Batch Create Order" API endpoint |
| `addTaxClasses` | `taxClasses` | Add an array of tax classes if they do not exist |
| `addTaxRates` | `taxRates` | Add an array of tax rates if they do not exist |
| `createProductCategory` | `categoryName` | Create a product category with the provided name |
| `deleteAllCoupons` | | Permanently delete all coupons |
| `deleteAllProducts` | | Permanently delete all products |
| `deleteAllShippingZones` | | Permanently delete all shipping zones except the default |
| `deleteAllShippingClasses` | Permanently delete all shipping classes |
| `deleteCustomerByEmail` | `emailAddress` | Delete customer user account. Posts are reassigned to user ID 1 |
| `resetSettingsGroupToDefault` | `settingsGroup` | Reset settings in settings group to default except `select` fields |
| `batchCreateOrders` | `orders` | Create a batch of orders using the "Batch Create Order" API endpoint |
| `deleteAllOrders` | | Permanently delete all orders |
| `updateSettingOption` | `settingsGroup`, `settingID`, `payload` | Update a settings group |
| `updatePaymentGateway`| `paymentGatewayId`, `payload` | Update a payment gateway |
| `deleteAllProductAttributes` | `testResponse` | Permanently delete all product attributes |
| `deleteAllProductCategories` | `testResponse` | Permanently delete all product categories |
| `deleteAllProducts` | | Permanently delete all products |
| `deleteAllProductTags` | `testResponse` | Permanently delete all product tags |
| `deleteAllShippingClasses` | `testResponse` | Permanently delete all shipping classes |
| `deleteAllShippingZones` | `testResponse` | Permanently delete all shipping zones except the default |
| `deleteCoupon` | `couponId` | Permanently delete a coupon |
| `deleteCustomerByEmail` | `emailAddress` | Delete customer user account. Posts are reassigned to user ID 1 |
| `getSystemEnvironment` | | Get the current environment from the WooCommerce system status API. |
| `deleteAllProductAttributes` | | Permanently delete all product attributes. |
| `deleteAllProductCategories` | | Permanently delete all product categories. |
| `deleteAllProductTags` | | Permanently delete all product tags. |
| `createProductCategory` | `categoryName` | Create a product category with the provided name. |
| `resetOnboarding` | | Reset onboarding settings |
| `resetSettingsGroupToDefault` | `settingsGroup`, `testResponse` | Reset settings in settings group to default except `select` fields |
| `updateSettingOption` | `settingsGroup`, `settingID`, `payload` | Update a settings group |
| `updatePaymentGateway`| `paymentGatewayId`, `payload`, `testResponse` | Update a payment gateway |
### Classes
@ -229,6 +233,7 @@ There is a general utilities object `utils` with the following functions:
| `deleteAllShippingZones` | | Delete all the existing shipping zones |
| `waitForSelectorWithoutThrow` | `selector`, `timeoutInSeconds` | conditionally wait for a selector without throwing an error. Default timeout is 5 seconds |
| `createOrder` | `orderOptions` | Creates an order using the API with the passed in details |
| `clickAndWaitForSelector` | `buttonSelector`, `resultSelector`, `timeout` | Click a button and wait for response |
### Test Utilities

View File

View File

@ -0,0 +1,27 @@
{
"name": "woocommerce/e2e-utiles",
"description": "WooCommerce end to end testing utilities",
"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",
"Add",
"Update",
"Dev",
"Tweak",
"Performance",
"Enhancement"
],
"changelog": "NEXT_CHANGELOG.md"
}
}
}

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

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,18 @@
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@automattic/puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50",
"@wordpress/deprecated": "^2.10.0",
"@wordpress/e2e-test-utils": "^4.16.1",
"config": "3.3.3",
"faker": "^5.1.0",
"fishery": "^1.2.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"eslint": "^8.1.0"
},
"peerDependencies": {
"@woocommerce/api": "^0.2.0"
},
@ -27,6 +33,7 @@
"clean": "rm -rf ./build ./build-module",
"compile": "node ./../bin/build.js",
"build": "pnpm run clean && pnpm run compile",
"prepare": "pnpm run build"
"prepare": "pnpm run build",
"lint": "eslint src"
}
}

View File

@ -0,0 +1,37 @@
{
"root": "packages/js/e2e-utils/",
"sourceRoot": "packages/js/e2e-utils/src",
"projectType": "library",
"targets": {
"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"
}
}
}
}

View File

@ -190,13 +190,13 @@ const completeOnboardingWizard = async () => {
/**
* Create simple product.
*
* @param productTitle - Defaults to Simple Product. Customizable title.
* @param productPrice - Defaults to $9.99. Customizable pricing.
* @param productTitle Defaults to Simple Product. Customizable title.
* @param productPrice Defaults to $9.99. Customizable pricing.
*/
const createSimpleProduct = async ( productTitle = simpleProductName, productPrice = simpleProductPrice ) => {
const product = await factories.products.simple.create( {
name: productTitle,
regularPrice: productPrice
regularPrice: productPrice,
} );
return product.id;
} ;

View File

@ -4,12 +4,14 @@ import {Coupon, Setting, SimpleProduct, Order} from '@woocommerce/api';
const client = factories.api.withDefaultPermalinks;
const onboardingProfileEndpoint = '/wc-admin/onboarding/profile';
const shippingZoneEndpoint = '/wc/v3/shipping/zones';
const shippingClassesEndpoint = '/wc/v3/products/shipping_classes';
const userEndpoint = '/wp/v2/users';
const systemStatusEndpoint = '/wc/v3/system_status';
const productsEndpoint = '/wc/v3/products';
const productCategoriesEndpoint = '/wc/v3/products/categories';
const shippingClassesEndpoint = '/wc/v3/products/shipping_classes';
const shippingZoneEndpoint = '/wc/v3/shipping/zones';
const systemStatusEndpoint = '/wc/v3/system_status';
const taxClassesEndpoint = '/wc/v3/taxes/classes';
const taxRatesEndpoint = '/wc/v3/taxes';
const userEndpoint = '/wp/v2/users';
/**
* Utility function to delete all merchant created data store objects.
@ -42,6 +44,16 @@ const deleteAllRepositoryObjects = async ( repository, defaultObjectId = null, s
}
};
/**
* Utility to flatten a tax rate.
*
* @param {object} taxRate Tax rate to be flattened.
* @return {string}
*/
const flattenTaxRate = ( taxRate ) => {
return taxRate.rate + '/' + taxRate.class + '/' + taxRate.name;
};
/**
* Utility functions that use the REST API to process the requested function.
*/
@ -77,6 +89,16 @@ export const withRestApi = {
const repository = Coupon.restRepository( client );
await deleteAllRepositoryObjects( repository );
},
/**
* Use api package to delete a coupon.
*
* @param {number} couponId Coupon ID.
* @return {Promise} Promise resolving once coupon has been deleted.
*/
deleteCoupon: async ( couponId ) => {
const repository = Coupon.restRepository( client );
await repository.delete( couponId );
},
/**
* Use api package to delete products.
*
@ -89,24 +111,28 @@ export const withRestApi = {
/**
* Use the API to delete all product attributes.
*
* @param {boolean} testResponse Test the response status code.
* @return {Promise} Promise resolving once attributes have been deleted.
*/
deleteAllProductAttributes: async () => {
deleteAllProductAttributes: async ( testResponse = true ) => {
const productAttributesPath = productsEndpoint + '/attributes';
const productAttributes = await client.get( productAttributesPath );
if ( productAttributes.data && productAttributes.data.length ) {
for ( let a = 0; a < productAttributes.data.length; a++ ) {
const response = await client.delete( productAttributesPath + `/${productAttributes.data[a].id}?force=true` );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
/**
* Use the API to delete all product categories.
*
* @param {boolean} testResponse Test the response status code.
* @return {Promise} Promise resolving once categories have been deleted.
*/
deleteAllProductCategories: async () => {
deleteAllProductCategories: async ( testResponse = true ) => {
const productCategoriesPath = productsEndpoint + '/categories';
const productCategories = await client.get( productCategoriesPath );
if ( productCategories.data && productCategories.data.length ) {
@ -116,22 +142,27 @@ export const withRestApi = {
continue;
}
const response = await client.delete( productCategoriesPath + `/${productCategories.data[c].id}?force=true` );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
/**
* Use the API to delete all product tags.
*
* @param {boolean} testResponse Test the response status code.
* @return {Promise} Promise resolving once tags have been deleted.
*/
deleteAllProductTags: async () => {
deleteAllProductTags: async ( testResponse = true ) => {
const productTagsPath = productsEndpoint + '/tags';
const productTags = await client.get( productTagsPath );
if ( productTags.data && productTags.data.length ) {
for ( let t = 0; t < productTags.data.length; t++ ) {
const response = await client.delete( productTagsPath + `/${productTags.data[t].id}?force=true` );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
@ -155,6 +186,7 @@ export const withRestApi = {
* @param zoneMethod Shipping method type. Defaults to flat_rate (use also: free_shipping or local_pickup).
* @param cost Shipping method cost. Default is no cost.
* @param additionalZoneMethods Array of additional zone methods to add to the shipping zone.
* @param {boolean} testResponse Test the response status code.
*/
addShippingZoneAndMethod: async (
zoneName,
@ -162,33 +194,38 @@ export const withRestApi = {
zipCode = '',
zoneMethod = 'flat_rate',
cost = '',
additionalZoneMethods = [] ) => {
additionalZoneMethods = [],
testResponse = true ) => {
const path = 'wc/v3/shipping/zones';
const path = 'wc/v3/shipping/zones';
const response = await client.post( path, { name: zoneName } );
expect(response.status).toEqual(201);
let zoneId = response.data.id;
const response = await client.post( path, { name: zoneName } );
if ( testResponse ) {
expect( response.status ).toEqual( 201 );
}
let zoneId = response.data.id;
// Select shipping zone location
let [ zoneType, zoneCode ] = zoneLocation.split(/:(.+)/);
let zoneLocationPayload = [
// Select shipping zone location
let [ zoneType, zoneCode ] = zoneLocation.split(/:(.+)/);
let zoneLocationPayload = [
{
code: zoneCode,
type: zoneType,
}
];
];
// Fill shipping zone postcode if provided
if ( zipCode ) {
// Fill shipping zone postcode if provided
if ( zipCode ) {
zoneLocationPayload.push( {
code: zipCode,
type: "postcode",
} );
}
}
const locationResponse = await client.put( path + `/${zoneId}/locations`, zoneLocationPayload );
expect(locationResponse.status).toEqual(200);
const locationResponse = await client.put( path + `/${zoneId}/locations`, zoneLocationPayload );
if ( testResponse ) {
expect( locationResponse.status ).toEqual( 200 );
}
// Add shipping zone method
let methodPayload = {
@ -196,7 +233,9 @@ export const withRestApi = {
}
const methodsResponse = await client.post( path + `/${zoneId}/methods`, methodPayload );
expect(methodsResponse.status).toEqual(200);
if ( testResponse ) {
expect( methodsResponse.status ).toEqual( 200 );
}
let methodId = methodsResponse.data.id;
// Add in cost, if provided
@ -208,23 +247,28 @@ export const withRestApi = {
}
const costResponse = await client.put( path + `/${zoneId}/methods/${methodId}`, costPayload );
expect(costResponse.status).toEqual(200);
if ( testResponse ) {
expect( costResponse.status ).toEqual( 200 );
}
}
// Add any additional zones, if provided
if (additionalZoneMethods.length > 0) {
for ( let z = 0; z < additionalZoneMethods.length; z++ ) {
let response = await client.post( path + `/${zoneId}/methods`, { method_id: additionalZoneMethods[z] } );
expect(response.status).toEqual(200);
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
/**
* Use api package to delete shipping zones.
*
* @param {boolean} testResponse Test the response status code.
* @return {Promise} Promise resolving once shipping zones have been deleted.
*/
deleteAllShippingZones: async () => {
deleteAllShippingZones: async ( testResponse = true ) => {
const shippingZones = await client.get( shippingZoneEndpoint );
if ( shippingZones.data && shippingZones.data.length ) {
for ( let z = 0; z < shippingZones.data.length; z++ ) {
@ -233,21 +277,26 @@ export const withRestApi = {
continue;
}
const response = await client.delete( shippingZoneEndpoint + `/${shippingZones.data[z].id}?force=true` );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
/**
* Use api package to delete shipping classes.
*
* @param {boolean} testResponse Test the response status code.
* @return {Promise} Promise resolving once shipping classes have been deleted.
*/
deleteAllShippingClasses: async () => {
deleteAllShippingClasses: async ( testResponse = true ) => {
const shippingClasses = await client.get( shippingClassesEndpoint );
if ( shippingClasses.data && shippingClasses.data.length ) {
for ( let c = 0; c < shippingClasses.data.length; c++ ) {
const response = await client.delete( shippingClassesEndpoint + `/${shippingClasses.data[c].id}?force=true` );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
}
}
},
@ -278,9 +327,10 @@ export const withRestApi = {
/**
* Reset a settings group to default values except selects.
* @param settingsGroup
* @param {boolean} testResponse Test the response status code.
* @returns {Promise<void>}
*/
resetSettingsGroupToDefault: async ( settingsGroup ) => {
resetSettingsGroupToDefault: async ( settingsGroup, testResponse = true ) => {
const settingsClient = Setting.restRepository( client );
const settings = await settingsClient.list( settingsGroup );
if ( ! settings.length ) {
@ -300,7 +350,7 @@ export const withRestApi = {
const response = await settingsClient.update( settingsGroup, defaultSetting.id, defaultSetting );
// Multi-selects have a default '' but return an empty [].
if ( settings[s].type != 'multiselect' ) {
if ( testResponse && settings[s].type != 'multiselect' ) {
expect( response.value ).toBe( defaultSetting.value );
}
}
@ -321,22 +371,61 @@ export const withRestApi = {
*
* @param {string} paymentGatewayId The ID of the payment gateway to update.
* @param {object} payload An object with the key/value pair to update.
* @param {boolean} testResponse Test the response status code.
*/
updatePaymentGateway: async ( paymentGatewayId, payload = {} ) => {
updatePaymentGateway: async ( paymentGatewayId, payload = {}, testResponse = true ) => {
const response = await client.put( `/wc/v3/payment_gateways/${paymentGatewayId}`, payload );
expect( response.status ).toBe( 200 );
if ( testResponse ) {
expect( response.status ).toEqual( 200 );
}
},
/**
* Create a batch of orders using the "Batch Create Order" API endpoint.
*
* @param orders Array of orders to be created
* @param {boolean} testResponse Test the response status code.
*/
batchCreateOrders: async (orders) => {
batchCreateOrders: async ( orders, testResponse = true ) => {
const path = '/wc/v3/orders/batch';
const payload = { create: orders };
const response = await client.post(path, payload);
expect( response.status ).toEqual(200);
if ( testResponse ) {
expect( response.status ).toBe( 200 );
}
},
/**
* Add tax classes.
*
* @param {<Array<Object>>} taxClasses Array of tax class objects.
* @returns {Promise<void>}
*/
addTaxClasses: async ( taxClasses ) => {
// Only add tax classes which don't already exist.
const existingTaxClasses = await client.get( taxClassesEndpoint );
const existingTaxNames = existingTaxClasses.data.map( taxClass => taxClass.name );
const newTaxClasses = taxClasses.filter( taxClass => ! existingTaxNames.includes( taxClass.name ) );
for ( const taxClass of newTaxClasses ) {
await client.post( taxClassesEndpoint, taxClass );
}
},
/**
* Add tax rates.
*
* @param {<Array<Object>>} taxRates Array of tax rate objects.
* @returns {Promise<void>}
*/
addTaxRates: async ( taxRates ) => {
// Only add rates which don't already exist
const existingTaxRates = await client.get( taxRatesEndpoint );
const existingRates = existingTaxRates.data.map( taxRate => flattenTaxRate( taxRate ) );
for ( const taxRate of taxRates ) {
if ( ! existingRates.includes( flattenTaxRate( taxRate ) ) ) {
await client.post( taxRatesEndpoint, taxRate );
}
}
},
/**
* Get the current environment from the WooCommerce system status API.

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { pressKeyWithModifier } from '@wordpress/e2e-test-utils';
import { waitForSelector } from '@automattic/puppeteer-utils';
/**
* Internal dependencies
@ -220,11 +221,11 @@ export const evalAndClick = async ( selector ) => {
* @param {string} selector Selector of the select2 search field
*/
export const selectOptionInSelect2 = async ( value, selector = 'input.select2-search__field' ) => {
await page.waitForSelector(selector);
await page.click(selector);
await page.type(selector, value);
await page.waitForSelector( selector );
await page.click( selector );
await page.type( selector, value );
await waitForTimeout( 2000 ); // to avoid flakyness, must wait before pressing Enter
await page.keyboard.press('Enter');
await page.keyboard.press( 'Enter' );
};
/**
@ -234,12 +235,12 @@ export const selectOptionInSelect2 = async ( value, selector = 'input.select2-se
* @param {string} orderId Order ID
* @param {string} customerName Customer's full name attached to order ID.
*/
export const searchForOrder = async (value, orderId, customerName) => {
await clearAndFillInput('#post-search-input', value);
await expect(page).toMatchElement('#post-search-input', value);
await expect(page).toClick('#search-submit' );
await page.waitForSelector('#the-list', { timeout: 10000 } );
await expect(page).toMatchElement('.order_number > a.order-view', {text: `#${orderId} ${customerName}`});
export const searchForOrder = async ( value, orderId, customerName) => {
await clearAndFillInput( '#post-search-input', value );
await expect( page ).toMatchElement( '#post-search-input', value );
await expect( page ).toClick( '#search-submit' );
await page.waitForSelector( '#the-list', { timeout: 10000 } );
await expect( page ).toMatchElement( '.order_number > a.order-view', { text: `#${orderId} ${customerName}` } );
};
/**
@ -255,16 +256,16 @@ export const applyCoupon = async ( couponCode ) => {
page.reload(),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
]);
await expect(page).toClick('a', {text: 'Click here to enter your code'});
await expect( page ).toClick( 'a', { text: 'Click here to enter your code' } );
await uiUnblocked();
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
await clearAndFillInput( '#coupon_code', couponCode );
await expect( page ).toClick( 'button', {text: 'Apply coupon' } );
await uiUnblocked();
} catch (error) {
await clearAndFillInput('#coupon_code', couponCode);
await expect(page).toClick('button', {text: 'Apply coupon'});
} catch ( error ) {
await clearAndFillInput( '#coupon_code', couponCode );
await expect( page ).toClick( 'button', { text: 'Apply coupon' } );
await uiUnblocked();
};
}
};
/**
@ -278,9 +279,9 @@ export const removeCoupon = async ( couponCode ) => {
page.reload(),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
]);
await expect(page).toClick('[data-coupon="'+couponCode.toLowerCase()+'"]', {text: '[Remove]'});
await expect( page ).toClick( '[data-coupon="'+couponCode.toLowerCase()+'"]', {text: '[Remove]' } );
await uiUnblocked();
await expect(page).toMatchElement('.woocommerce-message', {text: 'Coupon has been removed.'});
await expect( page ).toMatchElement( '.woocommerce-message', {text: 'Coupon has been removed.' } );
};
/**
@ -296,3 +297,24 @@ export const selectOrderAction = async ( action ) => {
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
}
/**
* Evaluate and click a button selector then wait for a result selector.
* This is a work around for what appears to be intermittent delays in handling confirm dialogs.
*
* @param {string} buttonSelector Selector of button to click
* @param {string} resultSelector Selector to wait for after click
* @param {number} timeout Timeout length in milliseconds. Default 5000.
* @returns {Promise<void>}
*/
export const clickAndWaitForSelector = async ( buttonSelector, resultSelector, timeout = 5000 ) => {
await evalAndClick( buttonSelector );
await waitForSelector(
page,
resultSelector,
{
timeout
}
);
};

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