[tests] Move API core tests as a suite in e2e-pw (#50024)

This commit is contained in:
Adrian Moldovan 2024-07-29 11:51:36 +01:00 committed by GitHub
parent 640312d0d1
commit c12cb6ccdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
304 changed files with 1033 additions and 3192 deletions

View File

@ -119,7 +119,7 @@ jobs:
with:
install: '${{ matrix.projectName }}...'
build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }}
build-type: ${{ ( ( matrix.testType == 'unit:php' || matrix.testType == 'api' ) && 'backend' ) || 'full' }}
build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }}
pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && matrix.testType == 'e2e' }}
pull-package-deps: '${{ matrix.projectName }}'

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Tests: moved api core tests as a suite in e2e-pw

View File

@ -51,10 +51,9 @@
"makepot": "composer run-script makepot",
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
"test": "pnpm test:unit",
"test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api",
"test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js",
"test:e2e-pw": "pnpm test:e2e:install && pnpm playwright test --config=tests/e2e-pw/envs/default/playwright.config.js",
"test:e2e": "pnpm test:e2e:install && pnpm test:e2e:with-env default",
"test:api": "pnpm test:e2e:default --project=api --workers 4",
"test:e2e": "pnpm test:e2e:default --project=ui",
"test:e2e:default": "pnpm test:e2e:install && pnpm test:e2e:with-env default",
"test:e2e:install": "pnpm playwright install chromium",
"test:e2e:blocks": "pnpm --filter='@woocommerce/block-library' test:e2e",
"test:e2e:with-env": "pnpm test:e2e:install && bash ./tests/e2e-pw/run-tests-with-env.sh",
@ -221,7 +220,7 @@
{
"name": "Core e2e tests",
"testType": "e2e",
"command": "test:e2e:with-env default",
"command": "test:e2e",
"shardingArguments": [
"--shard=1/6",
"--shard=2/6",
@ -365,7 +364,7 @@
{
"name": "Core e2e tests - HPOS disabled",
"testType": "e2e",
"command": "test:e2e:with-env default",
"command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@ -393,7 +392,7 @@
{
"name": "Core e2e tests - PHP 8.1",
"testType": "e2e",
"command": "test:e2e:with-env default",
"command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@ -421,7 +420,7 @@
{
"name": "Core e2e tests - WP latest-1",
"testType": "e2e",
"command": "test:e2e:with-env default",
"command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@ -449,7 +448,7 @@
{
"name": "Core API tests",
"testType": "api",
"command": "test:api-pw",
"command": "test:api",
"optional": false,
"changes": [
"client/admin/config/*.json",
@ -459,8 +458,6 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
"tests/api-core-tests/**",
"tests/e2e-pw/bin/**",
".wp-env.json"
],
"testEnv": {
@ -472,14 +469,14 @@
],
"report": {
"resultsBlobName": "core-api-report",
"resultsPath": "tests/api-core-tests/test-results",
"resultsPath": "tests/e2e-pw/test-results",
"allure": true
}
},
{
"name": "Core API tests - HPOS disabled",
"testType": "api",
"command": "test:api-pw",
"command": "test:api",
"optional": false,
"changes": [
"client/admin/config/*.json",
@ -489,7 +486,6 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
"tests/api-core-tests/**",
"tests/e2e-pw/bin/**",
".wp-env.json"
],
@ -504,7 +500,7 @@
},
"report": {
"resultsBlobName": "core-api-report-hpos-disabled",
"resultsPath": "tests/api-core-tests/test-results",
"resultsPath": "tests/e2e-pw/test-results",
"allure": true
}
},

View File

@ -1,13 +0,0 @@
module.exports = {
extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
rules: {
'jsdoc/check-tag-names': 'off',
'jest/no-test-callback': 'off',
camelcase: 'off',
'jest/no-disabled-tests': 'off',
'no-shadow': 'off',
'jest/no-identical-title': 'off',
'jest/no-standalone-expect': 'off',
'no-console': 'off',
},
};

View File

@ -1,498 +0,0 @@
# WooCommerce Core API Test Suite
This package contains automated API tests for WooCommerce, based on Playwright and `wp-env`. It supersedes the SuperTest based [api-core-tests package](https://www.npmjs.com/package/@woocommerce/api-core-tests) and e2e-environment [setup](../tests/e2e), which we will gradually deprecate.
## Table of contents
- [Pre-requisites](#pre-requisites)
- [Introduction](#introduction)
- [About the Environment](#about-the-environment)
- [Test Variables](#test-variables)
- [Guide for writing API tests](#guide-for-writing-api-tests)
- [What aspects of the API should we test?](#what-aspects-of-the-api-should-we-test)
- [Creating test structure](#creating-test-structure)
- [Test Data Setup/Teardown](#test-data-setupteardown)
- [Writing the test - A Quick Start Guide](#writing-the-test---a-quick-start-guide)
- [Examples](#examples)
- [Debugging tests](#debugging-tests)
- [Guide for using test reports](#guide-for-using-test-reports)
- [Viewing the Playwright HTML report](#viewing-the-playwright-html-report)
- [Viewing the Allure report](#viewing-the-allure-report)
## Pre-requisites
- Node.js ([Installation instructions](https://nodejs.org/en/download/))
- NVM ([Installation instructions](https://github.com/nvm-sh/nvm))
- PNPM ([Installation instructions](https://pnpm.io/installation))
- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/))
Note, that if you are on Mac and you install Docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary.
If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below.
### Introduction
WooCommerce's `api-core-tests` are powered by Playwright. The test site is spun up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime.
**Running tests for the first time:**
- `nvm use`
- `pnpm install`
- `pnpm --filter='@woocommerce/plugin-woocommerce' build`
- `cd plugins/woocommerce`
- `pnpm env:test`
- `pnpm test:api-pw`
To run the test again, re-create the environment to start with a fresh state
- `pnpm env:destroy`
- `pnpm env:test`
- `pnpm test:api-pw`
Other ways of running tests:
- `pnpm test:api-pw ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test file)
- `pnpm test:api-pw ./tests/api-core-tests/tests/hello` (running all tests in a single folder)
To see all options, run `cd plugins/woocommerce && pnpm playwright test --help`
## Environment variables
The following environment variables can be configured as shown in `.env.example`:
```
# Your site's base URL, not including a trailing slash
API_BASE_URL="https://mysite.com"
# The admin user's username or generated consumer key
USER_KEY=""
# The admin user's password or generated consumer secret
USER_SECRET=""
```
For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce/tests/api-core-tests` folder with the three required values described above. If any of these variables are configured they will override the values automatically set in the `playwright.config.js`
When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site.
For more information about authentication with the WooCommerce API, please see the [Authentication](https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#authentication) section in the WooCommerce REST API documentation.
### About the environment
The default values are:
- Latest stable WordPress version
- PHP 7.4
- MariaDB
- URL: `http://localhost:8086/`
- Admin credentials: `admin/password`
If you want to customize these, check the [Test Variables](#test-variables) section.
For more information how to configure the test environment for `wp-env`, please checkout the [documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/env) documentation.
### Test Variables
The test environment uses the following test variables:
```json
{
"url": "http://localhost:8086/",
"users": {
"admin": {
"username": "admin",
"password": "password"
},
"customer": {
"username": "customer",
"password": "password"
}
}
}
```
If you need to modify the port for your local test environment (eg. port is already in use) or use, edit [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). Depending on what environment tool you are using, you will need to also edit the respective `.json` file.
**Modify the port wp-env**
Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/.wp-env.json) and [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js).
**Modify port for e2e-environment**
Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).****
### Starting/stopping the environment
After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress.
- `pnpm env:down` to stop the environment
- `pnpm env:destroy` when you make changes to `.wp-env.json`
- `pnpm env:test` to spin up the test environment
## Guide for writing API tests
When writing new tests, a good source on how to get started is to reference the [existing tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/tests). Data that is required for the tests should be located in an equivalent file in the [data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/data) folder.
Good examples to reference are the `coupons` and `customers` tests. The [Quick Start Guide](#writing-the-test---a-quick-start-guide) below has the key steps to put together a test and examples of how those steps were implemented for `coupons` and `customers`.
The [Playwright documentation](https://playwright.dev/docs/intro) is a good source for finding out more details on the various methods used in the tests, including what is available to you when you write new tests. The [API testing](https://playwright.dev/docs/test-api-testing) section has a good example on how the [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/playwright.config.js) is used (see [Configuration](https://playwright.dev/docs/test-api-testing#configuration) section) and also how [Setup and Teardown](https://playwright.dev/docs/test-api-testing#setup-and-teardown) works.
Note: Playwright uses the [expect](https://jestjs.io/docs/expect) library for test assertions. This library provides a lot of matchers like `toEqual`, `toContain`, `toHaveLength` and many more. Examples of these are throughout the test files.
## What aspects of the API should we test?
Assuming that we have validated the API contract (inspected the spec/contract, made sure the endpoints are correctly named, resources and types reflect the object model and there is no missing/duplicate functionality) we are ready to test.
A test contains 3 different stages:
1. `precondition`
2. `action`
3. `validation`
and in the case of API testing, this equates to:
1. `data creation` - see [Test Data Setup Examples](#test-data-setup-examples) below
2. `send API request` - see [Request Examples](#request-examples) below
3. `response validation` - see [Validation Examples](#validation-examples) below
For each API request method (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`), the test would need to take the following actions:
1. Verify correct HTTP status code. For example, creating a resource should return 201 CREATED and un-permitted requests should return 403 FORBIDDEN, etc.
2. Verify response payload. Check the JSON body is valid and field names, types, and values are correct (i.e. check the response payload schema is implemented according to the specification) — including error responses.
3. Verify response headers where appropriate e.g. Some headers hold information related to search totals, pagination values etc. (see [Examples](#examples) below).
At a minimum, we want to ensure each possible CRUD operation can be applied and sufficient assertions have been validated based on the above.
A reasonable process would be:
1. Create an object using a `POST` request
2. Retrieve the created object using the `GET` request
3. Update the object using a `POST` request
4. Delete the object using a `DELETE`request
5. Test any batch create/update/delete operations (if applicable)
Additional tests can also be added to test the following general test scenarios:
- Basic positive tests (happy paths) - check basic functionality and the acceptance criteria of the API
- Extended positive testing with optional parameters - more thorough testing (can be used for testing a bug/updates/new functionality)
- Negative testing with valid input - expect to gracefully handle problem scenarios with valid user input e.g. trying to add an existing username
- Negative testing with invalid input - expect to gracefully handle problem scenarios with invalid user input e.g. trying to add a username which is null
The intention here is to validate that we get error responses when expected as per specification and the error status code and message are correct as per documentation.
## Creating test structure
The structure of the test serves as a skeleton for the test itself.
Each test file requires the `@playwright/test` module to be imported as follows:
```js
const { test, expect } = require( '@playwright/test' );
```
You can create a test by using the `test.describe()` and `test()` methods of Playwright:
- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe-1) - creates a block that groups together several related tests;
- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test.
Based on our example, the test skeleton would look as follows:
```js
test.describe( 'Coupons API tests', () => {
test( 'can create a coupon', async ( {request} ) => {
// test to create a coupon here
} );
test( 'can retrieve a coupon', async ( {request} ) => {
// test to retrieve a coupon here
} );
test( 'can update a coupon', async ( { request } ) => {
// test to update a coupon here
} );
} );
```
Note: you can also nest a `test.describe()` inside a `test.describe()`. Example:
```js
test.describe('Orders API tests: CRUD', () => {
let orderId; //test variable
test.describe('Create an order', () => {
test('can create a pending order by default', async ({request}) => {
//test code here
}
```
This allows you to further subgroup tests. When viewing the tests results locally, each test describe 'level' will be separated by `>` in the console as below:
`Orders API tests: CRUD Create an order can create a pending order by default`
## Test Data Setup/Teardown
You may need test data setup prior to the execution of your tests. If so, make sure it is removed after the execution of your tests. This can be achieved with any of the following methods, depending on the needs of the test:
- [`test.beforeAll()`](https://playwright.dev/docs/api/class-test#test-before-all) - runs before all the tests in file/group
- [`test.beforeEach()`](https://playwright.dev/docs/api/class-test#test-before-each) - runs before each test in file/group
- [`test.afterEach()`](https://playwright.dev/docs/api/class-test#test-after-each) - runs after each test in file/group
- [`test.afterAll()`](https://playwright.dev/docs/api/class-test#test-after-all) - runs after all the tests in file/group
## Writing the test - a Quick Start Guide
1. Ensure you have your authentication setup as mentioned in the [Environment Variables](#environment-variables) section above. i.e.
> For local setup, create a `.env` file
2. Create `test.js` file inside the tests directory
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js) and [`customers`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js)
3. Import `@playwright/test` module
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L1) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L1)
4. Group tests with `test.describe()` methods
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L10) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L20)
5. Add tests with `test()` methods
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L14) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L93)
6. Separate data where required into files in the `data` directory
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/data/coupon.js) and [`customers`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/data/customer.js)
7. Import data required by your tests
- Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L2) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L5)
8. After writing your tests, ensure all tests pass successfully with `pnpm test:api-pw`
If you have made updates to functionality that breaks the tests then the tests should be updated accordingly. Similarly, if there is new functionality added then new tests should be added.
## Examples
Below are examples in our `api-core-tests` including references and typical API test operations.
Playwright [configuration file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/playwright.config.js)
Test files [location](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/tests)
Data files [location](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/data)
### Test Data Setup Examples
Setup data with [test.beforeAll()](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L347)
```js
test.beforeAll( async ( { request } ) => {
// Create a coupon
const createCouponResponse = await request.post(
'/wp-json/wc/v3/coupons/',
{
data: testCoupon,
}
);
const createCouponResponseJSON = await createCouponResponse.json();
testCoupon.id = createCouponResponseJSON.id;
} );
```
Teardown data with [test.afterAll()](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L360)
```js
// Clean up created coupon and order
test.afterAll( async ( { request } ) => {
await request.delete( `/wp-json/wc/v3/coupons/${ testCoupon.id }`, {
data: { force: true },
} );
await request.delete( `/wp-json/wc/v3/orders/${ orderId }`, {
data: { force: true },
} );
} );
```
### Request Examples
`GET` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L44)
```js
//call API to get previously created coupon
const response = await request.get(
`/wp-json/wc/v3/coupons/${ couponId }`
);
```
`POST` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L63)
```js
//call API to update previously created coupon
const response = await request.post(
`/wp-json/wc/v3/coupons/${ couponId }`,
{
data: updatedCouponDetails,
}
);
```
`PUT` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/orders/order-complex.test.js#L220)
```js
//ensure tax calculations are enabled
await request.put(
'/wp-json/wc/v3/settings/general/woocommerce_calc_taxes',
{
data: {
value: 'yes',
},
}
);
```
`DELETE` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L80)
```js
//call API to delete previously created coupon
const response = await request.delete(
`/wp-json/wc/v3/coupons/${ couponId }`,
{
data: { force: true },
}
);
```
`BATCH` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L119)
```js
// Batch create 2 new coupons.
const batchCreatePayload = {
create: expectedCoupons,
};
// call API to batch create coupons
const batchCreateResponse = await request.post(
'wp-json/wc/v3/coupons/batch',
{
data: batchCreatePayload,
}
);
```
### Validation Examples
Verify [Status code](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L30)
```js
expect( response.status() ).toEqual( 201 );
```
Verify [Response payload](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L33)
```js
//validate the response data
expect( await response.json() ).toEqual(
expect.objectContaining( {
code: testCoupon.code,
amount: Number( coupon.amount ).toFixed( 2 ),
discount_type: coupon.discount_type,
} )
);
```
Verify [field names](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L142)
```js
expect( id ).toBeDefined();
expect( code ).toEqual( expectedCouponCode );
```
Verify [field types](https://github.com/woocommerce/woocommerce/blob/d19c20491e5a7ade64c8fd530f01e0f3f3f7e29c/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js#L85)
```js
expect(typeof responseJSON.id).toEqual('string');
```
Verify [field values](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L186)
```js
expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' );
```
Verify [field length](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L136)
```js
expect( actualCoupons ).toHaveLength( expectedCoupons.length );
```
Verify search [headers](https://github.com/woocommerce/woocommerce/blob/d19c20491e5a7ade64c8fd530f01e0f3f3f7e29c/plugins/woocommerce/tests/api-core-tests/tests/orders/orders.test.js#L2703)
```js
// Verify total page count.
expect( page1.headers()[ 'x-wp-total' ] ).toEqual(
ORDERS_COUNT.toString()
);
expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '3' );
```
Verify variable [not undefined](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L31)
```js
expect( couponId ).toBeDefined();
```
Verify [response contains an object](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L73)
```js
expect( await response.json() ).toEqual(
expect.objectContaining( updatedCouponDetails )
);
```
Verify [response contains an array containing an object](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L388)
```js
expect( responseJSON.coupon_lines[ 0 ].meta_data ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
key: 'coupon_data',
value: expect.objectContaining( {
code: testCoupon.code,
} ),
} ),
] )
);
```
## Debugging tests
The Playwright debugger won't work for the API tests as it is based around GUI interactions.
For now it is simple enough to add `console.log()` statements to output the values of your response/JSON/variables/status etc. Be sure to remove them when done ;)
You can also use the handy [REST API Log](https://wordpress.org/plugins/wp-rest-api-log/) plugin to see the API request information within WordPress. It displays the details, request headers, query params, body params, body content, response headers and response body information.
For the list of WooCommerce API endpoints, expected responses, and more, please see the [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/).
## Guide for using test reports
The tests would generate three kinds of reports after the run:
1. A Playwright HTML report.
1. A Playwright JSON report.
1. Allure results.
By default, they are saved inside the `test-results` folder. If you want to save them in a custom location, just assign the absolute path to the environment variables mentioned in the [Playwright](https://playwright.dev/docs/test-reporters) and [Allure-Playwright](https://www.npmjs.com/package/allure-playwright) documentation.
| Report | Default location | Environment variable for custom location |
| ----------- | ---------------- | ---------------------------------------- |
| Playwright HTML report | `test-results/playwright-report` | `PLAYWRIGHT_HTML_REPORT` |
| Playwright JSON report | `test-results/test-results.json` | `PLAYWRIGHT_JSON_OUTPUT_NAME` |
| Allure results | `test-results/allure-results` | `ALLURE_RESULTS_DIR` |
### Viewing the Playwright HTML report
Use the `playwright show-report $PATH_TO_PLAYWRIGHT_HTML_REPORT` command to open the report. For example, assuming that you're at the root of the WooCommerce monorepo, and that you did not specify a custom location for the report, you would use the following commands:
```bash
cd plugins/woocommerce
pnpm exec playwright show-report tests/api-core-tests/test-results/playwright-report
```
For more details about the Playwright HTML report, see their [HTML Reporter](https://playwright.dev/docs/test-reporters#html-reporter) documentation.
### Viewing the Allure report
This assumes that you're already familiar with reports generated by the [Allure Framework](https://github.com/allure-framework), particularly:
- What the `allure-results` and `allure-report` folders are, and how they're different from each other.
- Allure commands like `allure generate` and `allure open`.
Use the `allure generate` command to generate an HTML report from the `allure-results` directory created at the end of the test run. Then, use the `allure open` command to open it on your browser. For example, assuming that:
- You're at the root of the WooCommerce monorepo
- You did not specify a custom location for `allure-results` (you did not assign a value to `ALLURE_RESULTS_DIR`)
- You want to generate the `allure-report` folder in `plugins/woocommerce/tests/api-core-tests/test-results`
Then you would need to use the following commands:
```bash
cd plugins/woocommerce
pnpm exec allure generate --clean tests/api-core-tests/test-results/allure-results --output tests/api-core-tests/test-results/allure-report
pnpm exec allure open tests/api-core-tests/test-results/allure-report
```
A browser window should open the Allure report.
If you're using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) however, you might get this message right after running the `allure open` command:
```
Starting web server...
2022-12-09 18:52:01.323:INFO::main: Logging initialized @286ms to org.eclipse.jetty.util.log.StdErrLog
Can not open browser because this capability is not supported on your platform. You can use the link below to open the report manually.
Server started at <http://127.0.1.1:38917/>. Press <Ctrl+C> to exit
```
In this case, take note of the port number (38917 in the example above) and then use it to navigate to `http://localhost`. Taking the example above, you should be able to view the Allure report on http://localhost:38917.
To know more about the allure-playwright integration, see their [GitHub documentation](https://github.com/allure-framework/allure-js/tree/master/packages/allure-playwright).

View File

@ -1,122 +0,0 @@
const { UPDATE_WC, USER_KEY, USER_SECRET } = process.env;
const { test: setup, expect } = require( '@playwright/test' );
const fs = require( 'fs' );
const { downloadWooCommerceRelease } = require( './utils/plugin-utils' );
const pluginEndpoint = '/wp-json/wp/v2/plugins/woocommerce/woocommerce';
let zipPath;
setup( `Setup remote test site`, async ( { page, request } ) => {
setup.setTimeout( 5 * 60 * 1000 );
await setup.step( `Download WooCommerce build zip`, async () => {
zipPath = await downloadWooCommerceRelease( { request } );
} );
await setup.step( 'Login to wp-admin', async () => {
const Username = 'Username or Email Address';
const Password = 'Password';
const Log_In = 'Log In';
const Dashboard = 'Dashboard';
// Need to wait until network idle. Otherwise, Password field gets auto-cleared after typing password in.
await page.goto( '/wp-admin', { waitUntil: 'networkidle' } );
await page.getByLabel( Username ).fill( USER_KEY );
await page.getByLabel( Password, { exact: true } ).fill( USER_SECRET );
await page.getByRole( 'button', { name: Log_In } ).click();
await expect(
page
.locator( '#menu-dashboard' )
.getByRole( 'link', { name: Dashboard } )
).toBeVisible();
} );
const installed = await setup.step(
`See if there's a WooCommerce plugin installed`,
async () => {
const response = await request.get( pluginEndpoint );
const isOK = response.ok();
const status = response.status();
// Fast-fail if response was neither OK nor 404.
expect( isOK || status === 404 ).toEqual( true );
return isOK;
}
);
await setup.step(
`Deactivate currently installed WooCommerce version`,
async () => {
if ( ! installed ) {
return;
}
const options = {
data: {
status: 'inactive',
},
};
const response = await request.put( pluginEndpoint, options );
expect( response.ok() ).toBeTruthy();
}
);
await setup.step(
`Delete currently installed WooCommerce version`,
async () => {
if ( ! installed ) {
return;
}
const response = await request.delete( pluginEndpoint );
expect( response.ok() ).toBeTruthy();
}
);
await setup.step( `Install WooCommerce ${ UPDATE_WC }`, async () => {
const Upload_Plugin = 'Upload Plugin';
const Plugin_zip_file = 'Plugin zip file';
const Install_Now = 'Install Now';
const Activate_Plugin = 'Activate Plugin';
const timeout = 3 * 60 * 1000;
await page.goto( '/wp-admin/plugin-install.php' );
await page.getByRole( 'button', { name: Upload_Plugin } ).click();
await page.getByLabel( Plugin_zip_file ).setInputFiles( zipPath );
await page.getByRole( 'button', { name: Install_Now } ).click();
await expect(
page.getByRole( 'link', { name: Activate_Plugin } )
).toBeVisible( { timeout } );
} );
await setup.step( `Activate WooCommerce`, async () => {
const options = {
data: {
status: 'active',
},
};
const response = await request.put( pluginEndpoint, options );
expect( response.ok() ).toBeTruthy();
} );
await setup.step( `Verify WooCommerce version was installed`, async () => {
const response = await request.get( pluginEndpoint );
const { status, version } = await response.json();
expect( status ).toEqual( 'active' );
expect( version ).toEqual( UPDATE_WC );
} );
await setup.step( `Verify WooCommerce database version`, async () => {
const response = await request.get( '/wp-json/wc/v3/system_status' );
const { database } = await response.json();
const { wc_database_version } = database;
const [ major, minor ] = UPDATE_WC.split( '.' );
const pattern = new RegExp( `^${ major }\.${ minor }` );
expect( wc_database_version ).toMatch( pattern );
} );
await setup.step( `Delete zip`, async () => {
fs.unlinkSync( zipPath );
} );
} );

View File

@ -1,26 +0,0 @@
const defaultConfig = require( './playwright.config' );
const { devices } = require( '@playwright/test' );
// Global setup will be done through the 'Setup' project, not through the `globalSetup` property
delete defaultConfig[ 'globalSetup' ];
/**
* @type {import('@playwright/test').PlaywrightTestConfig}
*/
const config = {
...defaultConfig,
projects: [
{
name: 'Setup',
testDir: './',
testMatch: 'ci-release.global-setup.js',
use: { ...devices[ 'Desktop Chrome' ] },
},
{
name: 'API tests',
dependencies: [ 'Setup' ],
},
],
};
module.exports = config;

View File

@ -1,46 +0,0 @@
/**
* This file contains objects that can be used as test data for scenarios around creating, retrieivng, updating, and deleting customers.
*
* For more details on the Product properties, see:
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#customers
*
*/
/**
* A customer
*/
const customer = {
email: "john.doe@example.com",
first_name: "John",
last_name: "Doe",
username: "john.doe",
billing: {
first_name: "John",
last_name: "Doe",
company: "",
address_1: "969 Market",
address_2: "",
city: "San Francisco",
state: "CA",
postcode: "94103",
country: "US",
email: "john.doe@example.com",
phone: "(555) 555-5555"
},
shipping: {
first_name: "John",
last_name: "Doe",
company: "",
address_1: "969 Market",
address_2: "",
city: "San Francisco",
state: "CA",
postcode: "94103",
country: "US"
}
};
module.exports = {
customer,
};

View File

@ -1,421 +0,0 @@
/**
* A standard tax rate.
*
* For more details on the tax rate properties, see:
*
* https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rate-properties
*
*/
const standardTaxRate = {
name: 'Standard Rate',
rate: '10.0000',
class: 'standard',
};
const reducedTaxRate = {
name: 'Reduced Rate',
rate: '1.0000',
class: 'reduced-rate',
};
const zeroTaxRate = {
name: 'Zero Rate',
rate: '0.0000',
class: 'zero-rate',
};
const getTaxRateExamples = () => {
return { standardTaxRate, reducedTaxRate, zeroTaxRate };
};
const allUSTaxesExample = [
{
country: "US",
state: "AL",
rate: "4.0000",
name: "State Tax",
shipping: false,
order: 1
},
{
country: "US",
state: "AZ",
rate: "5.6000",
name: "State Tax",
shipping: false,
order: 2
},
{
country: "US",
state: "AR",
rate: "6.5000",
name: "State Tax",
shipping: true,
order: 3
},
{
country: "US",
state: "CA",
rate: "7.5000",
name: "State Tax",
shipping: false,
order: 4
},
{
country: "US",
state: "CO",
rate: "2.9000",
name: "State Tax",
shipping: false,
order: 5
},
{
country: "US",
state: "CT",
rate: "6.3500",
name: "State Tax",
shipping: true,
order: 6
},
{
country: "US",
state: "DC",
rate: "5.7500",
name: "State Tax",
shipping: true,
order: 7
},
{
country: "US",
state: "FL",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 8
},
{
country: "US",
state: "GA",
rate: "4.0000",
name: "State Tax",
shipping: true,
order: 9
},
{
country: "US",
state: "GU",
rate: "4.0000",
name: "State Tax",
shipping: false,
order: 10
},
{
country: "US",
state: "HI",
rate: "4.0000",
name: "State Tax",
shipping: true,
order: 11
},
{
country: "US",
state: "ID",
rate: "6.0000",
name: "State Tax",
shipping: false,
order: 12
},
{
country: "US",
state: "IL",
rate: "6.2500",
name: "State Tax",
shipping: false,
order: 13
},
{
country: "US",
state: "IN",
rate: "7.0000",
name: "State Tax",
shipping: false,
order: 14
},
{
country: "US",
state: "IA",
rate: "6.0000",
name: "State Tax",
shipping: false,
order: 15
},
{
country: "US",
state: "KS",
rate: "6.1500",
name: "State Tax",
shipping: true,
order: 16
},
{
country: "US",
state: "KY",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 17
},
{
country: "US",
state: "LA",
rate: "4.0000",
name: "State Tax",
shipping: false,
order: 18
},
{
country: "US",
state: "ME",
rate: "5.5000",
name: "State Tax",
shipping: false,
order: 19
},
{
country: "US",
state: "MD",
rate: "6.0000",
name: "State Tax",
shipping: false,
order: 20
},
{
country: "US",
state: "MA",
rate: "6.2500",
name: "State Tax",
shipping: false,
order: 21
},
{
country: "US",
state: "MI",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 22
},
{
country: "US",
state: "MN",
rate: "6.8750",
name: "State Tax",
shipping: true,
order: 23
},
{
country: "US",
state: "MS",
rate: "7.0000",
name: "State Tax",
shipping: true,
order: 24
},
{
country: "US",
state: "MO",
rate: "4.2250",
name: "State Tax",
shipping: false,
order: 25
},
{
country: "US",
state: "NE",
rate: "5.5000",
name: "State Tax",
shipping: true,
order: 26
},
{
country: "US",
state: "NV",
rate: "6.8500",
name: "State Tax",
shipping: false,
order: 27
},
{
country: "US",
state: "NJ",
rate: "7.0000",
name: "State Tax",
shipping: true,
order: 28
},
{
country: "US",
state: "NM",
rate: "5.1250",
name: "State Tax",
shipping: true,
order: 29
},
{
country: "US",
state: "NY",
rate: "4.0000",
name: "State Tax",
shipping: true,
order: 30
},
{
country: "US",
state: "NC",
rate: "4.7500",
name: "State Tax",
shipping: true,
order: 31
},
{
country: "US",
state: "ND",
rate: "5.0000",
name: "State Tax",
shipping: true,
order: 32
},
{
country: "US",
state: "OH",
rate: "5.7500",
name: "State Tax",
shipping: true,
order: 33
},
{
country: "US",
state: "OK",
rate: "4.5000",
name: "State Tax",
shipping: false,
order: 34
},
{
country: "US",
state: "PA",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 35
},
{
country: "US",
state: "PR",
rate: "6.0000",
name: "State Tax",
shipping: false,
order: 36
},
{
country: "US",
state: "RI",
rate: "7.0000",
name: "State Tax",
shipping: false,
order: 37
},
{
country: "US",
state: "SC",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 38
},
{
country: "US",
state: "SD",
rate: "4.0000",
name: "State Tax",
shipping: true,
order: 39
},
{
country: "US",
state: "TN",
rate: "7.0000",
name: "State Tax",
shipping: true,
order: 40
},
{
country: "US",
state: "TX",
rate: "6.2500",
name: "State Tax",
shipping: true,
order: 41
},
{
country: "US",
state: "UT",
rate: "5.9500",
name: "State Tax",
shipping: false,
order: 42
},
{
country: "US",
state: "VT",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 43
},
{
country: "US",
state: "VA",
rate: "5.3000",
name: "State Tax",
shipping: false,
order: 44
},
{
country: "US",
state: "WA",
rate: "6.5000",
name: "State Tax",
shipping: true,
order: 45
},
{
country: "US",
state: "WV",
rate: "6.0000",
name: "State Tax",
shipping: true,
order: 46
},
{
country: "US",
state: "WI",
rate: "5.0000",
name: "State Tax",
shipping: true,
order: 47
},
{
country: "US",
state: "WY",
rate: "4.0000",
name: "State Tax",
shipping: true,
order: 48
}
];
module.exports = {
getTaxRateExamples,
allUSTaxesExample
};

View File

@ -1,251 +0,0 @@
const { DISABLE_HPOS, GITHUB_TOKEN, UPDATE_WC } = process.env;
const { downloadZip, deleteZip } = require( './utils/plugin-utils' );
const axios = require( 'axios' ).default;
const playwrightConfig = require( './playwright.config' );
const { site } = require( './utils' );
/**
*
* @param {import('@playwright/test').FullConfig} config
*/
module.exports = async ( config ) => {
// If API_BASE_URL is configured and doesn't include localhost, running on daily host
if (
process.env.API_BASE_URL &&
! process.env.API_BASE_URL.includes( 'localhost' )
) {
const { chromium, expect } = require( '@playwright/test' );
const { baseURL, userAgent } = config.projects[ 0 ].use;
const contextOptions = { baseURL, userAgent };
const browser = await chromium.launch();
const setupContext = await browser.newContext( contextOptions );
const setupPage = await setupContext.newPage();
const getWCDownloadURL = async () => {
const requestConfig = {
method: 'get',
url: 'https://api.github.com/repos/woocommerce/woocommerce/releases',
headers: {
Accept: 'application/vnd.github+json',
},
params: {
per_page: 100,
},
};
if ( GITHUB_TOKEN ) {
requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`;
}
const response = await axios( requestConfig ).catch( ( error ) => {
if ( error.response ) {
console.log( error.response.data );
}
throw new Error( error.message );
} );
const releaseWithTagName = response.data.find(
( { tag_name } ) => tag_name === UPDATE_WC
);
if ( ! releaseWithTagName ) {
throw new Error(
`No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.`
);
}
const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) =>
name.match( /^woocommerce(-trunk-nightly)?\.zip$/ )
);
if ( wcZipAsset ) {
return GITHUB_TOKEN
? wcZipAsset.url
: wcZipAsset.browser_download_url;
}
throw new Error(
`WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.`
);
};
const url = await getWCDownloadURL();
const params = { url };
if ( GITHUB_TOKEN ) {
params.authorizationToken = GITHUB_TOKEN;
}
const woocommerceZipPath = await downloadZip( params );
let adminLoggedIn = false;
let pluginActive = false;
console.log( '--------------------------------------' );
console.log( 'Running daily tests, resetting site...' );
console.log( '--------------------------------------' );
const adminRetries = 5;
for ( let i = 0; i < adminRetries; i++ ) {
try {
console.log( 'Trying to log-in as admin...' );
await setupPage.goto( '/wp-admin' );
await setupPage
.locator( 'input[name="log"]' )
.fill( process.env.USER_KEY );
await setupPage
.locator( 'input[name="pwd"]' )
.fill( process.env.USER_SECRET );
await setupPage.locator( 'text=Log In' ).click();
await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
'Dashboard'
);
console.log( 'Logged-in as admin successfully.' );
adminLoggedIn = true;
break;
} catch ( e ) {
console.log(
`Admin log-in failed, Retrying... ${ i }/${ adminRetries }`
);
console.log( e );
}
}
if ( ! adminLoggedIn ) {
console.error(
'Cannot proceed api test, as admin login failed. Please check if the test site has been setup correctly.'
);
process.exit( 1 );
}
await setupPage.goto( 'wp-admin/plugins.php' );
await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
'Plugins'
);
console.log( 'Deactivating WooCommerce Plugin...' );
await setupPage.locator( '#deactivate-woocommerce' ).click();
await expect( setupPage.locator( 'div#message' ) ).toHaveText(
'Plugin deactivated.Dismiss this notice.'
);
console.log( 'Deleting WooCommerce Plugin...' );
setupPage.on( 'dialog', ( dialog ) => dialog.accept() );
await setupPage.locator( '#delete-woocommerce' ).click();
await expect( setupPage.locator( '#woocommerce-deleted' ) ).toHaveText(
'WooCommerce was successfully deleted.'
);
for ( let i = 0; i < adminRetries; i++ ) {
try {
console.log( 'Reinstalling WooCommerce Plugin...' );
await setupPage.goto( 'wp-admin/plugin-install.php' );
await setupPage.locator( 'a.upload-view-toggle' ).click();
await expect(
setupPage.locator( 'p.install-help' )
).toBeVisible();
await expect(
setupPage.locator( 'p.install-help' )
).toContainText(
'If you have a plugin in a .zip format, you may install or update it by uploading it here'
);
const [ fileChooser ] = await Promise.all( [
setupPage.waitForEvent( 'filechooser' ),
setupPage.locator( '#pluginzip' ).click(),
] );
await fileChooser.setFiles( woocommerceZipPath );
console.log( 'Uploading nightly build...' );
await setupPage
.locator( '#install-plugin-submit' )
.click( { timeout: 60000 } );
await setupPage.waitForLoadState( 'networkidle', {
timeout: 60000,
} );
await expect(
setupPage.getByRole(
'link',
{ name: 'Activate Plugin' },
{ timeout: 60000 }
)
).toBeVisible();
console.log( 'Activating Plugin...' );
await setupPage
.getByRole( 'link', { name: 'Activate Plugin' } )
.click( { timeout: 60000 } );
pluginActive = true;
break;
} catch ( e ) {
console.log(
`Installing and activating plugin failed, Retrying... ${ i }/${ adminRetries }`
);
console.log( e );
}
}
if ( ! pluginActive ) {
console.error(
'Cannot proceed api test, as installing WC failed. Please check if the test site has been setup correctly.'
);
process.exit( 1 );
}
console.log( 'WooCommerce Re-installed.' );
await expect(
setupPage.getByRole( 'heading', { name: 'Welcome to Woo!' } )
).toBeVisible();
await deleteZip( woocommerceZipPath );
// Might need to update the database
await setupPage.goto( 'wp-admin/plugins.php' );
const updateButton = setupPage.locator(
'text=Update WooCommerce Database'
);
const updateCompleteMessage = setupPage.locator(
'text=WooCommerce database update complete.'
);
await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
'Plugins'
);
if ( await updateButton.isVisible() ) {
console.log( 'Database update button present. Click it.' );
await updateButton.click( { timeout: 60000 } );
await expect( updateCompleteMessage ).toBeVisible();
} else {
console.log( 'No DB update needed' );
}
} else {
// running on localhost using wp-env so ensure HPOS is set if DISABLE_HPOS env variable is passed
if ( DISABLE_HPOS ) {
let hposConfigured = false;
const value = DISABLE_HPOS === '1' ? 'no' : 'yes';
try {
const auth = {
username: playwrightConfig.userKey,
password: playwrightConfig.userSecret,
};
const hposResponse = await axios.post(
playwrightConfig.use.baseURL +
'/wp-json/wc/v3/settings/advanced/woocommerce_custom_orders_table_enabled',
{ value },
{ auth }
);
if ( hposResponse.data.value === value ) {
console.log(
`HPOS Switched ${
value === 'yes' ? 'on' : 'off'
} successfully`
);
hposConfigured = true;
}
} catch ( error ) {
console.log( 'HPOS setup failed.' );
console.log( error );
process.exit( 1 );
}
if ( ! hposConfigured ) {
console.error(
'Cannot proceed to api tests, HPOS configuration failed. Please check if the correct DISABLE_HPOS value was used and the test site has been setup correctly.'
);
process.exit( 1 );
}
}
await site.useCartCheckoutShortcodes( config );
}
};

View File

@ -1,71 +0,0 @@
const { devices } = require( '@playwright/test' );
require( 'dotenv' ).config( { path: __dirname + '/.env' } );
const { API_BASE_URL, CI, DEFAULT_TIMEOUT_OVERRIDE, USER_KEY, USER_SECRET } =
process.env;
const baseURL = API_BASE_URL ?? 'http://localhost:8086';
const userKey = USER_KEY ?? 'admin';
const userSecret = USER_SECRET ?? 'password';
const base64auth = btoa( `${ userKey }:${ userSecret }` );
const config = {
userKey,
userSecret,
timeout: DEFAULT_TIMEOUT_OVERRIDE
? Number( DEFAULT_TIMEOUT_OVERRIDE )
: 90 * 1000,
expect: { timeout: 20 * 1000 },
globalSetup: require.resolve( './global-setup' ),
outputDir: './test-results/report',
testDir: 'tests',
retries: CI ? 4 : 2,
workers: 4,
reporter: [
[ 'list' ],
[
'html',
{
outputFolder:
process.env.PLAYWRIGHT_HTML_REPORT ??
'./test-results/playwright-report',
open: CI ? 'never' : 'always',
},
],
[
'allure-playwright',
{
outputFolder:
process.env.ALLURE_RESULTS_DIR ??
'./tests/api-core-tests/test-results/allure-results',
},
],
[
'json',
{
outputFile:
process.env.PLAYWRIGHT_JSON_OUTPUT_NAME ??
'./test-results/test-results.json',
},
],
],
use: {
screenshot: 'only-on-failure',
video: 'on-first-retry',
trace: 'retain-on-failure',
viewport: { width: 1280, height: 720 },
baseURL,
extraHTTPHeaders: {
// Add authorization token to all requests.
Authorization: `Basic ${ base64auth }`,
},
},
projects: [
{
name: 'Chrome',
use: { ...devices[ 'Desktop Chrome' ] },
},
],
};
module.exports = config;

View File

@ -1,407 +0,0 @@
const {
test,
expect
} = require('@playwright/test');
/**
* Tests for the WooCommerce Refunds API.
*
* @group api
* @group reports
*
*/
test.describe('Reports API tests', () => {
test('can view all reports', async ({
request
}) => {
// call API to retrieve the reports
const response = await request.get('/wp-json/wc/v3/reports');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "sales",
"description": "List of sales reports.",
})
]));
});
test('can view sales reports', async ({
request
}) => {
// call API to retrieve the sales reports
const response = await request.get('/wp-json/wc/v3/reports/sales');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0!
const yyyy = today.getFullYear();
const dateString = yyyy + '-' + mm + '-' + dd;
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"total_sales": expect.any(String),
"net_sales": expect.any(String),
"average_sales": expect.any(String),
"total_orders": expect.any(Number),
"total_items": expect.any(Number),
"total_tax": expect.any(String),
"total_shipping": expect.any(String),
"total_refunds": expect.any(Number),
"total_discount": expect.any(String),
"totals_grouped_by": "day",
"totals": expect.objectContaining({
[dateString]: {
"sales": expect.any(String),
"orders": expect.any(Number),
"items": expect.any(Number),
"tax": expect.any(String),
"shipping": expect.any(String),
"discount": expect.any(String),
"customers": expect.any(Number)
}
}),
"total_customers": expect.any(Number),
})
]));
});
test('can view top sellers reports', async ({
request
}) => {
// call API to retrieve the top sellers
const response = await request.get('/wp-json/wc/v3/reports/top_sellers');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([]));
});
test('can view coupons totals', async ({
request
}) => {
// call API to retrieve the coupons totals
const response = await request.get('/wp-json/wc/v3/reports/coupons/totals');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "percent",
"name": "Percentage discount",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "fixed_cart",
"name": "Fixed cart discount",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "fixed_product",
"name": "Fixed product discount",
"total": expect.any(Number)
})
]));
});
test('can view customers totals', async ({
request
}) => {
// call API to retrieve the customers totals
const response = await request.get('/wp-json/wc/v3/reports/customers/totals');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "paying",
"name": "Paying customer",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "non_paying",
"name": "Non-paying customer",
"total": expect.any(Number)
})
]));
});
test('can view orders totals', async ({
request
}) => {
// call API to retrieve the orders totals
const response = await request.get('/wp-json/wc/v3/reports/orders/totals');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "pending",
"name": "Pending payment",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "processing",
"name": "Processing",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "on-hold",
"name": "On hold",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "completed",
"name": "Completed",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "cancelled",
"name": "Cancelled",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "refunded",
"name": "Refunded",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "failed",
"name": "Failed",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "checkout-draft",
"name": "Draft",
"total": expect.any(Number)
})
]));
});
test('can view products totals', async ({
request
}) => {
// call API to retrieve the products totals
const response = await request.get('/wp-json/wc/v3/reports/products/totals');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "external",
"name": "External/Affiliate product",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "grouped",
"name": "Grouped product",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "simple",
"name": "Simple product",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "variable",
"name": "Variable product",
"total": expect.any(Number)
})
]));
});
test('can view reviews totals', async ({
request
}) => {
// call API to retrieve the reviews totals
const response = await request.get('/wp-json/wc/v3/reports/reviews/totals');
const responseJSON = await response.json();
expect(response.status()).toEqual(200);
expect(Array.isArray(responseJSON)).toBe(true);
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "rated_1_out_of_5",
"name": "Rated 1 out of 5",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "rated_2_out_of_5",
"name": "Rated 2 out of 5",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "rated_3_out_of_5",
"name": "Rated 3 out of 5",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "rated_4_out_of_5",
"name": "Rated 4 out of 5",
"total": expect.any(Number)
})
]));
expect(responseJSON).toEqual(
expect.arrayContaining([
expect.objectContaining({
"slug": "rated_5_out_of_5",
"name": "Rated 5 out of 5",
"total": expect.any(Number)
})
]));
});
});

View File

@ -1,431 +0,0 @@
const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
const { async } = require( 'regenerator-runtime' );
const config = require( '../playwright.config' );
let api;
// Ensure that global-setup.js runs before creating api client
if ( process.env.CONSUMER_KEY && process.env.CONSUMER_SECRET ) {
api = new wcApi( {
url: config.use.baseURL,
consumerKey: process.env.CONSUMER_KEY,
consumerSecret: process.env.CONSUMER_SECRET,
version: 'wc/v3',
} );
}
/**
* Allow explicit construction of api client.
*/
const constructWith = ( consumerKey, consumerSecret ) => {
api = new wcApi( {
url: config.use.baseURL,
consumerKey,
consumerSecret,
version: 'wc/v3',
} );
};
const throwCustomError = (
error,
customMessage = 'Something went wrong. See details below.'
) => {
throw new Error(
customMessage
.concat(
`\nResponse status: ${ error.response.status } ${ error.response.statusText }`
)
.concat(
`\nResponse headers:\n${ JSON.stringify(
error.response.headers,
null,
2
) }`
).concat( `\nResponse data:\n${ JSON.stringify(
error.response.data,
null,
2
) }
` )
);
};
const update = {
storeDetails: async ( store ) => {
const res = await api.post( 'settings/general/batch', {
update: [
{
id: 'woocommerce_store_address',
value: store.address,
},
{
id: 'woocommerce_store_city',
value: store.city,
},
{
id: 'woocommerce_default_country',
value: store.countryCode,
},
{
id: 'woocommerce_store_postcode',
value: store.zip,
},
],
} );
},
enableCashOnDelivery: async () => {
await api.put( 'payment_gateways/cod', {
enabled: true,
} );
},
disableCashOnDelivery: async () => {
await api.put( 'payment_gateways/cod', {
enabled: false,
} );
},
};
const get = {
coupons: async ( params ) => {
const response = await api
.get( 'coupons', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all coupons.'
);
} );
return response.data;
},
defaultCountry: async () => {
const response = await api.get(
'settings/general/woocommerce_default_country'
);
const code = response.data.default;
return code;
},
orders: async ( params ) => {
const response = await api
.get( 'orders', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all orders.'
);
} );
return response.data;
},
products: async ( params ) => {
const response = await api
.get( 'products', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all products.'
);
} );
return response.data;
},
productAttributes: async ( params ) => {
const response = await api
.get( 'products/attributes', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all product attributes.'
);
} );
return response.data;
},
productCategories: async ( params ) => {
const response = await api
.get( 'products/categories', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all product categories.'
);
} );
return response.data;
},
productTags: async ( params ) => {
const response = await api
.get( 'products/tags', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all product tags.'
);
} );
return response.data;
},
shippingClasses: async ( params ) => {
const response = await api
.get( 'products/shipping_classes', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all shipping classes.'
);
} );
return response.data;
},
shippingZones: async ( params ) => {
const response = await api
.get( 'shipping/zones', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all shipping zones.'
);
} );
return response.data;
},
shippingZoneMethods: async ( shippingZoneId ) => {
const response = await api
.get( `shipping/zones/${ shippingZoneId }/methods` )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
`Something went wrong when trying to list all shipping methods in shipping zone ${ shippingZoneId }.`
);
} );
return response.data;
},
taxClasses: async () => {
const response = await api
.get( 'taxes/classes' )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all tax classes.'
);
} );
return response.data;
},
taxRates: async ( params ) => {
const response = await api
.get( 'taxes', params )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when trying to list all tax rates.'
);
} );
return response.data;
},
};
const create = {
product: async ( product ) => {
const response = await api.post( 'products', product );
return response.data.id;
},
/**
* Batch create product variations.
*
* @see {@link [Batch update product variations](https://woocommerce.github.io/woocommerce-rest-api-docs/#batch-update-product-variations)}
* @param {number|string} productId Product ID to add variations to
* @param {object[]} variations Array of variations to add. See [Product variation properties](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variation-properties)
* @returns {Promise<number[]>} Array of variation ID's.
*/
productVariations: async ( productId, variations ) => {
const response = await api.post(
`products/${ productId }/variations/batch`,
{
create: variations,
}
);
return response.data.create.map( ( { id } ) => id );
},
};
const deletePost = {
coupons: async ( ids ) => {
const res = await api
.post( 'coupons/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting coupons.'
);
} );
return res.data;
},
product: async ( id ) => {
await api.delete( `products/${ id }`, {
force: true,
} );
},
products: async ( ids ) => {
const res = await api
.post( 'products/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting products.'
);
} );
return res.data;
},
productAttributes: async ( id ) => {
const res = await api
.post( 'products/attributes/batch', { delete: id } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting product attributes.'
);
} );
return res.data;
},
productCategories: async ( ids ) => {
const res = await api
.post( 'products/categories/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting product categories.'
);
} );
return res.data;
},
productTags: async ( ids ) => {
const res = await api
.post( 'products/tags/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting product tags.'
);
} );
return res.data;
},
order: async ( id ) => {
await api.delete( `orders/${ id }`, {
force: true,
} );
},
orders: async ( ids ) => {
const res = await api
.post( 'orders/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting orders.'
);
} );
return res.data;
},
shippingClasses: async ( ids ) => {
const res = await api
.post( 'products/shipping_classes/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting shipping classes.'
);
} );
return res.data;
},
shippingZone: async ( id ) => {
const res = await api
.delete( `shipping/zones/${ id }`, {
force: true,
} )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when deleting shipping zone.'
);
} );
return res.data;
},
shippingZoneMethod: async ( shippingZoneId, shippingMethodId ) => {
const res = await api
.delete(
`shipping/zones/${ shippingZoneId }/methods/${ shippingMethodId }`,
{
force: true,
}
)
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when deleting shipping zone method.'
);
} );
return res.data;
},
taxClass: async ( slug ) => {
const res = await api
.delete( `taxes/classes/${ slug }`, {
force: true,
} )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
`Something went wrong when deleting tax class ${ slug }.`
);
} );
return res.data;
},
taxRates: async ( ids ) => {
const res = await api
.post( 'taxes/batch', { delete: ids } )
.then( ( response ) => response )
.catch( ( error ) => {
throwCustomError(
error,
'Something went wrong when batch deleting tax rates.'
);
} );
return res.data;
},
};
module.exports = {
update,
get,
create,
deletePost,
constructWith,
};

View File

@ -1,6 +0,0 @@
const api = require( './api' );
const site = require( './site' );
module.exports = {
api,
site,
};

View File

@ -1,366 +0,0 @@
const { APIRequest, expect } = require( '@playwright/test' );
const axios = require( 'axios' ).default;
const fs = require( 'fs' );
const path = require( 'path' );
const { promisify } = require( 'util' );
const execAsync = promisify( require( 'child_process' ).exec );
/**
* GitHub [release asset](https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28) object.
* @typedef {Object} ReleaseAsset
* @property {string} name
* @property {string} url
*/
/**
* GitHub [release](https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28) object.
* @typedef {Object} Release
* @property {ReleaseAsset[]} assets
* @property {string} tag_name
* @property {string} name
*/
/**
* Encode basic auth username and password to be used in HTTP Authorization header.
*
* @param {string} username
* @param {string} password
* @returns Base64-encoded string
*/
const encodeCredentials = ( username, password ) => {
return Buffer.from( `${ username }:${ password }` ).toString( 'base64' );
};
/**
* Deactivate and delete a plugin specified by the given `slug` using the WordPress API.
*
* @param {object} params
* @param {APIRequest} params.request
* @param {string} params.baseURL
* @param {string} params.slug
* @param {string} params.username
* @param {string} params.password
*/
export const deletePlugin = async ( {
request,
baseURL,
slug,
username,
password,
} ) => {
// Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`.
const apiContext = await request.newContext( {
baseURL,
extraHTTPHeaders: {
Authorization: `Basic ${ encodeCredentials( username, password ) }`,
cookie: '',
},
} );
const listPluginsResponse = await apiContext.get(
`/wp-json/wp/v2/plugins`,
{
failOnStatusCode: true,
}
);
const pluginsList = await listPluginsResponse.json();
const pluginToDelete = pluginsList.find(
( { textdomain } ) => textdomain === slug
);
// If installed, get its `plugin` value and use it to deactivate and delete it.
if ( pluginToDelete ) {
const { plugin } = pluginToDelete;
const requestURL = `/wp-json/wp/v2/plugins/${ plugin }`;
await apiContext.put( requestURL, {
data: { status: 'inactive' },
} );
await apiContext.delete( requestURL );
}
};
/**
* Download the zip file from a remote location.
*
* @param {object} param
* @param {string} param.url
* @param {string} param.repository
* @param {string} param.authorizationToken
* @param {boolean} param.prerelease
* @param {string} param.downloadDir
*
* @param {string} url The URL where the zip file is located. Takes precedence over `repository`.
* @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. Ignored when `url` was given.
* @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required.
* @param {boolean} prerelease Flag on whether to get a prelease or not. Default `false`.
* @param {string} downloadDir Relative path to the download directory. Non-existing folders will be auto-created. Defaults to `tmp` under current working directory.
*
* @return {string} Absolute path to the downloaded zip.
*/
export const downloadZip = async ( {
url,
repository,
authorizationToken,
prerelease = false,
downloadDir = 'tmp',
} ) => {
let zipFilename = path.basename( url || repository );
zipFilename = zipFilename.endsWith( '.zip' )
? zipFilename
: zipFilename.concat( '.zip' );
const zipFilePath = path.resolve( downloadDir, zipFilename );
let response;
// Create destination folder.
fs.mkdirSync( downloadDir, { recursive: true } );
const downloadURL =
url ??
( await getLatestReleaseZipUrl( {
repository,
authorizationToken,
prerelease,
} ) );
// Download the zip.
const options = {
method: 'get',
url: downloadURL,
responseType: 'stream',
headers: {
Authorization: authorizationToken
? `token ${ authorizationToken }`
: '',
Accept: 'application/octet-stream',
},
};
response = await axios( options ).catch( ( error ) => {
if ( error.response ) {
console.error( error.response.data );
}
throw new Error( error.message );
} );
response.data.pipe( fs.createWriteStream( zipFilePath ) );
return zipFilePath;
};
/**
* Delete a zip file. Useful when cleaning up downloaded plugin zips.
*
* @param {string} zipFilePath Local file path to the ZIP.
*/
export const deleteZip = async ( zipFilePath ) => {
await fs.unlink( zipFilePath, ( err ) => {
if ( err ) throw err;
} );
};
/**
* Get the download URL of the latest release zip for a plugin using GitHub API.
*
* @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param
* @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`.
* @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required.
* @param {boolean} prerelease Flag on whether to get a prelease or not.
* @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3.
* @return {string} Download URL for the release zip file.
*/
export const getLatestReleaseZipUrl = async ( {
repository,
authorizationToken,
prerelease = false,
perPage = 3,
} ) => {
let release;
const requesturl = prerelease
? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }`
: `https://api.github.com/repos/${ repository }/releases/latest`;
const options = {
method: 'get',
url: requesturl,
headers: {
Authorization: authorizationToken
? `token ${ authorizationToken }`
: '',
},
};
// Get the first prerelease, or the latest release.
let response;
try {
response = await axios( options );
} catch ( error ) {
let errorMessage =
'Something went wrong when downloading the plugin.\n';
if ( error.response ) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
errorMessage = errorMessage.concat(
`Response status: ${ error.response.status } ${ error.response.statusText }`,
'\n',
`Response body:`,
'\n',
JSON.stringify( error.response.data, null, 2 ),
'\n'
);
} else if ( error.request ) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
errorMessage = errorMessage.concat(
JSON.stringify( error.request, null, 2 ),
'\n'
);
} else {
// Something happened in setting up the request that triggered an Error
errorMessage = errorMessage.concat( error.toJSON(), '\n' );
}
throw new Error( errorMessage );
}
release = prerelease
? response.data.find( ( { prerelease } ) => prerelease )
: response.data;
// If response contains assets, return URL of first asset.
// Otherwise, return the github.com URL from the tag name.
const { assets } = release;
if ( assets && assets.length ) {
return assets[ 0 ].url;
} else {
const tagName = release.tag_name;
return `https://github.com/${ repository }/archive/${ tagName }.zip`;
}
};
/**
* Install a plugin using WP CLI within a WP ENV environment.
* This is a workaround to the "The uploaded file exceeds the upload_max_filesize directive in php.ini" error encountered when uploading a plugin to the local WP Env E2E environment through the UI.
*
* @see https://github.com/WordPress/gutenberg/issues/29430
*
* @param {string} pluginPath
*/
export const installPluginThruWpCli = async ( pluginPath ) => {
const runWpCliCommand = async ( command ) => {
const { stdout, stderr } = await execAsync(
`pnpm exec wp-env run tests-cli -- ${ command }`
);
console.log( stdout );
console.error( stderr );
};
const wpEnvPluginPath = pluginPath.replace(
/.*\/plugins\/woocommerce/,
'wp-content/plugins/woocommerce'
);
await runWpCliCommand( `ls ${ wpEnvPluginPath }` );
await runWpCliCommand(
`wp plugin install --activate --force ${ wpEnvPluginPath }`
);
await runWpCliCommand( `wp plugin list` );
};
/**
* Download the WooCommerce release zip. Can download draft releases when `token` is specified.
*
* @param {Object} params
* @param {import("@playwright/test").APIRequestContext} params.request
* @param {string} params.version The version indicated in the release `tag_name` or `name` field.
* @param {string} params.token
* @param {string} params.downloadDir
*
* @throws When `version` was not found.
*
* @returns {Promise<string>} Absolute path to the downloaded WooCommerce zip.
*/
export const downloadWooCommerceRelease = async ( {
request,
version = process.env.UPDATE_WC,
token = process.env.GITHUB_TOKEN,
downloadDir = 'tmp',
} ) => {
/**
*
* @returns {Promise<Release>}
*/
const getRelease = async () => {
const url =
'https://api.github.com/repos/woocommerce/woocommerce/releases';
const options = {
params: {
per_page: 100,
},
headers: {
Authorization: token ? `Bearer ${ token }` : undefined,
},
};
const response = await request.get( url, options );
/**
* @type {Release[]}
*/
const releases = await response.json();
const match = releases.find( ( { tag_name, name } ) =>
[ tag_name, name ].includes( version )
);
if ( ! match ) {
throw new Error( `Release ${ version } not found!` );
}
return match;
};
/**
*
* @param {Release} release
* @throws When `release` does not contain a woocommerce zip.
* @returns {ReleaseAsset}
*/
const getWooCommerceZipAsset = ( release ) => {
const zipName =
version.toLowerCase() === 'nightly'
? 'woocommerce-trunk-nightly.zip'
: 'woocommerce.zip';
const asset = release.assets.find( ( { name } ) => name === zipName );
if ( ! asset ) {
throw new Error(
`Release ${ version } does not contain a WooCommerce ZIP asset`
);
}
return asset;
};
const release = await getRelease();
const asset = getWooCommerceZipAsset( release );
const downloadResponse = await request.get( asset.url, {
headers: {
Authorization: token ? `Bearer ${ token }` : undefined,
Accept: 'application/octet-stream',
},
} );
expect( downloadResponse.ok() ).toBeTruthy();
const body = await downloadResponse.body();
const zipPath = path.resolve( downloadDir, asset.name );
fs.mkdirSync( path.resolve( downloadDir ), { recursive: true } );
fs.writeFileSync( zipPath, body );
return zipPath;
};

View File

@ -1,304 +0,0 @@
const api = require( './api' );
const deleteAllCoupons = async () => {
console.log( 'Deleting all coupons...' );
let coupons,
page = 1;
while (
( coupons = await api.get.coupons( { per_page: 100, page: page++ } ) )
.length > 0
) {
const ids = coupons.map( ( { id } ) => id );
await api.deletePost.coupons( ids );
}
console.log( 'Done.' );
};
const deleteAllProducts = async () => {
console.log( 'Deleting all products...' );
let products,
page = 1;
while (
( products = await api.get.products( { per_page: 100, page: page++ } ) )
.length > 0
) {
const ids = products.map( ( { id } ) => id );
await api.deletePost.products( ids );
}
console.log( 'Done.' );
};
const deleteAllProductAttributes = async () => {
console.log( 'Deleting all product attributes...' );
let attributes,
page = 1;
while (
( attributes = await api.get.productAttributes( {
per_page: 100,
page: page++,
} ) ).length > 0
) {
const ids = attributes.map( ( { id } ) => id );
await api.deletePost.productAttributes( ids );
}
console.log( 'Done.' );
};
const deleteAllProductCategories = async () => {
console.log( 'Deleting all product categories...' );
let categories,
page = 1;
// Exclude "Uncategorized" as it cannot be deleted
while (
( categories = (
await api.get.productCategories( { per_page: 100, page: page++ } )
).filter( ( { slug } ) => slug !== 'uncategorized' ) ).length > 0
) {
const ids = categories.map( ( { id } ) => id );
await api.deletePost.productCategories( ids );
}
console.log( 'Done.' );
};
const deleteAllProductTags = async () => {
console.log( 'Deleting all product tags...' );
let tags,
page = 1;
while (
( tags = await api.get.productTags( {
per_page: 100,
page: page++,
} ) ).length > 0
) {
const ids = tags.map( ( { id } ) => id );
await api.deletePost.productTags( ids );
}
console.log( 'Done.' );
};
const deleteAllOrders = async () => {
console.log( 'Deleting all orders...' );
let orders,
page = 1;
while (
( orders = await api.get.orders( { per_page: 100, page: page++ } ) )
.length > 0
) {
const ids = orders.map( ( { id } ) => id );
await api.deletePost.orders( ids );
}
console.log( 'Done.' );
};
const deleteAllShippingZones = async () => {
console.log( 'Deleting all shipping zones...' );
let shippingZones,
page = 1;
// Exclude "Locations not covered by your other zones" as it cannot be deleted.
while (
( shippingZones = (
await api.get.shippingZones( {
per_page: 100,
page: page++,
} )
).filter(
( { name } ) => name !== 'Locations not covered by your other zones'
) ).length > 0
) {
const ids = shippingZones.map( ( { id } ) => id );
for ( const id of ids ) {
await api.deletePost.shippingZone( id );
}
}
console.log( 'Done.' );
};
const deleteAllShippingClasses = async () => {
console.log( 'Deleting all shipping classes...' );
let shippingClasses,
page = 1;
while (
( shippingClasses = await api.get.shippingClasses( {
per_page: 100,
page: page++,
} ) ).length > 0
) {
const ids = shippingClasses.map( ( { id } ) => id );
await api.deletePost.shippingClasses( ids );
}
console.log( 'Done.' );
};
const deleteAllShippingMethodsInDefaultShippingZone = async () => {
console.log( 'Deleting all shipping methods...' );
let shippingMethods;
while (
( shippingMethods = await api.get.shippingZoneMethods( 0 ) ).length > 0
) {
const ids = shippingMethods.map( ( { id } ) => id );
for ( const id of ids ) {
await api.deletePost.shippingZoneMethod( 0, id );
}
}
console.log( 'Done.' );
};
const deleteAllTaxClasses = async () => {
console.log( 'Deleting all non-default tax classes...' );
let taxClasses;
const getExistingNonDefaultTaxClasses = async () => {
return ( await api.get.taxClasses() ).filter(
( { slug } ) =>
! [ 'standard', 'reduced-rate', 'zero-rate' ].includes( slug )
);
};
while (
( taxClasses = await getExistingNonDefaultTaxClasses() ).length > 0
) {
const slugs = taxClasses.map( ( { slug } ) => slug );
for ( const slug of slugs ) {
await api.deletePost.taxClass( slug );
}
}
console.log( 'Done.' );
};
const deleteAllTaxRates = async () => {
console.log( 'Deleting all tax rates...' );
let taxes,
page = 1;
while (
( taxes = await api.get.taxRates( { per_page: 100, page: page++ } ) )
.length > 0
) {
const ids = taxes.map( ( { id } ) => id );
await api.deletePost.taxRates( ids );
}
console.log( 'Done.' );
};
/**
* Reset the test site. Useful when running E2E tests on a hosted test site to reset it to a somewhat pristine state prior to running tests.
*
* @param {string} cKey Consumer key
* @param {string} cSecret Consumer secret
*/
const reset = async ( cKey, cSecret ) => {
console.log( '--------------------------' );
console.log( 'Resetting test site...' );
console.log( '--------------------------' );
api.constructWith( cKey, cSecret );
await deleteAllCoupons();
await deleteAllProducts();
await deleteAllProductAttributes();
await deleteAllProductCategories();
await deleteAllProductTags();
await deleteAllOrders();
await deleteAllShippingClasses();
await deleteAllShippingZones();
await deleteAllShippingMethodsInDefaultShippingZone();
await deleteAllTaxClasses();
await deleteAllTaxRates();
};
/**
* Convert Cart and Checkout pages to shortcode.
* @param {import('@playwright/test').FullConfig} config
*/
const useCartCheckoutShortcodes = async ( config ) => {
/**
* A WordPress page.
* @typedef {Object} WPPage
* @property {number} id
* @property {string} slug
*/
const { request: apiRequest } = require( '@playwright/test' );
const { baseURL, userAgent, extraHTTPHeaders } = config.projects[ 0 ].use;
const options = {
baseURL,
userAgent,
extraHTTPHeaders,
};
const request = await apiRequest.newContext( options );
// List all pages
const response_list = await request.get( '/wp-json/wp/v2/pages', {
data: {
_fields: [ 'id', 'slug' ],
},
failOnStatusCode: true,
} );
/**
* @type {WPPage[]}
*/
const list = await response_list.json();
// Find the cart and checkout pages
const cart = list.find( ( page ) => page.slug === 'cart' );
const checkout = list.find( ( page ) => page.slug === 'checkout' );
// Convert their contents to shortcodes
await request.put( `/wp-json/wp/v2/pages/${ cart.id }`, {
data: {
content: {
raw: '<!-- wp:shortcode -->[woocommerce_cart]<!-- /wp:shortcode -->',
},
},
failOnStatusCode: true,
} );
console.log( 'Cart page converted to shortcode.' );
await request.put( `/wp-json/wp/v2/pages/${ checkout.id }`, {
data: {
content: {
raw: '<!-- wp:shortcode -->[woocommerce_checkout]<!-- /wp:shortcode -->',
},
},
failOnStatusCode: true,
} );
console.log( 'Checkout page converted to shortcode.' );
};
module.exports = {
reset,
useCartCheckoutShortcodes,
};

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