Merge pull request #31260 from woocommerce/poc/e2e-scaffold

E2E test script scaffold
This commit is contained in:
Greg 2021-12-15 13:42:14 -07:00 committed by GitHub
commit 12b4b65cd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1105 additions and 93 deletions

View File

@ -4,6 +4,7 @@
- A `specs/data` folder to store page element data. - A `specs/data` folder to store page element data.
- Tests to verify that different top-level menu and their associated sub-menus load successfully. - Tests to verify that different top-level menu and their associated sub-menus load successfully.
- Test scaffolding via `npx wc-e2e install @woocommerce/e2e-core-tests`
## Changed ## Changed

View File

@ -20,6 +20,18 @@ Follow [E2E setup instructions](https://github.com/woocommerce/woocommerce/blob/
### Setting up core tests ### Setting up core tests
#### Version 0.2.0 or newer
Version 0.2.0 added a test installer that will populate the `tests/e2e/specs` folder with test scripts for all the current core test suite. It also creates sample configuration files including all the configuration data needed to run the core tests.
- Install the e2e-environment `npm install @woocommerce/e2e-environment --save-dev`
- Run the installer `npx wc-e2e install @woocommerce/e2e-core-tests`
- Merge the sample configuration files:
- `tests/e2e/docker/woocommerce.e2e-core-tests.sh` => `initialize.sh`
- `tests/e2e/config/default-woocommerce.e2e-core-tests.json` => `default.json`
#### Version 0.1.X or other test runner
- Create the folder `tests/e2e/specs` in your repository if it does not exist. - Create the folder `tests/e2e/specs` in your repository if it does not exist.
- To add a core test to your test suite, create a new `.test.js` file within `tests/e2e/specs` . Example code to run all the shopper tests: - To add a core test to your test suite, create a new `.test.js` file within `tests/e2e/specs` . Example code to run all the shopper tests:
```js ```js

View File

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

View File

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

View File

@ -0,0 +1,25 @@
#!/bin/bash
echo "Initializing WooCommerce E2E"
# This is a workaround to accommodate different directory names.
wp plugin activate --all
wp plugin deactivate akismet
wp plugin deactivate hello
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com \
--user_pass=password \
--role=subscriber \
--first_name='Jane' \
--last_name='Smith' \
--path=/var/www/html
# we cannot create API keys for the API, so we using basic auth, this plugin allows that.
wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate
# install the WP Mail Logging plugin to test emails
wp plugin install wp-mail-logging --activate
# initialize pretty permalinks
wp rewrite structure /%postname%/

View File

@ -0,0 +1,138 @@
{
"active": [
{
"name": "front-end",
"description": "Shopper tests",
"testFiles": [
{
"name": "cart-begin",
"functions": [ "runCartPageTest" ]
}, {
"name": "cart-calculate-shipping",
"functions": [ "runCartCalculateShippingTest" ]
}, {
"name": "cart-coupons",
"functions": [ "runCartApplyCouponsTest" ]
}, {
"name": "checkout-begin",
"functions": [ "runCheckoutPageTest" ]
}, {
"name": "checkout-coupons",
"functions": [ "runCheckoutApplyCouponsTest" ]
}, {
"name": "checkout-create-account",
"functions": [ "runCheckoutCreateAccountTest" ]
}, {
"name": "checkout-login-account",
"functions": [ "runCheckoutLoginAccountTest" ]
}, {
"name": "my-account-create-account",
"functions": [ "runMyAccountCreateAccountTest" ]
}, {
"name": "my-account-pay-order",
"functions": [ "runMyAccountPayOrderTest" ]
}, {
"name": "my-account",
"functions": [ "runMyAccountPageTest" ]
}, {
"name": "order-email-receiving",
"functions": [ "runOrderEmailReceivingTest" ]
}, {
"name": "product-browse-search-sort",
"functions": [ "runProductBrowseSearchSortTest" ]
}, {
"name": "single-product-page",
"functions": [ "runSingleProductPageTest" ]
}, {
"name": "variable-product-updates",
"functions": [ "runVariableProductUpdateTest" ]
}
]
}, {
"name": "rest-api",
"description": "REST API tests",
"testFiles": [
{
"name": "api",
"functions": [ "runApiTests" ]
}
]
}, {
"name": "wp-admin",
"description": "Merchant tests",
"testFiles": [
{
"name": "create-coupon",
"functions": [ "runCreateCouponTest" ]
}, {
"name": "create-order",
"functions": [ "runCreateOrderTest" ]
}, {
"name": "create-shipping-classes",
"functions": [ "runAddShippingClassesTest" ]
}, {
"name": "create-shipping-zones",
"functions": [ "runAddNewShippingZoneTest" ]
}, {
"name": "create-simple-product",
"functions": [ "runAddSimpleProductTest" ]
}, {
"name": "create-variable-product",
"functions": [ "runAddVariableProductTest" ]
}, {
"name": "order-coupon",
"functions": [ "runOrderApplyCouponTest" ]
}, {
"name": "order-customer-payment-page",
"functions": [ "runMerchantOrdersCustomerPaymentPage" ]
}, {
"name": "order-edit",
"functions": [ "runEditOrderTest" ]
}, {
"name": "order-emails",
"functions": [ "runMerchantOrderEmailsTest" ]
}, {
"name": "order-refund",
"functions": [ "runOrderRefundTest" ]
}, {
"name": "order-searching",
"functions": [ "runOrderSearchingTest" ]
}, {
"name": "order-status-filters",
"functions": [ "runOrderStatusFiltersTest" ]
}, {
"name": "product-edit",
"functions": [ "runProductEditDetailsTest" ]
}, {
"name": "product-import-csv",
"functions": [ "runImportProductsTest" ]
}, {
"name": "product-search",
"functions": [ "runProductSearchTest" ]
}, {
"name": "update-general-settings",
"functions": [ "runUpdateGeneralSettingsTest" ]
}, {
"name": "update-product-settings",
"functions": [ "runProductSettingsTest" ]
}, {
"name": "update-tax-settings",
"functions": [ "runTaxSettingsTest" ]
}, {
"name": "wccom-connect",
"functions": [ "runInitiateWccomConnectionTest" ]
}
]
}
],
"deprecated": [
{
"name": "example-folder",
"testFiles": [
{ "name": "any-filename-to-deprecate" }
]
}
]
}

View File

@ -3,10 +3,12 @@
## Added ## Added
- Added `await` for every call to `shopper.logout` - Added `await` for every call to `shopper.logout`
- Test setup, scaffolding, and removal via `wc-e2e install` and `wc-e2e uninstall`
## Fixed ## Fixed
- Updated the browserViewport in `jest.setup.js` to match the `defaultViewport` dimensions defined in `jest-puppeteer.config.js` - Updated the browserViewport in `jest.setup.js` to match the `defaultViewport` dimensions defined in `jest-puppeteer.config.js`
## Added ## Added
- Added quotes around `WORDPRESS_TITLE` value in .env file to address issue with docker compose 2 "key cannot contain a space" error. - Added quotes around `WORDPRESS_TITLE` value in .env file to address issue with docker compose 2 "key cannot contain a space" error.

View File

@ -9,6 +9,19 @@ npm install @woocommerce/e2e-environment --save
npm install jest --global npm install jest --global
``` ```
### Version 0.3.0 and newer
Version 0.3.0 added a test installer that will populate the `tests/e2e/*` folder with test scripts and configuration files. The installer will create test scripts for E2E test packages that include support for the installer.
- [Adding test scaffolding to E2E test packages](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-environment/test-packages.md)
#### Using the installer
- Install a default test environment: `npx wc-e2e install`
- Install test specs from an E2E tests package: `npx wc-e2e install @woocommerce-e2e-tests [--format cjs] [--ext spec.js]`
- The default test spec format and extension are `ES6` and `test.js`
- Remove test specs for an E2E tests package: `npx wc-e2e uninstall @woocommerce-e2e-tests`
## Configuration ## Configuration
The `@woocommerce/e2e-environment` package exports configuration objects that can be consumed in JavaScript config files in your project. Additionally, it includes a basic hosting container for running tests and includes instructions for creating your Travis CI setup. The `@woocommerce/e2e-environment` package exports configuration objects that can be consumed in JavaScript config files in your project. Additionally, it includes a basic hosting container for running tests and includes instructions for creating your Travis CI setup.
@ -60,10 +73,10 @@ The E2E environment uses Jest as a test runner. Extending the base config is nec
```js ```js
const path = require( 'path' ); const path = require( 'path' );
const { useE2EJestConfig } = require( '@woocommerce/e2e-environment' ); const { useE2EJestConfig, resolveLocalE2ePath } = require( '@woocommerce/e2e-environment' );
const jestConfig = useE2EJestConfig( { const jestConfig = useE2EJestConfig( {
roots: [ path.resolve( __dirname, '../specs' ) ], roots: [ resolveLocalE2ePath( 'specs' ) ],
} ); } );
module.exports = jestConfig; module.exports = jestConfig;

View File

@ -11,6 +11,7 @@ const {
getAppName, getAppName,
getTestConfig, getTestConfig,
resolveLocalE2ePath, resolveLocalE2ePath,
resolvePackagePath,
} = require( '../utils' ); } = require( '../utils' );
const dockerArgs = []; const dockerArgs = [];
@ -63,7 +64,7 @@ if ( appPath ) {
if ( fs.existsSync( appInitFile ) ) { if ( fs.existsSync( appInitFile ) ) {
fs.copyFileSync( fs.copyFileSync(
appInitFile, appInitFile,
path.resolve( __dirname, '../docker/wp-cli/initialize.sh' ) resolvePackagePath( 'docker/wp-cli/initialize.sh' )
); );
console.log( 'Initializing ' + appInitFile ); console.log( 'Initializing ' + appInitFile );
} }
@ -90,7 +91,7 @@ if ( ! process.env.WORDPRESS_URL ) {
} }
// Ensure that the first Docker compose file loaded is from our local env. // Ensure that the first Docker compose file loaded is from our local env.
dockerArgs.unshift( '-f', path.resolve( __dirname, '../docker-compose.yaml' ) ); dockerArgs.unshift( '-f', resolvePackagePath( 'docker-compose.yaml' ) );
const dockerProcess = spawnSync( 'docker-compose', dockerArgs, { const dockerProcess = spawnSync( 'docker-compose', dockerArgs, {
stdio: 'inherit', stdio: 'inherit',

View File

@ -4,7 +4,11 @@ const { spawnSync } = require( 'child_process' );
const program = require( 'commander' ); const program = require( 'commander' );
const path = require( 'path' ); const path = require( 'path' );
const fs = require( 'fs' ); const fs = require( 'fs' );
const { getAppRoot, resolveLocalE2ePath } = require( '../utils' ); const {
getAppRoot,
resolveLocalE2ePath,
resolvePackagePath,
} = require( '../utils' );
const { const {
WC_E2E_SCREENSHOTS, WC_E2E_SCREENSHOTS,
JEST_PUPPETEER_CONFIG, JEST_PUPPETEER_CONFIG,
@ -30,7 +34,7 @@ if ( WC_E2E_SCREENSHOTS ) {
} }
} }
const nodeConfigDirs = [ path.resolve( __dirname, '../config' ) ]; const nodeConfigDirs = [ resolvePackagePath( 'config' ) ];
if ( appPath ) { if ( appPath ) {
nodeConfigDirs.unshift( resolveLocalE2ePath( 'config' ) ); nodeConfigDirs.unshift( resolveLocalE2ePath( 'config' ) );
@ -51,10 +55,7 @@ if ( ! JEST_PUPPETEER_CONFIG ) {
// Use local Puppeteer config if there is one. // Use local Puppeteer config if there is one.
// Load test configuration file into an object. // Load test configuration file into an object.
const localJestConfigFile = resolveLocalE2ePath( 'config/jest-puppeteer.config.js' ); const localJestConfigFile = resolveLocalE2ePath( 'config/jest-puppeteer.config.js' );
const jestConfigFile = path.resolve( const jestConfigFile = resolvePackagePath( 'config/jest-puppeteer.config.js' );
__dirname,
'../config/jest-puppeteer.config.js'
);
testEnvVars.JEST_PUPPETEER_CONFIG = fs.existsSync( localJestConfigFile ) testEnvVars.JEST_PUPPETEER_CONFIG = fs.existsSync( localJestConfigFile )
? localJestConfigFile ? localJestConfigFile
@ -88,7 +89,7 @@ if ( program.debug ) {
const envVars = Object.assign( {}, process.env, testEnvVars ); const envVars = Object.assign( {}, process.env, testEnvVars );
let configPath = path.resolve( __dirname, '../config/jest.config.js' ); let configPath = resolvePackagePath( 'config/jest.config.js' );
// Look for a Jest config in the dependent app's path. // Look for a Jest config in the dependent app's path.
if ( appPath ) { if ( appPath ) {

View File

@ -0,0 +1,233 @@
#!/usr/bin/env node
/**
* External dependencies.
*/
const fs = require( 'fs' );
const path = require( 'path' );
const sprintf = require( 'sprintf-js' ).sprintf;
/**
* Internal dependencies.
*/
const {
resolvePackage,
resolvePackagePath,
} = require( '../utils' );
const {
createLocalE2ePath,
confirm,
confirmLocalCopy,
confirmLocalDelete,
getPackageData,
installDefaults
} = require( '../utils/scaffold' );
const args = process.argv.slice( 2 );
const [ command, packageName ] = args;
// Allow multiple spec file extensions and formats.
let testExtension = 'test.js';
let testFormat = '';
for ( let a = 2; a < args.length; a++ ) {
const nextArg = a + 1;
if ( nextArg >= args.length ) {
break;
}
switch ( args[ a ] ) {
case '--format':
testFormat = args[ nextArg ];
break;
case '--ext':
testExtension = args[ nextArg ];
break;
}
}
/**
* Install the test scripts and sample default.json configuration
*/
if ( command == 'install' ) {
// Install some environment defaults if no package is requested.
if ( ! packageName ) {
installDefaults();
return;
}
// `package` is a reserved word
const pkg = resolvePackage( packageName ).name;
if ( ! pkg.length ) {
//@todo add error message
return;
}
const { packageSlug, testSpecs, defaultJson, initializeSh } = getPackageData( pkg );
// Write sample default.json
if ( defaultJson ) {
const defaultJsonName = `config${path.sep}default-${packageSlug}.json`;
createLocalE2ePath( 'config' );
if ( confirmLocalCopy( defaultJsonName, defaultJson, pkg ) ) {
console.log( `Created sample test configuration to 'tests/e2e/${defaultJsonName}'.` );
}
}
// Write sample initialize.sh
if ( initializeSh ) {
const defaultInitName = `docker${path.sep}${packageSlug}.sh`;
createLocalE2ePath( 'docker' );
if ( confirmLocalCopy( defaultInitName, initializeSh, pkg ) ) {
console.log( `Created sample test container initialization script to 'tests/e2e/${defaultInitName}'.` );
}
}
if ( ! testSpecs ) {
return;
}
// Write test files
const testsSpecFile = resolvePackagePath( testSpecs, pkg );
const specs = fs.readFileSync( testsSpecFile );
const tests = JSON.parse( specs );
const { active, deprecated } = tests;
if ( active && active.length ) {
const blankLine = '';
const eol = "\n";
const autoGenerate = sprintf( '/* This file was auto-generated by the command `npx wc-e2e install %s`. */', packageName );
let importLineFormat;
let overwriteFiles;
let confirmPrompt;
if ( testFormat.toLowerCase() == 'cjs' ) {
importLineFormat = sprintf( "const {%%s} = require( '%s' );", pkg );
} else {
importLineFormat = sprintf( "import {%%s} from '%s';", pkg );
}
// Create the specs folder if not present
let specFolderPath = createLocalE2ePath( 'specs' );
// Loop through folders and files to write test scripts.
for ( let f = 0; f < active.length; f++ ) {
if ( overwriteFiles == 'q' ) {
overwriteFiles = '';
break;
}
const testFolder = active[ f ];
const { testFiles } = testFolder;
if ( ! testFiles || ! testFiles.length ) {
continue;
}
let specFolder;
if ( testFolder.name.length ) {
specFolder = createLocalE2ePath( `specs${path.sep}${testFolder.name}` );
} else {
specFolder = specFolderPath;
}
// Create the test files.
for ( let t = 0; t < testFiles.length; t++ ) {
const testFile = testFiles[ t ];
if ( ! testFile.functions.length ) {
continue;
}
const testFileName = `${testFolder.name}${path.sep}${testFile.name}.${testExtension}`;
const testFilePath = `${specFolder}${path.sep}${testFile.name}.${testExtension}`;
// Check to see if file exists.
if ( fs.existsSync( testFilePath ) ) {
if ( overwriteFiles != 'a' ) {
confirmPrompt = `${testFileName} already exists. Overwrite? [y]es/[n]o/[a]ll/[q]uit: `;
overwriteFiles = confirm( confirmPrompt, 'anqy' );
overwriteFiles = overwriteFiles.toLowerCase();
}
if ( overwriteFiles == 'q' ) {
break;
}
if ( overwriteFiles != 'a' && overwriteFiles != 'y' ) {
continue;
}
}
console.log( 'Writing tests/e2e/specs/' + testFileName );
let buffer = [ autoGenerate ];
let testSeparator, testTerminator, importPrefix;
// Add the import line.
if ( testFile.functions.length > 3 ) {
testSeparator = ',' + eol;
testTerminator = eol;
importPrefix = eol;
} else {
testSeparator = ', ';
testTerminator = ' ';
importPrefix = ' ';
}
const testImport = testFile.functions.join( testSeparator ) + testTerminator;
buffer.push( sprintf( importLineFormat, importPrefix + testImport ), blankLine );
// Add test function calls and write the file
let functionCalls = testFile.functions.map( functionName => functionName + '();' );
buffer.push( ...functionCalls, blankLine );
fs.writeFileSync( testFilePath, buffer.join( eol ) );
}
}
}
// @todo: deprecated files.
} else if ( command == 'uninstall' ) {
if ( ! packageName ) {
// @todo: write error message
return;
}
const pkg = resolvePackage( packageName ).name;
const { packageSlug, testSpecs, defaultJson, initializeSh } = getPackageData( pkg );
// Delete sample default.json
if ( defaultJson ) {
const defaultJsonName = `config${path.sep}default-${packageSlug}.json`;
confirmLocalDelete( defaultJsonName );
}
// Delete sample initialize.sh
if ( initializeSh ) {
const defaultInitName = `docker${path.sep}${packageSlug}.sh`;
confirmLocalDelete( defaultInitName );
}
if ( ! testSpecs ) {
return;
}
const testsSpecFile = resolvePackagePath( testSpecs, pkg );
const specs = fs.readFileSync( testsSpecFile );
const tests = JSON.parse( specs );
const { active } = tests;
if ( ! active || ! active.length ) {
return;
}
// Loop through folders and files to delete test scripts.
for ( let f = 0; f < active.length; f++ ) {
const testFolder = active[ f ];
const { testFiles } = testFolder;
if ( ! testFiles || ! testFiles.length ) {
continue;
}
const specFolder = testFolder.name.length ? `specs${path.sep}${testFolder.name}` : 'specs';
for ( let t = 0; t < testFiles.length; t++ ) {
const testFile = testFiles[ t ];
const testFilePath = `${specFolder}${path.sep}${testFile.name}.${testExtension}`;
confirmLocalDelete( testFilePath );
}
}
}

View File

@ -71,6 +71,10 @@ case $1 in
./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev --debug $2 ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js --dev --debug $2
TESTRESULT=$? TESTRESULT=$?
;; ;;
'install' | \
'uninstall')
./bin/scaffold.js $@
;;
*) *)
usage usage
;; ;;

View File

@ -0,0 +1,25 @@
#!/bin/bash
echo "Initializing WooCommerce E2E"
# This is a workaround to accommodate different directory names.
wp plugin activate --all
wp plugin deactivate akismet
wp plugin deactivate hello
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com \
--user_pass=password \
--role=subscriber \
--first_name='Jane' \
--last_name='Smith' \
--path=/var/www/html
# we cannot create API keys for the API, so we using basic auth, this plugin allows that.
wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate
# install the WP Mail Logging plugin to test emails
wp plugin install wp-mail-logging --activate
# initialize pretty permalinks
wp rewrite structure /%postname%/

View File

@ -0,0 +1,8 @@
const path = require( 'path' );
const { useE2EJestConfig, getAppRoot } = require( '@woocommerce/e2e-environment' );
const jestConfig = useE2EJestConfig( {
roots: [ path.resolve( __dirname, '../specs' ) ],
} );
module.exports = jestConfig;

View File

@ -0,0 +1,80 @@
import {
clearLocalStorage,
setBrowserViewport,
withRestApi,
WP_ADMIN_LOGIN
} from '@woocommerce/e2e-utils';
const config = require( 'config' );
const { HTTPClientFactory } = require( '@woocommerce/api' );
const { addConsoleSuppression, updateReadyPageStatus } = require( '@woocommerce/e2e-environment' );
const { DEFAULT_TIMEOUT_OVERRIDE } = process.env;
// @todo: remove this once https://github.com/woocommerce/woocommerce-admin/issues/6992 has been addressed
addConsoleSuppression( 'woocommerce_shared_settings', false );
/**
* Uses the WordPress API to delete all existing posts
*/
async function trashExistingPosts() {
const apiUrl = config.get('url');
const wpPostsEndpoint = '/wp/v2/posts';
const adminUsername = config.get('users.admin.username');
const adminPassword = config.get('users.admin.password');
const client = HTTPClientFactory.build(apiUrl)
.withBasicAuth(adminUsername, adminPassword)
.create();
// List all existing posts
const response = await client.get(wpPostsEndpoint);
const posts = response.data;
// Delete each post
for (const post of posts) {
await client.delete(`${wpPostsEndpoint}/${post.id}`);
}
}
// Before every test suite run, delete all content created by the test. This ensures
// other posts/comments/etc. aren't dirtying tests and tests don't depend on
// each other's side-effects.
beforeAll(async () => {
if ( DEFAULT_TIMEOUT_OVERRIDE ) {
page.setDefaultNavigationTimeout( DEFAULT_TIMEOUT_OVERRIDE );
page.setDefaultTimeout( DEFAULT_TIMEOUT_OVERRIDE );
}
try {
// Update the ready page to prevent concurrent test runs
await updateReadyPageStatus('draft');
await trashExistingPosts();
await withRestApi.deleteAllProducts();
await withRestApi.deleteAllCoupons();
await withRestApi.deleteAllOrders();
} catch ( error ) {
// Prevent an error here causing tests to fail.
}
await page.goto(WP_ADMIN_LOGIN);
await clearLocalStorage();
await setBrowserViewport( {
width: 1280,
height: 800,
});
});
// Clear browser cookies and cache using DevTools.
// This is to ensure that each test ends with no user logged in.
afterAll(async () => {
// Reset the ready page to published to allow future test runs
try {
await updateReadyPageStatus('publish');
} catch ( error ) {
// Prevent an error here causing tests to fail.
}
const client = await page.target().createCDPSession();
await client.send('Network.clearBrowserCookies');
await client.send('Network.clearBrowserCache');
});

View File

@ -33,7 +33,9 @@
"jest-each": "25.5.0", "jest-each": "25.5.0",
"jest-puppeteer": "^4.4.0", "jest-puppeteer": "^4.4.0",
"node-stream-zip": "^1.13.6", "node-stream-zip": "^1.13.6",
"request": "^2.88.2" "readline-sync": "^1.4.10",
"request": "^2.88.2",
"sprintf-js": "^1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.12.8", "@babel/cli": "7.12.8",

View File

@ -0,0 +1,104 @@
# WooCommerce End-to-End Test Packages
There are two limitations which significantly impact the architecture of E2E test packages:
- Referencing the `jest` functions `describe`, `it`, `beforeAll`, etc. throws a fatal error outside the `jest` environment.
- `jest` will not scan for tests in any path containing `node_mdules`.
## Creating a tests package
The way to create a tests package with the above limitations is
- **In the tests package**, wrap each test in a function
```js
/**
* Require the necessary jest functions to prevent the package build from referencing them
* `import` references imported functions during package build
*/
const { describe, it, beforeAll } = require( '@jest/globals' );
const testMyCriticalFlow = () => {
describe( 'My Critical Flow', () => {
beforeAll( async () => {
// Test setup
} );
it( 'can complete first step', async () => {
// Do stuff
expect( someValue ).toBeTruthy();
} );
} );
};
modules.exports = testMyFlow;
```
- **In the `tests/e2e/specs` folder**, create a test spec that calls the test function
```js
import { testMyCriticalFlow } from 'MyTestsPackage';
testMyCriticalFlow();
```
## Adding the scaffolds for the test installer
To work with the limitations outlined above, the test installer needs to access the test scaffolding information without accessing the package index. As a result, the `installFiles` is a required path in the steps below
- Create an `installFiles` folder in the root of the package
- Add an `index.js` to the folder which exports an object with some or all of three properties
```js
module.exports = {
defaultJson: 'installFiles/default-test-config.json',
initializeSh: 'installFiles/initialize.sh.default',
testSpecs: 'installFiles/scaffold-tests.json',
};
```
- The value of each of the properties should be a relative path from the package `index.js`. The test installer will remove `dist`, `build`, and `build-modules` from the end of the package index path.
- `defaultJson`: Path to a JSON file containing all `default.json` entries needed for the tests in the package.
- `initializeSh`: Path to a bash script containing the WP CLI commands needed to initialize the `e2e-environment` test container.
- `testSpecs`: Path to a JSON file containing a nested object
```json
{
"active": [
{
"name": "first-folder-name",
"description": "First tests",
"testFiles": [
{
"name": "test-name-a",
"functions": [
"testMyCriticalFlow"
]
},
{
"name": "test-name-b",
"functions": [
"testSecondCriticalFlow",
"testThirdCriticalFlow"
]
}
]
},
{
"name": "second-folder-name",
"description": "Second tests",
"testFiles": [
....
]
}
]
}
```
The test installer uses the `testSpecs` nested object to create test specs. Using the example above, create `tests/e2e/specs/first-folder-name/test-name-b.test.js`:
```js
/* This file was auto-generated by the command `npx wc-e2e install your-package-name`. */
import { testSecondCriticalFlow, testThirdCriticalFlow } from 'your-package-name';
testSecondCriticalFlow();
testThirdCriticalFlow();
```

View File

@ -13,10 +13,7 @@ const StreamZip = require( 'node-stream-zip' );
*/ */
const getRemotePluginZip = async ( fileUrl ) => { const getRemotePluginZip = async ( fileUrl ) => {
const appPath = getAppRoot(); const appPath = getAppRoot();
const savePath = path.resolve( const savePath = resolveLocalE2ePath( 'plugins' );
appPath,
'plugins/woocommerce/tests/e2e/plugins'
);
mkdirp.sync( savePath ); mkdirp.sync( savePath );
// Pull the filename from the end of the URL // Pull the filename from the end of the URL

View File

@ -1,6 +1,6 @@
const getAppRoot = require( './app-root' ); const getAppRoot = require( './app-root' );
const { getAppName, getAppBase } = require( './app-name' ); const { getAppName, getAppBase } = require( './app-name' );
const { getTestConfig, getAdminConfig, resolveLocalE2ePath } = require( './test-config' ); const testConfig = require( './test-config' );
const { getRemotePluginZip, getLatestReleaseZipUrl } = require('./get-plugin-zip'); const { getRemotePluginZip, getLatestReleaseZipUrl } = require('./get-plugin-zip');
const takeScreenshotFor = require( './take-screenshot' ); const takeScreenshotFor = require( './take-screenshot' );
const updateReadyPageStatus = require('./update-ready-page'); const updateReadyPageStatus = require('./update-ready-page');
@ -10,12 +10,10 @@ module.exports = {
getAppBase, getAppBase,
getAppRoot, getAppRoot,
getAppName, getAppName,
getTestConfig,
getAdminConfig,
resolveLocalE2ePath,
getRemotePluginZip, getRemotePluginZip,
getLatestReleaseZipUrl, getLatestReleaseZipUrl,
takeScreenshotFor, takeScreenshotFor,
updateReadyPageStatus, updateReadyPageStatus,
...testConfig,
...consoleUtils, ...consoleUtils,
}; };

View File

@ -0,0 +1,124 @@
/**
* External dependencies.
*/
const fs = require( 'fs' );
const path = require( 'path' );
const readlineSync = require( 'readline-sync' );
/**
* Internal dependencies.
*/
const { resolveLocalE2ePath, resolvePackagePath } = require( './test-config' );
/**
* Create a path relative to the local `tests/e2e` folder.
* @param relativePath
* @return {string}
*/
const createLocalE2ePath = ( relativePath ) => {
let specFolderPath = '';
const folders = [ `..${path.sep}..${path.sep}tests`, `..${path.sep}e2e`, relativePath ];
folders.forEach( ( folder ) => {
specFolderPath = resolveLocalE2ePath( folder );
if ( ! fs.existsSync( specFolderPath ) ) {
console.log( `Creating folder ${specFolderPath}` );
fs.mkdirSync( specFolderPath );
}
} );
return specFolderPath;
};
/**
* Prompt the console for confirmation.
*
* @param {string} prompt Prompt for the user.
* @param {string} choices valid responses.
* @return {string}
*/
const confirm = ( prompt, choices ) => {
const answer = readlineSync.keyIn( prompt, choices );
return answer;
};
/**
*
* @param {string} localE2ePath Destination path
* @param {string} packageE2ePath Source path
* @param {string} packageName Source package. Default @woocommerce/e2e-environment package.
* @return {boolean}
*/
const confirmLocalCopy = ( localE2ePath, packageE2ePath, packageName = '' ) => {
const localPath = resolveLocalE2ePath( localE2ePath );
const packagePath = resolvePackagePath( packageE2ePath, packageName );
const confirmPrompt = `${localE2ePath} already exists. Overwrite? [Y]es/[n]o: `;
let overwriteFiles;
if ( fs.existsSync( localPath ) ) {
overwriteFiles = confirm( confirmPrompt, 'ny' );
overwriteFiles = overwriteFiles.toLowerCase();
} else {
overwriteFiles = 'y';
}
if ( overwriteFiles == 'y' ) {
fs.copyFileSync( packagePath, localPath );
return true;
}
return false;
};
/**
* Prompt for confirmation before deleting a local E2E file.
*
* @param {string} localE2ePath Relative path to local E2E file.
*/
const confirmLocalDelete = ( localE2ePath ) => {
const localPath = resolveLocalE2ePath( localE2ePath );
if ( ! fs.existsSync( localPath ) ) {
return;
}
const confirmPrompt = `${localE2ePath} exists. Delete? [y]es/[n]o: `;
const deleteFile = confirm( confirmPrompt, 'ny' );
if ( deleteFile == 'y' ) {
fs.unlinkSync( localPath );
}
};
/**
* Get the install data for a tests package.
*
* @param {string} packageName npm package name
* @return {string}
*/
const getPackageData = ( packageName ) => {
const packageSlug = packageName.replace( '@', '' ).replace( /\//g, '.' );
const installFiles = require( `${packageName}${path.sep}installFiles` );
return { packageSlug, ...installFiles };
};
/**
* Install test runner and test container defaults
*/
const installDefaults = () => {
createLocalE2ePath( 'docker' );
console.log( 'Writing tests/e2e/docker/initialize.sh' );
confirmLocalCopy( `docker${path.sep}initialize.sh`, `installFiles${path.sep}initialize.sh` );
createLocalE2ePath( 'config' );
console.log( 'Writing tests/e2e/config/jest.config.js' );
confirmLocalCopy( `config${path.sep}jest.config.js`, `installFiles${path.sep}jest.config.js` );
console.log( 'Writing tests/e2e/config/jest.setup.js' );
confirmLocalCopy( `config${path.sep}jest.setup.js`, `installFiles${path.sep}jest.setup.js` );
};
module.exports = {
createLocalE2ePath,
confirm,
confirmLocalCopy,
confirmLocalDelete,
getPackageData,
installDefaults,
};

View File

@ -19,15 +19,84 @@ const resolveLocalE2ePath = ( filename = '' ) => {
); );
return resolvedPath; return resolvedPath;
} };
/**
* Resolve a package name installable by npm install.
*
* @param {string} packageName Name of the installed package.
* @param {boolean} allowRecurse Allow a recursive call. Default true.
* @return {object}
*/
const resolvePackage = ( packageName, allowRecurse = true ) => {
const resolvedPackage = {};
try {
const resolvedPath = path.dirname( require.resolve( packageName ) );
const buildPaths = [ 'dist', 'build', 'build-modules' ];
// Remove build paths from the resolved path.
let resolvedParts = resolvedPath.split( path.sep );
for ( let rp = resolvedParts.length - 1; rp >= 0; rp-- ) {
if ( buildPaths.includes( resolvedParts[ rp ] ) ) {
resolvedParts = resolvedParts.slice( 0, -1 );
} else {
break;
}
}
resolvedPackage.path = resolvedParts.join( path.sep );
resolvedPackage.name = packageName;
} catch ( e ) {
// Package name installed is not the package name.
resolvedPackage.path = '';
resolvedPackage.name = '';
}
// Attempt to find the package through the project package lock file.
if ( ! resolvedPackage.path.length && allowRecurse ) {
const packageLockPath = path.resolve( appPath, 'package-lock.json' );
const packageLockContent = fs.readFileSync( packageLockPath );
const { dependencies } = JSON.parse( packageLockContent );
for ( const [ key, value ] of Object.entries( dependencies ) ) {
if ( value.version.indexOf( packageName ) == 0 ) {
resolvedPackage = resolvePackage( key, false );
break;
}
}
}
return resolvedPackage;
};
/**
* Resolve a file in a package.
*
* @param {string} filename Filename to append to the path.
* @param {string} packageName Name of the installed package. Default @woocommerce/e2e-environment.
* @return {string}
*/
const resolvePackagePath = ( filename, packageName = '' ) => {
let packagePath;
if ( ! packageName.length ) {
packagePath = path.resolve( __dirname, '../' );
} else {
const pkg = resolvePackage( packageName );
packagePath = pkg.path;
}
const resolvedPath = path.resolve(
packagePath,
filename.indexOf( '/' ) == 0 ? filename.slice( 1 ) : filename
);
return resolvedPath;
};
// Copy local test configuration file if it exists. // Copy local test configuration file if it exists.
const localTestConfigFile = resolveLocalE2ePath( 'config/default.json' ); const localTestConfigFile = resolveLocalE2ePath( 'config/default.json' );
const defaultConfigFile = path.resolve( const defaultConfigFile = resolvePackagePath( 'config/default/default.json' );
__dirname, const testConfigFile = resolvePackagePath( 'config/default.json' );
'../config/default/default.json'
);
const testConfigFile = path.resolve( __dirname, '../config/default.json' );
if ( fs.existsSync( localTestConfigFile ) ) { if ( fs.existsSync( localTestConfigFile ) ) {
fs.copyFileSync( localTestConfigFile, testConfigFile ); fs.copyFileSync( localTestConfigFile, testConfigFile );
@ -94,4 +163,6 @@ module.exports = {
getTestConfig, getTestConfig,
getAdminConfig, getAdminConfig,
resolveLocalE2ePath, resolveLocalE2ePath,
resolvePackage,
resolvePackagePath,
}; };

View File

@ -78,7 +78,7 @@ export const withRestApi = {
}; };
const response = await client.put( onboardingProfileEndpoint, onboardingReset ); const response = await client.put( onboardingProfileEndpoint, onboardingReset );
expect( response.status ).toEqual( 200 ); expect( response.statusCode ).toEqual( 200 );
}, },
/** /**
* Use api package to delete coupons. * Use api package to delete coupons.

View File

@ -35,12 +35,12 @@ importers:
wp-textdomain: 1.0.1 wp-textdomain: 1.0.1
devDependencies: devDependencies:
'@automattic/nx-composer': 0.1.0 '@automattic/nx-composer': 0.1.0
'@nrwl/cli': 13.3.4 '@nrwl/cli': 13.3.6
'@nrwl/devkit': 13.1.4 '@nrwl/devkit': 13.1.4
'@nrwl/linter': 13.1.4 '@nrwl/linter': 13.1.4
'@nrwl/tao': 13.3.4 '@nrwl/tao': 13.3.6
'@nrwl/web': 13.1.4_42cab1dece2b2240094de84cfd414406 '@nrwl/web': 13.1.4_42cab1dece2b2240094de84cfd414406
'@nrwl/workspace': 13.3.4_42cab1dece2b2240094de84cfd414406 '@nrwl/workspace': 13.3.6_42cab1dece2b2240094de84cfd414406
'@types/node': 14.14.33 '@types/node': 14.14.33
'@woocommerce/eslint-plugin': 1.3.0 '@woocommerce/eslint-plugin': 1.3.0
'@wordpress/prettier-config': 1.1.1 '@wordpress/prettier-config': 1.1.1
@ -132,8 +132,10 @@ importers:
jest-puppeteer: ^4.4.0 jest-puppeteer: ^4.4.0
ndb: ^1.1.5 ndb: ^1.1.5
node-stream-zip: ^1.13.6 node-stream-zip: ^1.13.6
readline-sync: ^1.4.10
request: ^2.88.2 request: ^2.88.2
semver: ^7.3.2 semver: ^7.3.2
sprintf-js: ^1.1.2
dependencies: dependencies:
'@automattic/puppeteer-utils': github.com/Automattic/puppeteer-utils/0f3ec50 '@automattic/puppeteer-utils': github.com/Automattic/puppeteer-utils/0f3ec50
'@jest/test-sequencer': 25.5.4 '@jest/test-sequencer': 25.5.4
@ -147,7 +149,9 @@ importers:
jest-each: 25.5.0 jest-each: 25.5.0
jest-puppeteer: 4.4.0 jest-puppeteer: 4.4.0
node-stream-zip: 1.15.0 node-stream-zip: 1.15.0
readline-sync: 1.4.10
request: 2.88.2 request: 2.88.2
sprintf-js: 1.1.2
devDependencies: devDependencies:
'@babel/cli': 7.12.8_@babel+core@7.12.9 '@babel/cli': 7.12.8_@babel+core@7.12.9
'@babel/core': 7.12.9 '@babel/core': 7.12.9
@ -4273,23 +4277,11 @@ packages:
yargs-parser: 20.0.0 yargs-parser: 20.0.0
dev: true dev: true
/@nrwl/cli/13.2.4: /@nrwl/cli/13.3.6:
resolution: {integrity: sha512-2PxQ7iNghvrDk7O7nPt8ySOi4BPUL/oTQajd4tcsytfWonMrsb5f7wMpwoP5UAoKhcpWyiiFENylizOzEQoQdg==} resolution: {integrity: sha512-rTTadMSMM4GAf3ZwKXq+lasoPTF6n5RAF4ApzmB3MhXmKWIkjgF1oMY2ZBm/RS0x1aOsNDWdggDKJwMgnDZQgA==}
hasBin: true hasBin: true
dependencies: dependencies:
'@nrwl/tao': 13.2.4 '@nrwl/tao': 13.3.6
chalk: 4.1.0
enquirer: 2.3.6
v8-compile-cache: 2.3.0
yargs: 15.4.1
yargs-parser: 20.0.0
dev: true
/@nrwl/cli/13.3.4:
resolution: {integrity: sha512-x3IM9X/q5Yv54ZSr+GsIxa07GJt+hG7dOdjvAFBnixMWb3o4utOVrd6GusDrn2t0HhnCuzqLosFnk+gcOrL34w==}
hasBin: true
dependencies:
'@nrwl/tao': 13.3.4
chalk: 4.1.0 chalk: 4.1.0
enquirer: 2.3.6 enquirer: 2.3.6
v8-compile-cache: 2.3.0 v8-compile-cache: 2.3.0
@ -4356,10 +4348,10 @@ packages:
tslib: 2.3.1 tslib: 2.3.1
dev: true dev: true
/@nrwl/devkit/13.3.4: /@nrwl/devkit/13.3.6:
resolution: {integrity: sha512-fmYScbZYJWCdjRsi1kFq+c5HotdJx4k3TofZ7Bm+OdI3CWsFtjdLajhNMDWDPw2vIRLIZPtZUAyVTkwajR7yJw==} resolution: {integrity: sha512-SBq1NLHomZiSj+Ayc8q3xxxnXuFpJz2jt7GHXR+Ls6WnJUBfAVc1b4qcteT7X5ncUk/tXyAxrJC/34Gada79nQ==}
dependencies: dependencies:
'@nrwl/tao': 13.3.4 '@nrwl/tao': 13.3.6
ejs: 3.1.6 ejs: 3.1.6
ignore: 5.1.9 ignore: 5.1.9
rxjs: 6.6.7 rxjs: 6.6.7
@ -4411,12 +4403,12 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@nrwl/jest/13.3.4: /@nrwl/jest/13.3.6:
resolution: {integrity: sha512-seeuHHZVRDdugUoEI+JQsHG7q/7BOoJOeb7djR9TIwPRd8Tj5nBVO6dzOq3pMdvsgYmNkiVMFwIqMWXy6rNvaQ==} resolution: {integrity: sha512-npzlPhcmOogky6gIClCjgUajgAGMj1CYU9MmGlfrUmrgiZa8DFQXBV+4v6jT+oD3CwLWKnhVMWyBmow1Rt9FfQ==}
dependencies: dependencies:
'@jest/reporters': 27.2.2 '@jest/reporters': 27.2.2
'@jest/test-result': 27.2.2 '@jest/test-result': 27.2.2
'@nrwl/devkit': 13.3.4 '@nrwl/devkit': 13.3.6
chalk: 4.1.0 chalk: 4.1.0
identity-obj-proxy: 3.0.0 identity-obj-proxy: 3.0.0
jest-config: 27.2.2 jest-config: 27.2.2
@ -4472,11 +4464,11 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@nrwl/linter/13.3.4_typescript@4.2.4: /@nrwl/linter/13.3.6_typescript@4.2.4:
resolution: {integrity: sha512-p3SEL5pDgv1g2Q9OjdoBcYwDIEDnuO7cHUEUS1H7oww+HfDhuzVs4Zx+PtGjOSmT5qi3sLZZvt7U7pRNtIyrHQ==} resolution: {integrity: sha512-GYqyz2N6Sr9tRYe7dRimCSkuCXZqT8xTM0G2w+yWKktoHXaJvf/i50TsPFrvR5h5qIbYUk7HWQ8ktBG+EyePAg==}
dependencies: dependencies:
'@nrwl/devkit': 13.3.4 '@nrwl/devkit': 13.3.6
'@nrwl/jest': 13.3.4 '@nrwl/jest': 13.3.6
'@phenomnomnominal/tsquery': 4.1.1_typescript@4.2.4 '@phenomnomnominal/tsquery': 4.1.1_typescript@4.2.4
eslint: 8.2.0 eslint: 8.2.0
glob: 7.1.4 glob: 7.1.4
@ -4527,25 +4519,8 @@ packages:
yargs-parser: 20.0.0 yargs-parser: 20.0.0
dev: true dev: true
/@nrwl/tao/13.2.4: /@nrwl/tao/13.3.6:
resolution: {integrity: sha512-iYzv+JiWMZOLwtunOzHFVMuYW9iiICkjau94mlLHHDqsa6RgMx4qcXoZyVM78N9hlkc2yNvA+XVYFsoFsFWrpg==} resolution: {integrity: sha512-tv5JX3H9pg0J4Br/I1qHLwSUef97R3Tu8U47ZZ537AQa1fIrixGV2lzKoLSDKAdQsQcKF832tRyqOWhrNqP0Kg==}
hasBin: true
dependencies:
chalk: 4.1.0
enquirer: 2.3.6
fs-extra: 9.1.0
jsonc-parser: 3.0.0
nx: 13.2.4
rxjs: 6.6.7
rxjs-for-await: 0.0.2_rxjs@6.6.7
semver: 7.3.4
tmp: 0.2.1
tslib: 2.3.1
yargs-parser: 20.0.0
dev: true
/@nrwl/tao/13.3.4:
resolution: {integrity: sha512-ujwxGZcR3De8FSj8IjVSGmfZ2CQZfFzeV9QXU8DeiZ9J1ylWPwWpMIS3XVxZIpe7gR++XP4jbvFodKFQP7PzVQ==}
hasBin: true hasBin: true
dependencies: dependencies:
chalk: 4.1.0 chalk: 4.1.0
@ -4554,7 +4529,7 @@ packages:
fs-extra: 9.1.0 fs-extra: 9.1.0
ignore: 5.1.9 ignore: 5.1.9
jsonc-parser: 3.0.0 jsonc-parser: 3.0.0
nx: 13.3.4 nx: 13.3.6
rxjs: 6.6.7 rxjs: 6.6.7
rxjs-for-await: 0.0.2_rxjs@6.6.7 rxjs-for-await: 0.0.2_rxjs@6.6.7
semver: 7.3.4 semver: 7.3.4
@ -4720,18 +4695,18 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@nrwl/workspace/13.3.4_42cab1dece2b2240094de84cfd414406: /@nrwl/workspace/13.3.6_42cab1dece2b2240094de84cfd414406:
resolution: {integrity: sha512-FIBa705KC2LumlfESTRZ54oAVg+a+CMNtI3QULPQVYOMtEJLrYR+D8MDmz3Qchbwx704YO70+hy7FI7dY1rKFQ==} resolution: {integrity: sha512-0Xk6PlW9FRBrl3XeXCKHHJAGQCDuqcn34jgcBEBf4uYGF8vI+ZJLiqhwRNqseh6OlNnkJZmVViE0i8wIFCitxw==}
peerDependencies: peerDependencies:
prettier: ^2.3.0 prettier: ^2.3.0
peerDependenciesMeta: peerDependenciesMeta:
prettier: prettier:
optional: true optional: true
dependencies: dependencies:
'@nrwl/cli': 13.3.4 '@nrwl/cli': 13.3.6
'@nrwl/devkit': 13.3.4 '@nrwl/devkit': 13.3.6
'@nrwl/jest': 13.3.4 '@nrwl/jest': 13.3.6
'@nrwl/linter': 13.3.4_typescript@4.2.4 '@nrwl/linter': 13.3.6_typescript@4.2.4
'@parcel/watcher': 2.0.4 '@parcel/watcher': 2.0.4
chalk: 4.1.0 chalk: 4.1.0
chokidar: 3.5.2 chokidar: 3.5.2
@ -16353,28 +16328,21 @@ packages:
resolution: {integrity: sha512-LpCfZCWsVEtmD2SI1j2KRaw1uIyn4DJ3eRzsjnDYitbq38aORpkvYO+L0zVMZRNDSYSRGTsuj0nHCS3OOxK/Cg==} resolution: {integrity: sha512-LpCfZCWsVEtmD2SI1j2KRaw1uIyn4DJ3eRzsjnDYitbq38aORpkvYO+L0zVMZRNDSYSRGTsuj0nHCS3OOxK/Cg==}
hasBin: true hasBin: true
dependencies: dependencies:
'@nrwl/cli': 13.2.4 '@nrwl/cli': 13.3.6
dev: true dev: true
/nx/13.1.4: /nx/13.1.4:
resolution: {integrity: sha512-m2j3wymaFlEl/7EoGxlgRzdmgQV1Rsh42df1cM8xFzAzV8ZGtR3Zq19qK7r9SUabpq8jMzp1e6rLQTHewCJWig==} resolution: {integrity: sha512-m2j3wymaFlEl/7EoGxlgRzdmgQV1Rsh42df1cM8xFzAzV8ZGtR3Zq19qK7r9SUabpq8jMzp1e6rLQTHewCJWig==}
hasBin: true hasBin: true
dependencies: dependencies:
'@nrwl/cli': 13.3.4 '@nrwl/cli': 13.3.6
dev: true dev: true
/nx/13.2.4: /nx/13.3.6:
resolution: {integrity: sha512-/ZQRSKl1582CvntOT9cqItIu9vqGp1DnKuYgLClxpqkhk7GzIHWE9mtm56qT3o8NvBjZXyf+IZcxBwPgjd49NQ==} resolution: {integrity: sha512-fQqW5NLDpIsOkOdY8/YDpu9Dx1OjiIEDnnPhLdjCWsLVLhv0i8zcgs7nVWv4vPHdOapoZ4tYZ6I9w6qvgki5Kg==}
hasBin: true hasBin: true
dependencies: dependencies:
'@nrwl/cli': 13.2.4 '@nrwl/cli': 13.3.6
dev: true
/nx/13.3.4:
resolution: {integrity: sha512-prWCa/8jKBWUiCWhAbRD3UftTvaD8MgUo98g557/EpPaC9LHPPWNOhXDyij21sedouyDVdQGhr8TW4pEHt2zeQ==}
hasBin: true
dependencies:
'@nrwl/cli': 13.3.4
dev: true dev: true
/oauth-1.0a/2.2.6: /oauth-1.0a/2.2.6:
@ -18306,6 +18274,11 @@ packages:
dependencies: dependencies:
picomatch: 2.3.0 picomatch: 2.3.0
/readline-sync/1.4.10:
resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==}
engines: {node: '>= 0.8.0'}
dev: false
/realpath-native/1.1.0: /realpath-native/1.1.0:
resolution: {integrity: sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==} resolution: {integrity: sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==}
engines: {node: '>=4'} engines: {node: '>=4'}