Merge branch 'trunk' into fix/a11y-improvement-add-to-cart-variable

This commit is contained in:
Cullen Whitmore 2021-12-04 22:57:55 -06:00
commit a96eb4d583
350 changed files with 17706 additions and 5997 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
@ -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.
@ -73,8 +73,9 @@ jobs:
with:
path: package/woocommerce
- name: Run npm install.
working-directory: package/woocommerce/plugins/woocommerce
- name: Install PNPM and install dependencies
working-directory: package/woocommerce
run: |
npm install -g pnpm
pnpm install
@ -82,7 +83,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 +104,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/

View File

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

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

@ -13,26 +13,32 @@
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"scripts": {
"preinstall": "npx only-allow pnpm"
},
"devDependencies": {
"@nrwl/tao": "12.10.0",
"@nrwl/cli": "12.10.0",
"@nrwl/workspace": "12.10.0",
"@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.2.0",
"@wordpress/prettier-config": "^1.0.5",
"@woocommerce/eslint-plugin": "^1.3.0",
"@wordpress/prettier-config": "^1.1.1",
"chalk": "^4.1.2",
"glob": "^7.2.0",
"jest": "^27.0.6",
"jest": "^27.3.1",
"mkdirp": "^1.0.4",
"node-stream-zip": "^1.13.6",
"prettier": "npm:wp-prettier@2.2.1-beta-1",
"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.3.3",
"@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

@ -6,3 +6,5 @@
- Coupons API Tests
- Refunds API Tests
- Products API Tests
- CRUD tests for the Orders API
- Order Search API Tests

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@ -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.',
@ -15,5 +14,5 @@
};
module.exports = {
refund: refund,
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,14 +29,16 @@ const ordersApi = {
method: 'GET',
path: 'orders/<id>',
responseCode: 200,
order: async ( orderId ) => getRequest( `orders/${orderId}` ),
order: async ( orderId, ordersQuery = {} ) =>
getRequest( `orders/${ orderId }`, ordersQuery ),
},
listAll: {
name: 'List all orders',
method: 'GET',
path: 'orders',
responseCode: 200,
orders: async () => getRequest( 'orders' ),
orders: async ( ordersQuery = {} ) =>
getRequest( 'orders', ordersQuery ),
},
update: {
name: 'Update an order',
@ -39,7 +46,8 @@ const ordersApi = {
path: 'orders/<id>',
responseCode: 200,
payload: getOrderExample(),
order: async ( orderId, orderDetails ) => putRequest( `orders/${orderId}`, orderDetails ),
order: async ( orderId, orderDetails ) =>
putRequest( `orders/${ orderId }`, orderDetails ),
},
delete: {
name: 'Delete an order',
@ -47,9 +55,10 @@ const ordersApi = {
path: 'orders/<id>',
responseCode: 200,
payload: {
force: false
force: false,
},
order: async ( orderId, deletePermanently ) => deleteRequest( `orders/${orderId}`, deletePermanently ),
order: async ( orderId, deletePermanently ) =>
deleteRequest( `orders/${ orderId }`, deletePermanently ),
},
batch: {
name: 'Batch update orders',
@ -57,7 +66,8 @@ const ordersApi = {
path: 'orders/batch',
responseCode: 200,
payload: shared.getBatchPayloadExample( getOrderExample() ),
orders: async ( batchUpdatePayload ) => postRequest( `orders/batch`, batchUpdatePayload ),
orders: async ( batchUpdatePayload ) =>
postRequest( `orders/batch`, batchUpdatePayload ),
},
};

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "NEXT_CHANGELOG.md"
}
}
}

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

File diff suppressed because it is too large Load Diff

View File

@ -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,7 +1,7 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"types": [ "node", "jest", "axios", "moxios", "create-hmac" ],
"types": [ "node", "jest", "axios", "create-hmac" ],
"rootDir": "src",
"outDir": "dist",
"target": "es5"

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

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

View File

@ -1,8 +1,14 @@
# Unreleased
## Added
- A `specs/data` folder to store page element data.
- Tests to verify that different top-level menu and their associated sub-menus load successfully.
## Changed
- New coupon test deletes the coupon instead of trashing it.
- A copy of sample_data.csv is included in the package.
# 0.1.6

View File

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

View File

@ -72,6 +72,7 @@ The functions to access the core tests are:
- `runAnalyticsPageLoadsTest` - Merchant can load and see all pages in Analytics
- `runImportProductsTest` - Merchant can import products via CSV file
- `runInitiateWccomConnectionTest` - Merchant can initiate connection to WooCommerce.com
- `runAdminPageLoadTests` - Merchant can load pages from the WP Admin sidebar
### Shopper

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
"main": "index.js",
"dependencies": {
"@jest/globals": "^26.4.2",
"@wordpress/deprecated": "^3.2.3",
"config": "3.3.3",
"faker": "^5.1.0"
},
@ -20,5 +21,9 @@
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "./bin/build.sh",
"prepare": "pnpm run build"
}
}

View File

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

View File

@ -11,6 +11,7 @@ const {
describe,
beforeAll,
} = require( '@jest/globals' );
import deprecated from '@wordpress/deprecated';
const runActivationTest = () => {
describe('Store owner can login and make sure WooCommerce is activated', () => {
@ -19,6 +20,10 @@ const runActivationTest = () => {
});
it('can make sure WooCommerce is activated. If not, activate it', async () => {
deprecated( 'runActivationTest', {
alternative: '@woocommerce/admin-e2e-tests `testAdminBasicSetup()`',
});
const slug = 'woocommerce';
await merchant.openPlugins();
const disableLink = await page.$(`tr[data-slug="${slug}"] .deactivate a`);

View File

@ -13,6 +13,7 @@ const {
* External dependencies
*/
const config = require( 'config' );
import deprecated from '@wordpress/deprecated';
const {
it,
describe,
@ -47,6 +48,9 @@ const runOnboardingFlowTest = () => {
}
it('can start and complete onboarding when visiting the site for the first time.', async () => {
deprecated( 'runOnboardingFlowTest', {
alternative: '@woocommerce/admin-e2e-tests `testAdminOnboardingWizard()`',
});
await completeOnboardingWizard();
});
});
@ -59,6 +63,9 @@ const runTaskListTest = () => {
});
it('can setup shipping', async () => {
deprecated( 'runTaskListTest', {
alternative: '@woocommerce/admin-e2e-tests `testAdminHomescreenTasklist()`',
});
await page.evaluate(() => {
document.querySelector('.woocommerce-list__item-title').scrollIntoView();
});

View File

@ -6,6 +6,7 @@ const {
it,
describe,
} = require( '@jest/globals' );
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
@ -31,6 +32,9 @@ const runInitialStoreSettingsTest = () => {
});
it('can enable tax rates and calculations', async () => {
deprecated( 'runInitialStoreSettingsTest', {
alternative: '@woocommerce/admin-e2e-tests `testAdminBasicSetup()`',
});
// Go to general settings page
await merchant.openSettings('general');

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' );
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 => {
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 );
.catch( ( err ) => {
expect( err.statusCode ).toBe( 400 );
} );
expect( response ).toBeUndefined();
} );
}
);
it( 'returns 200 with correct fields', async () => {
const response = await client
.post( `/wc-telemetry/tracker`, {
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

@ -0,0 +1,85 @@
/**
* WP top-level menu items and their associated sub-menus
*/
export const MENUS = [
[
'WooCommerce',
'#adminmenu > li:nth-child(8) > a',
[
[
'Home',
'#toplevel_page_woocommerce > ul > li:nth-child(2) > a',
'Home',
],
[
'Orders',
'#toplevel_page_woocommerce > ul > li:nth-child(3) > a',
'Orders',
],
[
'Reports',
'#toplevel_page_woocommerce > ul > li:nth-child(6) > a',
'Orders',
],
[
'Settings',
'#toplevel_page_woocommerce > ul > li:nth-child(7) > a',
'General',
],
[
'Status',
'#toplevel_page_woocommerce > ul > li:nth-child(8) > a',
'System status',
],
// [ 'Extensions', '#toplevel_page_woocommerce > ul > li:nth-child(9)', 'Extensions' ],
],
],
[
'Products',
'#adminmenu > li:nth-child(9) > a',
[
[
'All Products',
'#menu-posts-product > ul > li:nth-child(2) > a',
'Products',
],
[
'Add New',
'#menu-posts-product > ul > li:nth-child(3) > a',
'Add New',
],
[
'Categories',
'#menu-posts-product > ul > li:nth-child(4) > a',
'Product categories',
],
[
'Product tags',
'#menu-posts-product > ul > li:nth-child(5) > a',
'Product tags',
],
[
'Attributes',
'#menu-posts-product > ul > li:nth-child(6) > a',
'Attributes',
],
],
],
[
'Marketing',
'#adminmenu > li:nth-child(11) > a',
[
[
'Overview',
'#toplevel_page_woocommerce-marketing > ul > li:nth-child(2) > a',
'Overview',
],
[
'Coupons',
'#toplevel_page_woocommerce-marketing > ul > li:nth-child(3) > a',
'Coupons',
],
],
],
];

View File

@ -37,6 +37,7 @@ const runProductSettingsTest = require( './merchant/wp-admin-settings-product.te
const runTaxSettingsTest = require( './merchant/wp-admin-settings-tax.test' );
const runOrderStatusFiltersTest = require( './merchant/wp-admin-order-status-filters.test' );
const runOrderRefundTest = require( './merchant/wp-admin-order-refund.test' );
const runOrderRefundRestockTest = require( './merchant/wp-admin-order-refund-restock.test' );
const runOrderApplyCouponTest = require( './merchant/wp-admin-order-apply-coupon.test' );
const runProductEditDetailsTest = require( './merchant/wp-admin-product-edit-details.test' );
const runProductSearchTest = require( './merchant/wp-admin-product-search.test' );
@ -46,6 +47,7 @@ const runOrderSearchingTest = require( './merchant/wp-admin-order-searching.test
const runAnalyticsPageLoadsTest = require( './merchant/wp-admin-analytics-page-loads.test' );
const runImportProductsTest = require( './merchant/wp-admin-product-import-csv.test' );
const runInitiateWccomConnectionTest = require( './merchant/wp-admin-extensions-connect-wccom.test' );
const runAdminPageLoadTests = require( './merchant/wp-admin-page-loads.test.js' );
// REST API tests
const runExternalProductAPITest = require( './api/external-product.test' );
@ -95,12 +97,14 @@ const runMerchantTests = () => {
runTaxSettingsTest();
runOrderStatusFiltersTest();
runOrderRefundTest();
runOrderRefundRestockTest();
runOrderApplyCouponTest();
runProductEditDetailsTest();
runProductSearchTest();
runMerchantOrdersCustomerPaymentPage();
runAnalyticsPageLoadsTest();
runInitiateWccomConnectionTest();
runAdminPageLoadTests();
}
const runApiTests = () => {
@ -142,6 +146,7 @@ module.exports = {
runOrderApiTest,
runOrderStatusFiltersTest,
runOrderRefundTest,
runOrderRefundRestockTest,
runOrderApplyCouponTest,
runProductEditDetailsTest,
runProductSearchTest,
@ -163,4 +168,5 @@ module.exports = {
runOrderEmailReceivingTest,
runInitiateWccomConnectionTest,
runTelemetryAPITest,
runAdminPageLoadTests,
};

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

@ -15,6 +15,7 @@ const {
describe,
beforeAll,
} = require( '@jest/globals' );
import deprecated from '@wordpress/deprecated';
/**
* Quick check for page title and no data message.
@ -57,6 +58,10 @@ const runAnalyticsPageLoadsTest = () => {
await merchant.login();
});
deprecated( 'runAnalyticsPageLoadsTest', {
alternative: '@woocommerce/admin-e2e-tests `testAdminAnalyticsPages()`',
});
it.each(pages)(
'can see %s page properly',
async (pageTitle, element, elementText) => {

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

@ -0,0 +1,117 @@
/**
* Internal dependencies
*/
const {
merchant,
createOrder,
createSimpleProduct,
verifyCheckboxIsSet,
uiUnblocked,
evalAndClick,
clickUpdateOrder,
} = require( '@woocommerce/e2e-utils' );
const { waitForSelector } = require( '@woocommerce/e2e-environment' );
/**
* 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
* @param {string} resultSelector
* @returns {Promise<void>}
*/
const clickAndWaitForSelector = async ( buttonSelector, resultSelector ) => {
await evalAndClick( buttonSelector );
await waitForSelector( page, resultSelector, {
timeout: 5000,
} );
};
const getRefundQuantityInputSelector = ( productName ) =>
`td.name[data-sort-value="${ productName }"] ~ td.quantity input.refund_order_item_qty`;
const runOrderRefundRestockTest = () => {
describe( 'WooCommerce Orders > Refund and restock an order item', () => {
// See: https://github.com/woocommerce/woocommerce/issues/30618
it( 'Can update order after refunding item without automatic stock adjustment', async () => {
const noInventoryProductId = await createSimpleProduct();
const productToRestockId = await createSimpleProduct(
'Product with Stock',
'10',
{
trackInventory: true,
remainingStock: 10,
}
);
const orderId = await createOrder( {
status: 'completed',
lineItems: [
{
product_id: noInventoryProductId,
},
{
product_id: productToRestockId,
quantity: 2,
},
],
} );
await merchant.login();
await merchant.goToOrder( orderId );
// Get the currency symbol for the store's selected currency
await page.waitForSelector( '.woocommerce-Price-currencySymbol' );
// Verify stock reduction system note was added
await expect( page ).toMatchElement( '.system-note', {
text: `Stock levels reduced: Product with Stock (#${ productToRestockId }) 10→8`,
} );
// Click the Refund button
await expect( page ).toClick( 'button.refund-items' );
// Verify the refund section shows
await page.waitForSelector( 'div.wc-order-refund-items', {
visible: true,
} );
await verifyCheckboxIsSet( '#restock_refunded_items' );
// Initiate a refund
await expect( page ).toFill(
getRefundQuantityInputSelector( 'Product with Stock' ),
'2'
);
await expect( page ).toFill( '#refund_reason', 'No longer wanted' );
await clickAndWaitForSelector(
'.do-manual-refund',
'.quantity .refunded'
);
await uiUnblocked();
// Verify restock system note was added
await expect( page ).toMatchElement( '.system-note', {
text: `Item #${ productToRestockId } stock increased from 8 to 10.`,
} );
// Update the order.
await clickUpdateOrder( 'Order updated.' );
// Verify that inventory wasn't modified.
// For some reason using expect().not.toMatchElement() did not work for this case.
expect(
await page.$$eval( '.note', ( notes ) =>
notes.every(
( note ) =>
! note.textContent.match(
/Adjusted stock: Product with Stock \(10→8\)/
)
)
)
).toEqual( true );
} );
} );
};
module.exports = runOrderRefundRestockTest;

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

@ -0,0 +1,49 @@
/**
* Internal dependencies
*/
const { merchant } = require( '@woocommerce/e2e-utils' );
const { MENUS } = require( '../data/elements' );
/**
* External dependencies
*/
const { it, describe, beforeAll } = require( '@jest/globals' );
const runPageLoadTest = () => {
describe.each( MENUS )(
' %s > Opening Top Level Pages',
( menuTitle, menuElement, subMenus ) => {
beforeAll( async () => {
await merchant.login();
} );
afterAll( async () => {
await merchant.logout();
} );
it.each( subMenus )(
'can see %s page properly',
async ( subMenuTitle, subMenuElement, subMenuText ) => {
// Go to Top Level Menu
await Promise.all( [
page.click( menuElement ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
// Click sub-menu item and wait for the page to finish loading
await Promise.all( [
page.click( subMenuElement ),
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
] );
await expect( page ).toMatchElement( 'h1', {
text: subMenuText,
} );
}
);
}
);
};
// eslint-disable-next-line jest/no-export
module.exports = runPageLoadTest;

View File

@ -17,8 +17,8 @@ const { it, describe, beforeAll, afterAll } = require( '@jest/globals' );
const path = require( 'path' );
const coreTestsPath = getCoreTestsRoot();
const filePath = path.resolve(
coreTestsPath.appRoot,
'plugins/woocommerce/sample-data/sample_products.csv'
coreTestsPath.packageRoot,
'test-data/sample_products.csv'
);
const filePathOverride = path.resolve(
coreTestsPath.packageRoot,
@ -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

@ -8,12 +8,13 @@ const {
uiUnblocked,
evalAndClick,
setCheckbox,
setBrowserViewport,
verifyAndPublish,
waitForSelector,
waitForSelectorWithoutThrow
} = require( '@woocommerce/e2e-utils' );
const {
waitAndClick,
waitForSelector,
} = require( '@woocommerce/e2e-environment' );
/**
@ -45,6 +46,13 @@ const runAddSimpleProductTest = () => {
});
it('can create simple virtual product and add it to the cart', async () => {
// @todo: remove this once https://github.com/woocommerce/woocommerce/issues/31337 has been addressed
await setBrowserViewport( {
width: 970,
height: 700,
} );
await openNewProductAndVerify();
// Set product data and publish the product
@ -72,6 +80,12 @@ const runAddSimpleProductTest = () => {
});
it('can create simple non-virtual product and add it to the cart', async () => {
// @todo: remove this once https://github.com/woocommerce/woocommerce/issues/31337 has been addressed
await setBrowserViewport( {
width: 960,
height: 700,
} );
await merchant.login();
await openNewProductAndVerify();

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,10 +35,14 @@ const runAddNewShippingZoneTest = () => {
beforeAll(async () => {
productId = await createSimpleProduct();
await withRestApi.deleteAllShippingZones();
await withRestApi.deleteAllShippingZones( false );
await merchant.login();
});
afterAll( async () => {
shopper.logout();
} );
it('add shipping zone for San Francisco with free Local pickup', async () => {
// Add a new shipping zone for San Francisco 94107, CA, US with Local pickup
await addShippingZoneAndMethod(shippingZoneNameSF, california, sanFranciscoZIP, 'local_pickup');

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

@ -25,6 +25,10 @@ const runMyAccountCreateAccountTest = () => {
await merchant.logout();
});
afterAll( async () => {
shopper.logout();
} );
it('can create a new account via my account', async () => {
await shopper.gotoMyAccount();
await page.waitForSelector('.woocommerce-form-register');

View File

@ -34,6 +34,10 @@ const runMyAccountPayOrderTest = () => {
await merchant.logout();
});
afterAll( async () => {
shopper.logout();
} );
it('allows customer to pay for their order in My Account', async () => {
await shopper.login();
await shopper.goToOrders();

View File

@ -15,6 +15,10 @@ const pages = [
const runMyAccountPageTest = () => {
describe('My account page', () => {
afterAll( async () => {
shopper.logout();
} );
it('allows customer to login', async () => {
await shopper.login();
expect(page).toMatch('Hello');

View File

@ -36,6 +36,10 @@ const runOrderEmailReceivingTest = () => {
await merchant.logout();
});
afterAll( async () => {
shopper.logout();
} );
it('should receive order email after purchasing an item', async () => {
await shopper.login();

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

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