Merge branch 'master' of https://github.com/woocommerce/woocommerce
This commit is contained in:
commit
222852dea6
|
@ -8,12 +8,12 @@
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0",
|
"php": ">=7.0",
|
||||||
"automattic/jetpack-autoloader": "^2.0.2",
|
"automattic/jetpack-autoloader": "2.0.2",
|
||||||
"automattic/jetpack-constants": "^1.1",
|
"automattic/jetpack-constants": "1.4.0",
|
||||||
"composer/installers": "1.7.0",
|
"composer/installers": "1.7.0",
|
||||||
"league/container": "^3.3",
|
"league/container": "3.3.1",
|
||||||
"maxmind-db/reader": "1.6.0",
|
"maxmind-db/reader": "1.6.0",
|
||||||
"pelago/emogrifier": "^3.1",
|
"pelago/emogrifier": "3.1.0",
|
||||||
"woocommerce/action-scheduler": "3.1.6",
|
"woocommerce/action-scheduler": "3.1.6",
|
||||||
"woocommerce/woocommerce-admin": "1.4.0-beta.3",
|
"woocommerce/woocommerce-admin": "1.4.0-beta.3",
|
||||||
"woocommerce/woocommerce-blocks": "3.1.0",
|
"woocommerce/woocommerce-blocks": "3.1.0",
|
||||||
|
|
|
@ -4,20 +4,20 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d7df64252352cb3445d827cf204f24b4",
|
"content-hash": "d90fdd441ed3eebf7b0bb77b5304b616",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "automattic/jetpack-autoloader",
|
"name": "automattic/jetpack-autoloader",
|
||||||
"version": "v2.1.0",
|
"version": "v2.0.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Automattic/jetpack-autoloader.git",
|
"url": "https://github.com/Automattic/jetpack-autoloader.git",
|
||||||
"reference": "802517b3ff3010de89141d9f7c4d56aec1d21527"
|
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/802517b3ff3010de89141d9f7c4d56aec1d21527",
|
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/4502da4b2443fc1b61389cacc94c34876aca2b3d",
|
||||||
"reference": "802517b3ff3010de89141d9f7c4d56aec1d21527",
|
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
"GPL-2.0-or-later"
|
"GPL-2.0-or-later"
|
||||||
],
|
],
|
||||||
"description": "Creates a custom autoloader for a plugin or theme.",
|
"description": "Creates a custom autoloader for a plugin or theme.",
|
||||||
"time": "2020-07-27T20:37:00+00:00"
|
"time": "2020-07-09T13:18:38+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "automattic/jetpack-constants",
|
"name": "automattic/jetpack-constants",
|
||||||
|
@ -259,6 +259,12 @@
|
||||||
"provider",
|
"provider",
|
||||||
"service"
|
"service"
|
||||||
],
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/philipobenito",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-05-18T08:20:23+00:00"
|
"time": "2020-05-18T08:20:23+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -495,6 +501,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony CssSelector Component",
|
"description": "Symfony CssSelector Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-03-16T08:31:04+00:00"
|
"time": "2020-03-16T08:31:04+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -788,6 +808,20 @@
|
||||||
"constructor",
|
"constructor",
|
||||||
"instantiate"
|
"instantiate"
|
||||||
],
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/phpdoctrine",
|
||||||
|
"type": "patreon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-05-29T17:27:14+00:00"
|
"time": "2020-05-29T17:27:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1050,6 +1084,12 @@
|
||||||
"object",
|
"object",
|
||||||
"object graph"
|
"object graph"
|
||||||
],
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-06-29T13:22:24+00:00"
|
"time": "2020-06-29T13:22:24+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2574,6 +2614,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Finder Component",
|
"description": "Symfony Finder Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-02-14T07:34:21+00:00"
|
"time": "2020-02-14T07:34:21+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2636,6 +2690,20 @@
|
||||||
"polyfill",
|
"polyfill",
|
||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
"time": "2020-07-14T12:35:20+00:00"
|
"time": "2020-07-14T12:35:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3041,5 +3109,6 @@
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "7.1"
|
"php": "7.1"
|
||||||
}
|
},
|
||||||
|
"plugin-api-version": "1.1.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,15 @@
|
||||||
defined( 'ABSPATH' ) || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
'AT' => array(
|
||||||
|
'currency_code' => 'EUR',
|
||||||
|
'currency_pos' => 'left',
|
||||||
|
'thousand_sep' => '.',
|
||||||
|
'decimal_sep' => ',',
|
||||||
|
'num_decimals' => 2,
|
||||||
|
'weight_unit' => 'kg',
|
||||||
|
'dimension_unit' => 'cm',
|
||||||
|
),
|
||||||
'AU' => array(
|
'AU' => array(
|
||||||
'currency_code' => 'AUD',
|
'currency_code' => 'AUD',
|
||||||
'currency_pos' => 'left',
|
'currency_pos' => 'left',
|
||||||
|
@ -54,6 +63,15 @@ return array(
|
||||||
'weight_unit' => 'kg',
|
'weight_unit' => 'kg',
|
||||||
'dimension_unit' => 'cm',
|
'dimension_unit' => 'cm',
|
||||||
),
|
),
|
||||||
|
'CH' => array(
|
||||||
|
'currency_code' => 'CHF',
|
||||||
|
'currency_pos' => 'left_space',
|
||||||
|
'thousand_sep' => "'",
|
||||||
|
'decimal_sep' => '.',
|
||||||
|
'num_decimals' => 2,
|
||||||
|
'weight_unit' => 'kg',
|
||||||
|
'dimension_unit' => 'cm',
|
||||||
|
),
|
||||||
'DE' => array(
|
'DE' => array(
|
||||||
'currency_code' => 'EUR',
|
'currency_code' => 'EUR',
|
||||||
'currency_pos' => 'left',
|
'currency_pos' => 'left',
|
||||||
|
@ -135,6 +153,15 @@ return array(
|
||||||
'weight_unit' => 'kg',
|
'weight_unit' => 'kg',
|
||||||
'dimension_unit' => 'cm',
|
'dimension_unit' => 'cm',
|
||||||
),
|
),
|
||||||
|
'LI' => array(
|
||||||
|
'currency_code' => 'CHF',
|
||||||
|
'currency_pos' => 'left_space',
|
||||||
|
'thousand_sep' => "'",
|
||||||
|
'decimal_sep' => '.',
|
||||||
|
'num_decimals' => 2,
|
||||||
|
'weight_unit' => 'kg',
|
||||||
|
'dimension_unit' => 'cm',
|
||||||
|
),
|
||||||
'MD' => array(
|
'MD' => array(
|
||||||
'currency_code' => 'MDL',
|
'currency_code' => 'MDL',
|
||||||
'currency_pos' => 'right_space',
|
'currency_pos' => 'right_space',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* WC Order Item Shipping Data Store
|
* WC Order Item Shipping Data Store
|
||||||
*
|
*
|
||||||
* @version 3.0.0
|
* @version 3.0.0
|
||||||
* @package data-stores
|
* @package WooCommerce\DataStores
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
|
@ -12,7 +12,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "grunt && npm run makepot && npm run build:packages",
|
"build": "grunt && npm run makepot && npm run build:packages",
|
||||||
"build-watch": "grunt watch",
|
"build-watch": "grunt watch",
|
||||||
"build:packages": "node ./tests/e2e/bin/build.js",
|
"build:packages": "lerna run build",
|
||||||
"build:zip": "./bin/build-zip.sh",
|
"build:zip": "./bin/build-zip.sh",
|
||||||
"lint:js": "eslint assets/js --ext=js",
|
"lint:js": "eslint assets/js --ext=js",
|
||||||
"docker:up": "npm explore @woocommerce/e2e-environment -- npm run docker:up",
|
"docker:up": "npm explore @woocommerce/e2e-environment -- npm run docker:up",
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
"test:e2e-dev": "npm explore @woocommerce/e2e-environment -- npm run test:e2e-dev",
|
"test:e2e-dev": "npm explore @woocommerce/e2e-environment -- npm run test:e2e-dev",
|
||||||
"makepot": "composer run-script makepot",
|
"makepot": "composer run-script makepot",
|
||||||
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
|
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
|
||||||
"publish-packages": "npm run build:packages && lerna publish from-package",
|
"publish-packages": "lerna publish from-package",
|
||||||
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install"
|
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -30,12 +30,16 @@
|
||||||
"@babel/polyfill": "7.10.4",
|
"@babel/polyfill": "7.10.4",
|
||||||
"@babel/preset-env": "7.10.4",
|
"@babel/preset-env": "7.10.4",
|
||||||
"@babel/register": "7.10.4",
|
"@babel/register": "7.10.4",
|
||||||
"@jest/test-sequencer": "^25.0.0",
|
"@jest/test-sequencer": "25.1.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "3.1.0",
|
||||||
|
"@typescript-eslint/parser": "3.1.0",
|
||||||
"@woocommerce/e2e-environment": "file:tests/e2e/env",
|
"@woocommerce/e2e-environment": "file:tests/e2e/env",
|
||||||
|
"@woocommerce/model-factories": "file:tests/e2e/factories",
|
||||||
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
|
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
|
||||||
"@wordpress/babel-preset-default": "3.0.2",
|
"@wordpress/babel-preset-default": "3.0.2",
|
||||||
"@wordpress/e2e-test-utils": "4.6.0",
|
"@wordpress/e2e-test-utils": "4.6.0",
|
||||||
"autoprefixer": "9.8.4",
|
"@wordpress/eslint-plugin": "7.1.0",
|
||||||
|
"autoprefixer": "9.8.6",
|
||||||
"babel-eslint": "10.1.0",
|
"babel-eslint": "10.1.0",
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"chai-as-promised": "7.1.1",
|
"chai-as-promised": "7.1.1",
|
||||||
|
@ -47,7 +51,7 @@
|
||||||
"eslint-config-wpcalypso": "5.0.0",
|
"eslint-config-wpcalypso": "5.0.0",
|
||||||
"eslint-plugin-jest": "23.19.0",
|
"eslint-plugin-jest": "23.19.0",
|
||||||
"github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master",
|
"github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master",
|
||||||
"grunt": "1.1.0",
|
"grunt": "1.2.1",
|
||||||
"grunt-contrib-clean": "2.0.0",
|
"grunt-contrib-clean": "2.0.0",
|
||||||
"grunt-contrib-concat": "1.0.1",
|
"grunt-contrib-concat": "1.0.1",
|
||||||
"grunt-contrib-copy": "1.0.0",
|
"grunt-contrib-copy": "1.0.0",
|
||||||
|
@ -68,11 +72,12 @@
|
||||||
"lint-staged": "9.5.0",
|
"lint-staged": "9.5.0",
|
||||||
"mocha": "7.2.0",
|
"mocha": "7.2.0",
|
||||||
"node-sass": "4.13.0",
|
"node-sass": "4.13.0",
|
||||||
"prettier": "github:automattic/calypso-prettier#c56b4251",
|
"prettier": "npm:wp-prettier@^2.0.5",
|
||||||
"puppeteer": "2.0.0",
|
"puppeteer": "2.0.0",
|
||||||
"puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50",
|
"puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50",
|
||||||
"stylelint": "12.0.1",
|
"stylelint": "12.0.1",
|
||||||
"stylelint-config-wordpress": "16.0.0",
|
"stylelint-config-wordpress": "16.0.0",
|
||||||
|
"typescript": "3.9.5",
|
||||||
"webpack": "4.41.6",
|
"webpack": "4.41.6",
|
||||||
"webpack-cli": "3.3.11",
|
"webpack-cli": "3.3.11",
|
||||||
"wp-textdomain": "^1.0.1"
|
"wp-textdomain": "^1.0.1"
|
||||||
|
@ -100,6 +105,10 @@
|
||||||
"*.js": [
|
"*.js": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"git add"
|
"git add"
|
||||||
|
],
|
||||||
|
"*.ts": [
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|
|
@ -19,13 +19,12 @@ const deasync = require( 'deasync' );
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
const getPackages = require( './get-packages' );
|
|
||||||
const getBabelConfig = require( './get-babel-config' );
|
const getBabelConfig = require( './get-babel-config' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module Constants
|
* Module Constants
|
||||||
*/
|
*/
|
||||||
const PACKAGES_DIR = path.resolve( __dirname, '../' );
|
const PACKAGE_DIR = process.cwd();
|
||||||
const SRC_DIR = 'src';
|
const SRC_DIR = 'src';
|
||||||
const BUILD_DIR = {
|
const BUILD_DIR = {
|
||||||
main: 'build',
|
main: 'build',
|
||||||
|
@ -33,16 +32,6 @@ const BUILD_DIR = {
|
||||||
};
|
};
|
||||||
const DONE = chalk.reset.inverse.bold.green( ' DONE ' );
|
const DONE = chalk.reset.inverse.bold.green( ' DONE ' );
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the package name for a specified file
|
|
||||||
*
|
|
||||||
* @param {string} file File name
|
|
||||||
* @return {string} Package name
|
|
||||||
*/
|
|
||||||
function getPackageName( file ) {
|
|
||||||
return path.relative( PACKAGES_DIR, file ).split( path.sep )[ 0 ];
|
|
||||||
}
|
|
||||||
|
|
||||||
const isJsFile = ( filepath ) => {
|
const isJsFile = ( filepath ) => {
|
||||||
return /.\.js$/.test( filepath );
|
return /.\.js$/.test( filepath );
|
||||||
};
|
};
|
||||||
|
@ -55,9 +44,8 @@ const isJsFile = ( filepath ) => {
|
||||||
* @return {string} Build path
|
* @return {string} Build path
|
||||||
*/
|
*/
|
||||||
function getBuildPath( file, buildFolder ) {
|
function getBuildPath( file, buildFolder ) {
|
||||||
const pkgName = getPackageName( file );
|
const pkgSrcPath = path.resolve( PACKAGE_DIR, SRC_DIR );
|
||||||
const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, SRC_DIR );
|
const pkgBuildPath = path.resolve( PACKAGE_DIR, buildFolder );
|
||||||
const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder );
|
|
||||||
const relativeToSrcPath = path.relative( pkgSrcPath, file );
|
const relativeToSrcPath = path.relative( pkgSrcPath, file );
|
||||||
return path.resolve( pkgBuildPath, relativeToSrcPath );
|
return path.resolve( pkgBuildPath, relativeToSrcPath );
|
||||||
}
|
}
|
||||||
|
@ -121,9 +109,9 @@ function buildJsFileFor( file, silent, environment ) {
|
||||||
if ( ! silent ) {
|
if ( ! silent ) {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
chalk.green( ' \u2022 ' ) +
|
chalk.green( ' \u2022 ' ) +
|
||||||
path.relative( PACKAGES_DIR, file ) +
|
path.relative( PACKAGE_DIR, file ) +
|
||||||
chalk.green( ' \u21D2 ' ) +
|
chalk.green( ' \u21D2 ' ) +
|
||||||
path.relative( PACKAGES_DIR, destPath ) +
|
path.relative( PACKAGE_DIR, destPath ) +
|
||||||
'\n'
|
'\n'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -136,6 +124,15 @@ function buildJsFileFor( file, silent, environment ) {
|
||||||
*/
|
*/
|
||||||
function buildPackage( packagePath ) {
|
function buildPackage( packagePath ) {
|
||||||
const srcDir = path.resolve( packagePath, SRC_DIR );
|
const srcDir = path.resolve( packagePath, SRC_DIR );
|
||||||
|
|
||||||
|
let packageName;
|
||||||
|
try {
|
||||||
|
packageName = require( path.resolve( PACKAGE_DIR, 'package.json' ) ).name;
|
||||||
|
} catch ( e ) {
|
||||||
|
packageName = PACKAGE_DIR.split( path.sep ).pop();
|
||||||
|
}
|
||||||
|
process.stdout.write( chalk.inverse( `>> Building package: ${ packageName }\n` ) );
|
||||||
|
|
||||||
const jsFiles = glob.sync( `${ srcDir }/**/*.js`, {
|
const jsFiles = glob.sync( `${ srcDir }/**/*.js`, {
|
||||||
ignore: [
|
ignore: [
|
||||||
`${ srcDir }/**/test/**/*.js`,
|
`${ srcDir }/**/test/**/*.js`,
|
||||||
|
@ -144,8 +141,6 @@ function buildPackage( packagePath ) {
|
||||||
nodir: true,
|
nodir: true,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
process.stdout.write( `${ path.basename( packagePath ) }\n` );
|
|
||||||
|
|
||||||
// Build js files individually.
|
// Build js files individually.
|
||||||
jsFiles.forEach( ( file ) => buildJsFile( file, true ) );
|
jsFiles.forEach( ( file ) => buildJsFile( file, true ) );
|
||||||
|
|
||||||
|
@ -157,7 +152,5 @@ const files = process.argv.slice( 2 );
|
||||||
if ( files.length ) {
|
if ( files.length ) {
|
||||||
buildFiles( files );
|
buildFiles( files );
|
||||||
} else {
|
} else {
|
||||||
process.stdout.write( chalk.inverse( '>> Building packages \n' ) );
|
buildPackage( PACKAGE_DIR );
|
||||||
getPackages().forEach( buildPackage );
|
|
||||||
process.stdout.write( '\n' );
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
const fs = require( 'fs' );
|
|
||||||
const path = require( 'path' );
|
|
||||||
const { overEvery, compact, includes, negate } = require( 'lodash' );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Absolute path to packages directory.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const PACKAGES_DIR = path.resolve( __dirname, '../' );
|
|
||||||
|
|
||||||
const {
|
|
||||||
/**
|
|
||||||
* Comma-separated string of packages to include in build.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
INCLUDE_PACKAGES,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comma-separated string of packages to exclude from build.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
EXCLUDE_PACKAGES,
|
|
||||||
} = process.env;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a comma-separated string, returns a filter function which returns true
|
|
||||||
* if the item is contained within as a comma-separated entry.
|
|
||||||
*
|
|
||||||
* @param {Function} filterFn Filter function to call with item to test.
|
|
||||||
* @param {string} list Comma-separated list of items.
|
|
||||||
*
|
|
||||||
* @return {Function} Filter function.
|
|
||||||
*/
|
|
||||||
const createCommaSeparatedFilter = ( filterFn, list ) => {
|
|
||||||
const listItems = list.split( ',' );
|
|
||||||
return ( item ) => filterFn( listItems, item );
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given base file name for a file within the packages
|
|
||||||
* directory is itself a directory.
|
|
||||||
*
|
|
||||||
* @param {string} file Packages directory file.
|
|
||||||
*
|
|
||||||
* @return {boolean} Whether file is a directory.
|
|
||||||
*/
|
|
||||||
function isDirectory( file ) {
|
|
||||||
return fs.lstatSync( path.resolve( PACKAGES_DIR, file ) ).isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter predicate, returning true if the given base file name is to be
|
|
||||||
* included in the build.
|
|
||||||
*
|
|
||||||
* @param {string} pkg File base name to test.
|
|
||||||
*
|
|
||||||
* @return {boolean} Whether to include file in build.
|
|
||||||
*/
|
|
||||||
const filterPackages = overEvery( compact( [
|
|
||||||
isDirectory,
|
|
||||||
INCLUDE_PACKAGES && createCommaSeparatedFilter( includes, INCLUDE_PACKAGES ),
|
|
||||||
EXCLUDE_PACKAGES && createCommaSeparatedFilter( negate( includes ), EXCLUDE_PACKAGES ),
|
|
||||||
] ) );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute path of all WordPress packages
|
|
||||||
*
|
|
||||||
* @return {Array} Package paths
|
|
||||||
*/
|
|
||||||
function getPackages() {
|
|
||||||
return fs
|
|
||||||
.readdirSync( PACKAGES_DIR )
|
|
||||||
.filter( filterPackages )
|
|
||||||
.map( ( file ) => path.resolve( PACKAGES_DIR, file ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = getPackages;
|
|
|
@ -5,3 +5,6 @@ echo "Initializing WooCommerce E2E"
|
||||||
wp plugin install woocommerce --activate
|
wp plugin install woocommerce --activate
|
||||||
wp theme install twentynineteen --activate
|
wp theme install twentynineteen --activate
|
||||||
wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html
|
wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --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
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"clean": "rm -rf ./build ./build-module",
|
||||||
|
"compile": "node ./../bin/build.js",
|
||||||
|
"build": "npm run clean && npm run compile",
|
||||||
|
"prepare": "npm run build",
|
||||||
"docker:up": "./bin/docker-compose.js up",
|
"docker:up": "./bin/docker-compose.js up",
|
||||||
"docker:down": "./bin/docker-compose.js down",
|
"docker:down": "./bin/docker-compose.js down",
|
||||||
"docker:clear-all": "docker rmi --force $(docker images -q)",
|
"docker:clear-all": "docker rmi --force $(docker images -q)",
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/dist/
|
||||||
|
/node_modules
|
||||||
|
.eslintrc.js
|
||||||
|
.gitignore
|
||||||
|
jest.config.js
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
tsconfig.json
|
|
@ -0,0 +1,31 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
env: {
|
||||||
|
'jest/globals': true
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'dist/',
|
||||||
|
'node_modules/'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:@wordpress/eslint-plugin/recommended-with-formatting'
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
'files': [ '**/*.ts' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'files': [
|
||||||
|
'**/*.spec.ts',
|
||||||
|
'**/*.test.ts'
|
||||||
|
],
|
||||||
|
'rules': {
|
||||||
|
'no-console': 'off',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
project.xml
|
||||||
|
project.properties
|
||||||
|
/nbproject/private/
|
||||||
|
.buildpath
|
||||||
|
.project
|
||||||
|
.settings*
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
.sublimelinterrc
|
||||||
|
|
||||||
|
# Build Artifacts
|
||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
|
tsconfig.tsbuildinfo
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Model Factories
|
||||||
|
|
||||||
|
A simple interface for generating models of different types.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
``bash
|
||||||
|
npm install @woocommerce/model-factories --save-dev
|
||||||
|
``
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Consumers of this package should rely on an instance of `ModelRegistry` to access the factories.
|
||||||
|
Here is an example of how to initialize and use the package to generate a simple product:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
AdapterTypes,
|
||||||
|
initializeUsingBasicAuth,
|
||||||
|
ModelRegistry,
|
||||||
|
registerSimpleProduct,
|
||||||
|
SimpleProduct
|
||||||
|
} from '@woocommerce/model-factories';
|
||||||
|
|
||||||
|
// The ModelRegistry instance is where all of the factories and adapters are stored in an easy-to-access way.
|
||||||
|
const modelRegistry = new ModelRegistry()
|
||||||
|
|
||||||
|
// Call the register functions to add a kind of factory to the model registry.
|
||||||
|
// This will also add any adapters we've created for the factory, allowing it
|
||||||
|
// to be created on the server.
|
||||||
|
registerSimpleProduct( modelRegistry );
|
||||||
|
|
||||||
|
// Before you can use the included API adapter you need to initialize it using one of the utility methods.
|
||||||
|
// If you do not initialize the API adapters they will not be able to make requests to the API.
|
||||||
|
// Note that these utility functions only set up adapters that have been registered already
|
||||||
|
// and so further calls to `registeryXXX` functions will have adapters that aren't ready.
|
||||||
|
initializeUsingBasicAuth( modelRegistry, 'https://test.test/wp-json', 'admin', 'password' );
|
||||||
|
initializeUsingOAuth( modelRegistry, 'https://test.test/wp-json', 'consumer_key', 'consumer_secret' );
|
||||||
|
|
||||||
|
// In order to actually create the models on the server, each registered factory must have an adapter set.
|
||||||
|
// You can do this on a per-factory basis using
|
||||||
|
modelRegistry.changeFactoryAdapter( SimpleProduct, AdapterTypes.API );
|
||||||
|
// You can do this to all factories registered using
|
||||||
|
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
|
||||||
|
|
||||||
|
// Once all of the initialization has been taken care of you can create models!
|
||||||
|
// Any fields that are not defined will be filled out by random data.
|
||||||
|
const product = await modelRegistry.getFactory( SimpleProduct ).create( { name: 'Test Product' } );
|
||||||
|
// You can now access the ID of the created model using `product.id`!
|
||||||
|
|
||||||
|
// You can also create models in bulk!
|
||||||
|
const poducts = await modelRegistry.getFactory( SimpleProduct ).createList( 5 );
|
||||||
|
// You now have an array of products to work with!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Models
|
||||||
|
|
||||||
|
## Custom Adapters
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testPathIgnorePatterns: [ '/node_modules/', '/dist/' ],
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "@woocommerce/model-factories",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"author": "Automattic",
|
||||||
|
"description": "A simple interface for generating models of different types.",
|
||||||
|
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/factories/README.md",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/woocommerce/woocommerce.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"woocommerce",
|
||||||
|
"e2e"
|
||||||
|
],
|
||||||
|
"license": "GPL-3.0+",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"/dist/",
|
||||||
|
"!*.tsbuildinfo",
|
||||||
|
"!*.spec.js",
|
||||||
|
"!*.spec.d.ts",
|
||||||
|
"!*.test.js",
|
||||||
|
"!*.test.d.ts"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"clean": "rm -rf ./dist ./tsconfig.tsbuildinfo",
|
||||||
|
"compile": "tsc -b",
|
||||||
|
"build": "npm run clean && npm run compile",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "0.19.2",
|
||||||
|
"create-hmac": "1.1.7",
|
||||||
|
"faker": "4.1.0",
|
||||||
|
"fishery": "1.0.0",
|
||||||
|
"oauth-1.0a": "2.2.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/create-hmac": "1.1.0",
|
||||||
|
"@types/faker": "4.1.12",
|
||||||
|
"@types/jest": "25.2.1",
|
||||||
|
"@types/moxios": "0.4.9",
|
||||||
|
"@types/node": "13.13.5",
|
||||||
|
"jest": "25.5.4",
|
||||||
|
"moxios": "0.4.0",
|
||||||
|
"ts-jest": "25.5.0",
|
||||||
|
"typescript": "3.8.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Model } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for implementing adapters to create models.
|
||||||
|
*/
|
||||||
|
export interface Adapter<T extends Model> {
|
||||||
|
/**
|
||||||
|
* Creates a model or array of models using a service..
|
||||||
|
*
|
||||||
|
* @param {Model|Model[]} model The model or array of models to create.
|
||||||
|
* @return {Promise} Resolves to the created input model or array of models.
|
||||||
|
*/
|
||||||
|
create( model: T ): Promise<T>;
|
||||||
|
create( model: T[] ): Promise<T[]>;
|
||||||
|
create( model: T | T[] ): Promise<T> | Promise<T[]>;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Model } from '../model';
|
||||||
|
import { APIAdapter } from './api-adapter';
|
||||||
|
import { SimpleProduct } from '../../models/simple-product';
|
||||||
|
import { APIResponse, APIService } from './api-service';
|
||||||
|
|
||||||
|
class MockAPI implements APIService {
|
||||||
|
public get = jest.fn();
|
||||||
|
public post = jest.fn();
|
||||||
|
public put = jest.fn();
|
||||||
|
public patch = jest.fn();
|
||||||
|
public delete = jest.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe( 'APIModelCreator', () => {
|
||||||
|
let adapter: APIAdapter<Model>;
|
||||||
|
let mockService: MockAPI;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
adapter = new APIAdapter( '/wc/v3/product', () => 'test' );
|
||||||
|
mockService = new MockAPI();
|
||||||
|
adapter.setAPIService( mockService );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should create single instance', async () => {
|
||||||
|
mockService.post.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) );
|
||||||
|
|
||||||
|
const result = await adapter.create( new SimpleProduct() );
|
||||||
|
|
||||||
|
expect( result ).toBeInstanceOf( SimpleProduct );
|
||||||
|
expect( result.id ).toBe( 1 );
|
||||||
|
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||||
|
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should create multiple instances', async () => {
|
||||||
|
mockService.post
|
||||||
|
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) )
|
||||||
|
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 2 } ) ) )
|
||||||
|
.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 3 } ) ) );
|
||||||
|
|
||||||
|
const result = await adapter.create( [ new SimpleProduct(), new SimpleProduct(), new SimpleProduct() ] );
|
||||||
|
|
||||||
|
expect( result ).toBeInstanceOf( Array );
|
||||||
|
expect( result ).toHaveLength( 3 );
|
||||||
|
expect( result[ 0 ].id ).toBe( 1 );
|
||||||
|
expect( result[ 1 ].id ).toBe( 2 );
|
||||||
|
expect( result[ 2 ].id ).toBe( 3 );
|
||||||
|
expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||||
|
expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' );
|
||||||
|
expect( mockService.post.mock.calls[ 1 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||||
|
expect( mockService.post.mock.calls[ 1 ][ 1 ] ).toBe( 'test' );
|
||||||
|
expect( mockService.post.mock.calls[ 2 ][ 0 ] ).toBe( '/wc/v3/product' );
|
||||||
|
expect( mockService.post.mock.calls[ 2 ][ 1 ] ).toBe( 'test' );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { APIResponse, APIService } from './api-service';
|
||||||
|
import { Model } from '../model';
|
||||||
|
import { Adapter } from '../adapter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback for transforming models into an API request body.
|
||||||
|
*
|
||||||
|
* @callback APITransformerFn
|
||||||
|
* @param {Model} model The model that we want to transform.
|
||||||
|
* @return {*} The structured request data for the API.
|
||||||
|
*/
|
||||||
|
export type APITransformerFn<T extends Model> = ( model: T ) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class used for creating data models using a supplied API endpoint.
|
||||||
|
*/
|
||||||
|
export class APIAdapter<T extends Model> implements Adapter<T> {
|
||||||
|
private readonly endpoint: string;
|
||||||
|
private readonly transformer: APITransformerFn<T>;
|
||||||
|
private apiService: APIService | null;
|
||||||
|
|
||||||
|
public constructor( endpoint: string, transformer: APITransformerFn<T> ) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.transformer = transformer;
|
||||||
|
this.apiService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the API service that the adapter should use for creation actions.
|
||||||
|
*
|
||||||
|
* @param {APIService|null} service The new API service for the adapter to use.
|
||||||
|
*/
|
||||||
|
public setAPIService( service: APIService | null ): void {
|
||||||
|
this.apiService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a model or array of models using the API service.
|
||||||
|
*
|
||||||
|
* @param {Model|Model[]} model The model or array of models to create.
|
||||||
|
* @return {Promise} Resolves to the created input model or array of models.
|
||||||
|
*/
|
||||||
|
public create( model: T ): Promise<T>;
|
||||||
|
public create( model: T[] ): Promise<T[]>;
|
||||||
|
public create( model: T | T[] ): Promise<T> | Promise<T[]> {
|
||||||
|
if ( ! this.apiService ) {
|
||||||
|
throw new Error( 'An API service must be registered for the adapter to work.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray( model ) ) {
|
||||||
|
return this.createList( model );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createSingle( model );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a single model using the API service.
|
||||||
|
*
|
||||||
|
* @param {Model} model The model to create.
|
||||||
|
* @return {Promise} Resolves to the created input model.
|
||||||
|
*/
|
||||||
|
private async createSingle( model: T ): Promise<T> {
|
||||||
|
return this.apiService!.post(
|
||||||
|
this.endpoint,
|
||||||
|
this.transformer( model ),
|
||||||
|
).then( ( data: APIResponse ) => {
|
||||||
|
model.setID( data.data.id );
|
||||||
|
return model;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of models using the API service.
|
||||||
|
*
|
||||||
|
* @param {Model[]} models The array of models to create.
|
||||||
|
* @return {Promise} Resolves to the array of created input models.
|
||||||
|
*/
|
||||||
|
private async createList( models: T[] ): Promise<T[]> {
|
||||||
|
const promises: Promise<T>[] = [];
|
||||||
|
for ( const model of models ) {
|
||||||
|
promises.push( this.createSingle( model ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all( promises );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* A structured response from the API.
|
||||||
|
*/
|
||||||
|
export class APIResponse<T = any> {
|
||||||
|
public readonly status: number;
|
||||||
|
public readonly headers: any;
|
||||||
|
public readonly data: T;
|
||||||
|
|
||||||
|
public constructor( status: number, headers: any, data: T ) {
|
||||||
|
this.status = status;
|
||||||
|
this.headers = headers;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A structured error from the API.
|
||||||
|
*/
|
||||||
|
export class APIError {
|
||||||
|
public readonly code: string;
|
||||||
|
public readonly message: string;
|
||||||
|
public readonly data: any;
|
||||||
|
|
||||||
|
public constructor( code: string, message: string, data: any ) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not an APIResponse contains an error.
|
||||||
|
*
|
||||||
|
* @param {APIResponse} response The response to evaluate.
|
||||||
|
*/
|
||||||
|
export function isAPIError( response: APIResponse ): response is APIResponse<APIError> {
|
||||||
|
return response.status < 200 || response.status >= 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for implementing services to make calls against the API.
|
||||||
|
*/
|
||||||
|
export interface APIService {
|
||||||
|
/**
|
||||||
|
* Performs a GET request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} params Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
get<T>(
|
||||||
|
endpoint: string,
|
||||||
|
params?: any
|
||||||
|
): Promise<APIResponse<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a POST request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
post<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any
|
||||||
|
): Promise<APIResponse<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a PUT request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
put<T>( endpoint: string, data?: any ): Promise<APIResponse<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a PATCH request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
patch<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any
|
||||||
|
): Promise<APIResponse<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a DELETE request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
delete<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any
|
||||||
|
): Promise<APIResponse<T>>;
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import moxios from 'moxios';
|
||||||
|
import { APIResponse } from '../api-service';
|
||||||
|
import { AxiosAPIService } from './axios-api-service';
|
||||||
|
|
||||||
|
describe( 'AxiosAPIService', () => {
|
||||||
|
let apiClient: AxiosAPIService;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
moxios.install();
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
moxios.uninstall();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should add OAuth interceptors', async () => {
|
||||||
|
apiClient = AxiosAPIService.createUsingOAuth(
|
||||||
|
'http://test.test/wp-json/',
|
||||||
|
'consumer_key',
|
||||||
|
'consumer_secret',
|
||||||
|
);
|
||||||
|
|
||||||
|
moxios.stubOnce( 'GET', '/wc/v2/product', {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
responseText: JSON.stringify( { test: 'value' } ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const response = await apiClient.get( '/wc/v2/product' );
|
||||||
|
expect( response ).toBeInstanceOf( APIResponse );
|
||||||
|
|
||||||
|
const request = moxios.requests.mostRecent();
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
expect( request.headers.Authorization ).toMatch( /^OAuth/ );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should add basic auth interceptors', async () => {
|
||||||
|
apiClient = AxiosAPIService.createUsingBasicAuth( 'http://test.test/wp-json/', 'test', 'pass' );
|
||||||
|
|
||||||
|
moxios.stubOnce( 'GET', '/wc/v2/product', {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
responseText: JSON.stringify( { test: 'value' } ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const response = await apiClient.get( '/wc/v2/product' );
|
||||||
|
expect( response ).toBeInstanceOf( APIResponse );
|
||||||
|
|
||||||
|
const request = moxios.requests.mostRecent();
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
expect( request.headers.Authorization ).toMatch( /^Basic/ );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,127 @@
|
||||||
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import { APIResponse, APIService } from '../api-service';
|
||||||
|
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
|
||||||
|
import { AxiosInterceptor } from './axios-interceptor';
|
||||||
|
import { AxiosResponseInterceptor } from './axios-response-interceptor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An API service implementation that uses Axios to make requests to the WordPress API.
|
||||||
|
*/
|
||||||
|
export class AxiosAPIService implements APIService {
|
||||||
|
private readonly client: AxiosInstance;
|
||||||
|
private readonly interceptors: AxiosInterceptor[];
|
||||||
|
|
||||||
|
public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) {
|
||||||
|
this.client = axios.create( config );
|
||||||
|
this.interceptors = interceptors;
|
||||||
|
for ( const interceptor of this.interceptors ) {
|
||||||
|
interceptor.start( this.client );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Axios API Service using OAuth 1.0a one-legged authentication.
|
||||||
|
*
|
||||||
|
* @param {string} apiURL The base URL for the API requests to be sent.
|
||||||
|
* @param {string} consumerKey The OAuth consumer key.
|
||||||
|
* @param {string} consumerSecret The OAuth consumer secret.
|
||||||
|
* @return {AxiosAPIService} The created service.
|
||||||
|
*/
|
||||||
|
public static createUsingOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): AxiosAPIService {
|
||||||
|
return new AxiosAPIService(
|
||||||
|
{ baseURL: apiURL },
|
||||||
|
[
|
||||||
|
new AxiosOAuthInterceptor( consumerKey, consumerSecret ),
|
||||||
|
new AxiosResponseInterceptor(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Axios API Service using basic authentication.
|
||||||
|
*
|
||||||
|
* @param {string} apiURL The base URL for the API requests to be sent.
|
||||||
|
* @param {string} username The username for authentication.
|
||||||
|
* @param {string} password The password for authentication.
|
||||||
|
* @return {AxiosAPIService} The created service.
|
||||||
|
*/
|
||||||
|
public static createUsingBasicAuth( apiURL: string, username: string, password: string ): AxiosAPIService {
|
||||||
|
return new AxiosAPIService(
|
||||||
|
{
|
||||||
|
baseURL: apiURL,
|
||||||
|
auth: { username, password },
|
||||||
|
},
|
||||||
|
[ new AxiosResponseInterceptor() ],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a GET request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} params Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
public get<T>(
|
||||||
|
endpoint: string,
|
||||||
|
params?: any,
|
||||||
|
): Promise<APIResponse<T>> {
|
||||||
|
return this.client.get( endpoint, { params } );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a POST request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
public post<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
): Promise<APIResponse<T>> {
|
||||||
|
return this.client.post( endpoint, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a PUT request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
public put<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
): Promise<APIResponse<T>> {
|
||||||
|
return this.client.put( endpoint, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a PATCH request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
public patch<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
): Promise<APIResponse<T>> {
|
||||||
|
return this.client.patch( endpoint, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a DELETE request against the WordPress API.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint The API endpoint we should query.
|
||||||
|
* @param {*} data Any parameters that should be passed in the request.
|
||||||
|
* @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError.
|
||||||
|
*/
|
||||||
|
public delete<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: any,
|
||||||
|
): Promise<APIResponse<T>> {
|
||||||
|
return this.client.delete( endpoint, { data } );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
type ActiveInterceptor = {
|
||||||
|
client: AxiosInstance;
|
||||||
|
requestInterceptorID: number;
|
||||||
|
responseInterceptorID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for encapsulating the start and stop functionality required by all axios interceptors.
|
||||||
|
*/
|
||||||
|
export abstract class AxiosInterceptor {
|
||||||
|
private readonly activeInterceptors: ActiveInterceptor[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts intercepting requests and responses.
|
||||||
|
*
|
||||||
|
* @param {AxiosInstance} client The client to start intercepting the requests/responses of.
|
||||||
|
*/
|
||||||
|
public start( client: AxiosInstance ): void {
|
||||||
|
const requestInterceptorID = client.interceptors.request.use(
|
||||||
|
( response ) => this.handleRequest( response ),
|
||||||
|
);
|
||||||
|
const responseInterceptorID = client.interceptors.response.use(
|
||||||
|
( response ) => this.onResponseSuccess( response ),
|
||||||
|
( error ) => this.onResponseRejected( error ),
|
||||||
|
);
|
||||||
|
this.activeInterceptors.push( { client, requestInterceptorID, responseInterceptorID } );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops intercepting requests and responses.
|
||||||
|
*
|
||||||
|
* @param {AxiosInstance} client The client to stop intercepting the requests/responses of.
|
||||||
|
*/
|
||||||
|
public stop( client: AxiosInstance ): void {
|
||||||
|
for ( let i = this.activeInterceptors.length - 1; i >= 0; --i ) {
|
||||||
|
const active = this.activeInterceptors[ i ];
|
||||||
|
if ( client === active.client ) {
|
||||||
|
client.interceptors.request.eject( active.requestInterceptorID );
|
||||||
|
client.interceptors.response.eject( active.responseInterceptorID );
|
||||||
|
this.activeInterceptors.splice( i, 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interceptor method for handling requests before they are made to the server.
|
||||||
|
*
|
||||||
|
* @param {AxiosRequestConfig} config The axios request options.
|
||||||
|
*/
|
||||||
|
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interceptor method for handling successful responses.
|
||||||
|
*
|
||||||
|
* @param {AxiosResponse} response The response from the axios client.
|
||||||
|
*/
|
||||||
|
protected onResponseSuccess( response: AxiosResponse ): any {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interceptor method for handling response failures.
|
||||||
|
*
|
||||||
|
* @param {*} error The error that occurred.
|
||||||
|
*/
|
||||||
|
protected onResponseRejected( error: any ): any {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import moxios from 'moxios';
|
||||||
|
import { AxiosOAuthInterceptor } from './axios-oauth-interceptor';
|
||||||
|
|
||||||
|
describe( 'AxiosOAuthInterceptor', () => {
|
||||||
|
let apiAuthInterceptor: AxiosOAuthInterceptor;
|
||||||
|
let axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
axiosInstance = axios.create();
|
||||||
|
moxios.install( axiosInstance );
|
||||||
|
apiAuthInterceptor = new AxiosOAuthInterceptor(
|
||||||
|
'consumer_key',
|
||||||
|
'consumer_secret',
|
||||||
|
);
|
||||||
|
apiAuthInterceptor.start( axiosInstance );
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
apiAuthInterceptor.stop( axiosInstance );
|
||||||
|
moxios.uninstall( axiosInstance );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not run unless started', async () => {
|
||||||
|
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
|
||||||
|
|
||||||
|
apiAuthInterceptor.stop( axiosInstance );
|
||||||
|
await axiosInstance.get( 'https://api.test' );
|
||||||
|
|
||||||
|
let request = moxios.requests.mostRecent();
|
||||||
|
expect( request.headers ).not.toHaveProperty( 'Authorization' );
|
||||||
|
|
||||||
|
apiAuthInterceptor.start( axiosInstance );
|
||||||
|
await axiosInstance.get( 'https://api.test' );
|
||||||
|
|
||||||
|
request = moxios.requests.mostRecent();
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should use basic auth for HTTPS', async () => {
|
||||||
|
moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } );
|
||||||
|
await axiosInstance.get( 'https://api.test' );
|
||||||
|
|
||||||
|
const request = moxios.requests.mostRecent();
|
||||||
|
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
expect( request.headers.Authorization ).toBe(
|
||||||
|
'Basic ' +
|
||||||
|
Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ),
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should use OAuth 1.0a for HTTP', async () => {
|
||||||
|
moxios.stubOnce( 'GET', 'http://api.test', { status: 200 } );
|
||||||
|
await axiosInstance.get( 'http://api.test' );
|
||||||
|
|
||||||
|
const request = moxios.requests.mostRecent();
|
||||||
|
|
||||||
|
// We're going to assume that the oauth-1.0a package added the signature data correctly so we will
|
||||||
|
// focus on ensuring that the header looks roughly correct given what we readily know.
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
expect( request.headers.Authorization ).toMatch(
|
||||||
|
/^OAuth oauth_consumer_key="consumer_key".*oauth_signature_method="HMAC-SHA256".*oauth_version="1.0"/,
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should work with base URL', async () => {
|
||||||
|
moxios.stubOnce( 'GET', '/test', { status: 200 } );
|
||||||
|
await axiosInstance.request( {
|
||||||
|
method: 'GET',
|
||||||
|
baseURL: 'https://api.test/',
|
||||||
|
url: '/test',
|
||||||
|
} );
|
||||||
|
|
||||||
|
const request = moxios.requests.mostRecent();
|
||||||
|
|
||||||
|
expect( request.headers ).toHaveProperty( 'Authorization' );
|
||||||
|
expect( request.headers.Authorization ).toBe(
|
||||||
|
'Basic ' +
|
||||||
|
Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ),
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { AxiosRequestConfig } from 'axios';
|
||||||
|
import createHmac from 'create-hmac';
|
||||||
|
import OAuth from 'oauth-1.0a';
|
||||||
|
import { AxiosInterceptor } from './axios-interceptor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class for managing the lifecycle of an authentication interceptor.
|
||||||
|
*/
|
||||||
|
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||||
|
private oauth: OAuth;
|
||||||
|
|
||||||
|
public constructor( consumerKey: string, consumerSecret: string ) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.oauth = new OAuth( {
|
||||||
|
consumer: {
|
||||||
|
key: consumerKey,
|
||||||
|
secret: consumerSecret,
|
||||||
|
},
|
||||||
|
signature_method: 'HMAC-SHA256',
|
||||||
|
hash_function: ( base: any, key: any ) => {
|
||||||
|
return createHmac( 'sha256', key ).update( base ).digest( 'base64' );
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds WooCommerce API authentication details to the outgoing request.
|
||||||
|
*
|
||||||
|
* @param {AxiosRequestConfig} request The request that was intercepted.
|
||||||
|
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
||||||
|
*/
|
||||||
|
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
||||||
|
const url = ( request.baseURL || '' ) + ( request.url || '' );
|
||||||
|
if ( url.startsWith( 'https' ) ) {
|
||||||
|
request.auth = {
|
||||||
|
username: this.oauth.consumer.key,
|
||||||
|
password: this.oauth.consumer.secret,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
request.headers.Authorization = this.oauth.toHeader(
|
||||||
|
this.oauth.authorize( {
|
||||||
|
url,
|
||||||
|
method: request.method!,
|
||||||
|
} ),
|
||||||
|
).Authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import moxios from 'moxios';
|
||||||
|
import { APIResponse, APIError } from '../api-service';
|
||||||
|
import { AxiosResponseInterceptor } from './axios-response-interceptor';
|
||||||
|
|
||||||
|
describe( 'AxiosResponseInterceptor', () => {
|
||||||
|
let apiResponseInterceptor: AxiosResponseInterceptor;
|
||||||
|
let axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
axiosInstance = axios.create();
|
||||||
|
moxios.install( axiosInstance );
|
||||||
|
apiResponseInterceptor = new AxiosResponseInterceptor();
|
||||||
|
apiResponseInterceptor.start( axiosInstance );
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
apiResponseInterceptor.stop( axiosInstance );
|
||||||
|
moxios.uninstall();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should transform responses into APIResponse', async () => {
|
||||||
|
moxios.stubOnce( 'GET', 'http://test.test', {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
responseText: JSON.stringify( { test: 'value' } ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const response = await axiosInstance.get( 'http://test.test' );
|
||||||
|
|
||||||
|
expect( response ).toMatchObject( {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
test: 'value',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should transform response errors into APIError', async () => {
|
||||||
|
moxios.stubOnce( 'GET', 'http://test.test', {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
responseText: JSON.stringify( { code: 'error_code', message: 'value', data: null } ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject(
|
||||||
|
new APIResponse(
|
||||||
|
404,
|
||||||
|
{ 'content-type': 'application/json' },
|
||||||
|
new APIError( 'error_code', 'value', null ),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { APIResponse, APIError } from '../api-service';
|
||||||
|
import { AxiosInterceptor } from './axios-interceptor';
|
||||||
|
|
||||||
|
export class AxiosResponseInterceptor extends AxiosInterceptor {
|
||||||
|
/**
|
||||||
|
* Transforms the Axios response into our API response to be consumed in a consistent manner.
|
||||||
|
*
|
||||||
|
* @param {AxiosResponse} response The respons ethat we need to transform.
|
||||||
|
* @return {Promise} A promise containing the APIResponse.
|
||||||
|
*/
|
||||||
|
protected onResponseSuccess( response: AxiosResponse ): Promise<APIResponse> {
|
||||||
|
return Promise.resolve<APIResponse>(
|
||||||
|
new APIResponse( response.status, response.headers, response.data ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms HTTP errors into an API error if the error came from the API.
|
||||||
|
*
|
||||||
|
* @param {*} error The error that was caught.
|
||||||
|
*/
|
||||||
|
protected onResponseRejected( error: any ): Promise<APIResponse> {
|
||||||
|
// Only transform API errors.
|
||||||
|
if ( ! error.response ) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIResponse(
|
||||||
|
error.response.status,
|
||||||
|
error.response.headers,
|
||||||
|
new APIError(
|
||||||
|
error.response.data.code,
|
||||||
|
error.response.data.message,
|
||||||
|
error.response.data.data,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* CORE CLASSES
|
||||||
|
* These exports relate to extending the core functionality of the package.
|
||||||
|
*/
|
||||||
|
export { Adapter } from './adapter';
|
||||||
|
export { ModelFactory } from './model-factory';
|
||||||
|
export { Model } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API ADAPTER
|
||||||
|
* These exports relate to replacing the underlying HTTP layer of API adapters.
|
||||||
|
*/
|
||||||
|
export { APIAdapter } from './api/api-adapter';
|
||||||
|
export { APIService, APIResponse, APIError } from './api/api-service';
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ModelFactory } from './model-factory';
|
||||||
|
import { Adapter } from './adapter';
|
||||||
|
import { Product } from '../models/product';
|
||||||
|
import { SimpleProduct } from '../models/simple-product';
|
||||||
|
|
||||||
|
class MockAdapter implements Adapter<Product> {
|
||||||
|
public create = jest.fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe( 'ModelFactory', () => {
|
||||||
|
let mockAdapter: MockAdapter;
|
||||||
|
let factory: ModelFactory<Product>;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
mockAdapter = new MockAdapter();
|
||||||
|
factory = ModelFactory.define<Product, any, ModelFactory<Product>>(
|
||||||
|
( { params } ) => {
|
||||||
|
return new SimpleProduct( params );
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should error without adapter', async () => {
|
||||||
|
expect( () => factory.create() ).toThrowError( /no adapter/ );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should create using adapter', async () => {
|
||||||
|
factory.setAdapter( mockAdapter );
|
||||||
|
|
||||||
|
const expectedModel = new SimpleProduct( { name: 'test2' } );
|
||||||
|
expectedModel.setID( 1 );
|
||||||
|
mockAdapter.create.mockReturnValueOnce( Promise.resolve( expectedModel ) );
|
||||||
|
|
||||||
|
const created = await factory.create( { name: 'test' } );
|
||||||
|
|
||||||
|
expect( mockAdapter.create.mock.calls ).toHaveLength( 1 );
|
||||||
|
expect( created ).toBeInstanceOf( Product );
|
||||||
|
expect( created.id ).toBe( 1 );
|
||||||
|
expect( created.name ).toBe( 'test2' );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { DeepPartial, Factory, BuildOptions } from 'fishery';
|
||||||
|
import { Model } from './model';
|
||||||
|
import { Adapter } from './adapter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory that can be used to create models using an adapter.
|
||||||
|
*/
|
||||||
|
export class ModelFactory<T extends Model, I = any> extends Factory<T, I> {
|
||||||
|
private adapter: Adapter<T> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the adapter that the factory will use to create models.
|
||||||
|
*
|
||||||
|
* @param {Adapter|null} adapter
|
||||||
|
*/
|
||||||
|
public setAdapter( adapter: Adapter<T> | null ): void {
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an object using your factory
|
||||||
|
*
|
||||||
|
* @param {DeepPartial} params The parameters that should populate the object.
|
||||||
|
* @param {BuildOptions} options The options to be used in the builder.
|
||||||
|
* @return {Promise} Resolves to the created model.
|
||||||
|
*/
|
||||||
|
public create( params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T> {
|
||||||
|
if ( ! this.adapter ) {
|
||||||
|
throw new Error( 'The factory has no adapter to create using.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = this.build( params, options );
|
||||||
|
return this.adapter.create( model );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an array of objects using your factory
|
||||||
|
*
|
||||||
|
* @param {number} number The number of models to create.
|
||||||
|
* @param {DeepPartial} params The parameters that should populate the object.
|
||||||
|
* @param {BuildOptions} options The options to be used in the builder.
|
||||||
|
* @return {Promise} Resolves to the created model.
|
||||||
|
*/
|
||||||
|
public createList( number: number, params?: DeepPartial<T>, options?: BuildOptions<T, I> ): Promise<T[]> {
|
||||||
|
if ( ! this.adapter ) {
|
||||||
|
throw new Error( 'The factory has no adapter to create using.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = this.buildList( number, params, options );
|
||||||
|
return this.adapter.create( model );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { AdapterTypes, ModelRegistry } from './model-registry';
|
||||||
|
import { ModelFactory } from './model-factory';
|
||||||
|
import { Product } from '../models/product';
|
||||||
|
import { APIAdapter } from './api/api-adapter';
|
||||||
|
import { SimpleProduct } from '../models/simple-product';
|
||||||
|
|
||||||
|
describe( 'ModelRegistry', () => {
|
||||||
|
let factoryRegistry: ModelRegistry;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
factoryRegistry = new ModelRegistry();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should register factories once', () => {
|
||||||
|
const factory = ModelFactory.define<Product, any, ModelFactory<Product>>( ( { params } ) => {
|
||||||
|
return new SimpleProduct( params );
|
||||||
|
} );
|
||||||
|
|
||||||
|
expect( factoryRegistry.getFactory( SimpleProduct ) ).toBeNull();
|
||||||
|
|
||||||
|
factoryRegistry.registerFactory( SimpleProduct, factory );
|
||||||
|
|
||||||
|
expect( () => factoryRegistry.registerFactory( SimpleProduct, factory ) )
|
||||||
|
.toThrowError( /already been registered/ );
|
||||||
|
|
||||||
|
const loaded = factoryRegistry.getFactory( SimpleProduct );
|
||||||
|
|
||||||
|
expect( loaded ).toBe( factory );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should register adapters once', () => {
|
||||||
|
const adapter = new APIAdapter<Product>( '', ( model ) => model );
|
||||||
|
|
||||||
|
expect( factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API ) ).toBeNull();
|
||||||
|
|
||||||
|
factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter );
|
||||||
|
|
||||||
|
expect( () => factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter ) )
|
||||||
|
.toThrowError( /already been registered/ );
|
||||||
|
|
||||||
|
const loaded = factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API );
|
||||||
|
|
||||||
|
expect( loaded ).toBe( adapter );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { Adapter } from './adapter';
|
||||||
|
import { Model } from './model';
|
||||||
|
import { ModelFactory } from './model-factory';
|
||||||
|
|
||||||
|
type Registry<T> = { [key: string ]: T };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The types of adapters that can be stored in the registry.
|
||||||
|
*
|
||||||
|
* @typedef AdapterTypes
|
||||||
|
* @property {string} API "api"
|
||||||
|
* @property {string} Custom "custom"
|
||||||
|
*/
|
||||||
|
export enum AdapterTypes {
|
||||||
|
API = 'api',
|
||||||
|
Custom = 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry that allows for us to easily manage all of our factories and related state.
|
||||||
|
*/
|
||||||
|
export class ModelRegistry {
|
||||||
|
private readonly factories: Registry<ModelFactory<any>> = {};
|
||||||
|
private readonly adapters: { [key in AdapterTypes]: Registry<Adapter<any>> } = {
|
||||||
|
api: {},
|
||||||
|
custom: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a factory for the class.
|
||||||
|
*
|
||||||
|
* @param {Function} modelClass The class of model we're registering the factory for.
|
||||||
|
* @param {ModelFactory} factory The factory that we're registering.
|
||||||
|
*/
|
||||||
|
public registerFactory<T extends Model>( modelClass: new () => T, factory: ModelFactory<T> ): void {
|
||||||
|
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
|
||||||
|
throw new Error( 'A factory of this type has already been registered for the model class.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.factories[ modelClass.name ] = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a factory that was registered for the class.
|
||||||
|
*
|
||||||
|
* @param {Function} modelClass The class of model for the factory we're fetching.
|
||||||
|
*/
|
||||||
|
public getFactory<T extends Model>( modelClass: new () => T ): ModelFactory<T> | null {
|
||||||
|
if ( this.factories.hasOwnProperty( modelClass.name ) ) {
|
||||||
|
return this.factories[ modelClass.name ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an adapter for the class.
|
||||||
|
*
|
||||||
|
* @param {Function} modelClass The class of model that we're registering the adapter for.
|
||||||
|
* @param {AdapterTypes} type The type of adapter that we're registering.
|
||||||
|
* @param {Adapter} adapter The adapter that we're registering.
|
||||||
|
*/
|
||||||
|
public registerAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes, adapter: Adapter<T> ): void {
|
||||||
|
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
|
||||||
|
throw new Error( 'An adapter of this type has already been registered for the model class.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adapters[ type ][ modelClass.name ] = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an adapter registered for the class.
|
||||||
|
*
|
||||||
|
* @param {Function} modelClass The class of the model for the adapter we're fetching.
|
||||||
|
* @param {AdapterTypes} type The type of adapter we're fetching.
|
||||||
|
*/
|
||||||
|
public getAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): Adapter<T> | null {
|
||||||
|
if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) {
|
||||||
|
return this.adapters[ type ][ modelClass.name ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all of the adapters of a given type from the registry.
|
||||||
|
*
|
||||||
|
* @param {AdapterTypes} type The type of adapters to fetch.
|
||||||
|
*/
|
||||||
|
public getAdapters( type: AdapterTypes ): Adapter<any>[] {
|
||||||
|
return Object.values( this.adapters[ type ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the adapter a factory is using.
|
||||||
|
*
|
||||||
|
* @param {Function} modelClass The class of the model factory we're changing.
|
||||||
|
* @param {AdapterTypes} type The type of adapter to set.
|
||||||
|
*/
|
||||||
|
public changeFactoryAdapter<T extends Model>( modelClass: new () => T, type: AdapterTypes ): void {
|
||||||
|
const factory = this.getFactory( modelClass );
|
||||||
|
if ( ! factory ) {
|
||||||
|
throw new Error( 'No factory defined for this model class.' );
|
||||||
|
}
|
||||||
|
const adapter = this.getAdapter( modelClass, type );
|
||||||
|
if ( ! adapter ) {
|
||||||
|
throw new Error( 'No adapter of this type registered for this model class.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
factory.setAdapter( adapter );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the adapters of all factories to the given type or null if one is not registered for that type.
|
||||||
|
*
|
||||||
|
* @param {AdapterTypes} type The type of adapter to set.
|
||||||
|
*/
|
||||||
|
public changeAllFactoryAdapters( type: AdapterTypes ): void {
|
||||||
|
for ( const key in this.factories ) {
|
||||||
|
this.factories[ key ].setAdapter(
|
||||||
|
this.adapters[ type ][ key ] || null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { DeepPartial } from 'fishery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for all models.
|
||||||
|
*/
|
||||||
|
export abstract class Model {
|
||||||
|
private _id: number = 0;
|
||||||
|
|
||||||
|
protected constructor( partial: DeepPartial<any> = {} ) {
|
||||||
|
Object.assign( this, partial );
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): number {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setID( id: number ): void {
|
||||||
|
this._id = id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* FRAMEWORK CLASSES
|
||||||
|
* These exports relate to the core classes needed to utilize the package.
|
||||||
|
*/
|
||||||
|
export { ModelRegistry, AdapterTypes } from './framework/model-registry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MODELS
|
||||||
|
* This exports all of the models we have defined and their related functions.
|
||||||
|
*/
|
||||||
|
export * from './models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTILITIES
|
||||||
|
* These exports relate to common utilities that can be used to utilize the package.
|
||||||
|
*/
|
||||||
|
export { initializeUsingOAuth, initializeUsingBasicAuth } from './utils';
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { Product } from './product';
|
||||||
|
export { SimpleProduct, registerSimpleProduct } from './simple-product';
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Model } from '../framework/model';
|
||||||
|
import { DeepPartial } from 'fishery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base class for all product types.
|
||||||
|
*/
|
||||||
|
export abstract class Product extends Model {
|
||||||
|
public readonly name: string = '';
|
||||||
|
public readonly regularPrice: string = '';
|
||||||
|
|
||||||
|
protected constructor( partial: DeepPartial<Product> = {} ) {
|
||||||
|
super( partial );
|
||||||
|
Object.assign( this, partial );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { DeepPartial } from 'fishery';
|
||||||
|
import { Product } from './product';
|
||||||
|
import { AdapterTypes, ModelRegistry } from '../framework/model-registry';
|
||||||
|
import { ModelFactory } from '../framework/model-factory';
|
||||||
|
import { APIAdapter } from '../framework/api/api-adapter';
|
||||||
|
import faker from 'faker/locale/en';
|
||||||
|
|
||||||
|
export class SimpleProduct extends Product {
|
||||||
|
public constructor( partial: DeepPartial<SimpleProduct> = {} ) {
|
||||||
|
super( partial );
|
||||||
|
Object.assign( this, partial );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the simple product factory and adapters.
|
||||||
|
*
|
||||||
|
* @param {ModelRegistry} registry The registry to hold the model reference.
|
||||||
|
*/
|
||||||
|
export function registerSimpleProduct( registry: ModelRegistry ): void {
|
||||||
|
if ( null !== registry.getFactory( SimpleProduct ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = ModelFactory.define<SimpleProduct, any, ModelFactory<SimpleProduct>>(
|
||||||
|
( { params } ) => {
|
||||||
|
return new SimpleProduct(
|
||||||
|
{
|
||||||
|
name: params.name ?? faker.commerce.productName(),
|
||||||
|
regularPrice: params.regularPrice ?? faker.commerce.price(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
registry.registerFactory( SimpleProduct, factory );
|
||||||
|
|
||||||
|
const apiAdapter = new APIAdapter<SimpleProduct>(
|
||||||
|
'/wc/v3/products',
|
||||||
|
( model ) => {
|
||||||
|
return {
|
||||||
|
type: 'simple',
|
||||||
|
name: model.name,
|
||||||
|
regular_price: model.regularPrice,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
registry.registerAdapter( SimpleProduct, AdapterTypes.API, apiAdapter );
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { AdapterTypes, ModelRegistry } from './framework/model-registry';
|
||||||
|
import { APIAdapter } from './framework/api/api-adapter';
|
||||||
|
import { AxiosAPIService } from './framework/api/axios/axios-api-service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes all of the APIAdapters with a client to communicate with the API.
|
||||||
|
*
|
||||||
|
* @param {ModelRegistry} registry The model registry that we want to initialize.
|
||||||
|
* @param {string} apiURL The base URL for the API.
|
||||||
|
* @param {string} consumerKey The OAuth consumer key for the API service.
|
||||||
|
* @param {string} consumerSecret The OAuth consumer secret for the API service.
|
||||||
|
*/
|
||||||
|
export function initializeUsingOAuth(
|
||||||
|
registry: ModelRegistry,
|
||||||
|
apiURL: string,
|
||||||
|
consumerKey: string,
|
||||||
|
consumerSecret: string,
|
||||||
|
): void {
|
||||||
|
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
|
||||||
|
if ( ! adapters.length ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiService = AxiosAPIService.createUsingOAuth( apiURL, consumerKey, consumerSecret );
|
||||||
|
for ( const adapter of adapters ) {
|
||||||
|
adapter.setAPIService( apiService );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all of the APIAdapters with a client to communicate with the API.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {ModelRegistry} registry The model registry that we want to initialize.
|
||||||
|
* @param {string} apiURL The base URL for the API.
|
||||||
|
* @param {string} username The username to use for authentication.
|
||||||
|
* @param {string} password The password to use for authentication.
|
||||||
|
*/
|
||||||
|
export function initializeUsingBasicAuth(
|
||||||
|
registry: ModelRegistry,
|
||||||
|
apiURL: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): void {
|
||||||
|
const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter<any>[];
|
||||||
|
if ( ! adapters.length ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiService = AxiosAPIService.createUsingBasicAuth( apiURL, username, password );
|
||||||
|
for ( const adapter of adapters ) {
|
||||||
|
adapter.setAPIService( apiService );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [ "node", "jest", "faker", "axios", "moxios", "create-hmac" ],
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": [ "src/" ]
|
||||||
|
}
|
|
@ -7,6 +7,8 @@
|
||||||
*/
|
*/
|
||||||
import { StoreOwnerFlow } from './flows';
|
import { StoreOwnerFlow } from './flows';
|
||||||
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './index';
|
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './index';
|
||||||
|
import modelRegistry from './factories';
|
||||||
|
import { SimpleProduct } from '@woocommerce/model-factories';
|
||||||
|
|
||||||
const config = require( 'config' );
|
const config = require( 'config' );
|
||||||
const simpleProductName = config.get( 'products.simple.name' );
|
const simpleProductName = config.get( 'products.simple.name' );
|
||||||
|
@ -116,7 +118,7 @@ const completeOnboardingWizard = async () => {
|
||||||
expect( productTypesCheckboxes ).toHaveLength( 8 );
|
expect( productTypesCheckboxes ).toHaveLength( 8 );
|
||||||
|
|
||||||
// Select Physical and Downloadable products
|
// Select Physical and Downloadable products
|
||||||
for ( let i = 0; i < 2; i++ ) {
|
for ( let i = 1; i < 2; i++ ) {
|
||||||
await productTypesCheckboxes[i].click();
|
await productTypesCheckboxes[i].click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,22 +344,11 @@ const completeOldSetupWizard = async () => {
|
||||||
* Create simple product.
|
* Create simple product.
|
||||||
*/
|
*/
|
||||||
const createSimpleProduct = async () => {
|
const createSimpleProduct = async () => {
|
||||||
// Go to "add product" page
|
const product = await modelRegistry.getFactory( SimpleProduct ).create( {
|
||||||
await StoreOwnerFlow.openNewProduct();
|
name: simpleProductName,
|
||||||
|
regularPrice: '9.99'
|
||||||
// Make sure we're on the add order page
|
} );
|
||||||
await expect( page.title() ).resolves.toMatch( 'Add new product' );
|
return product.id;
|
||||||
|
|
||||||
// Set product data
|
|
||||||
await expect( page ).toFill( '#title', simpleProductName );
|
|
||||||
await clickTab( 'General' );
|
|
||||||
await expect( page ).toFill( '#_regular_price', '9.99' );
|
|
||||||
|
|
||||||
await verifyAndPublish();
|
|
||||||
|
|
||||||
const simplePostId = await page.$( '#post_ID' );
|
|
||||||
let simplePostIdValue = ( await ( await simplePostId.getProperty( 'value' ) ).jsonValue() );
|
|
||||||
return simplePostIdValue;
|
|
||||||
} ;
|
} ;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import {
|
||||||
|
AdapterTypes,
|
||||||
|
initializeUsingBasicAuth,
|
||||||
|
ModelRegistry,
|
||||||
|
registerSimpleProduct
|
||||||
|
} from '@woocommerce/model-factories';
|
||||||
|
|
||||||
|
const config = require( 'config' );
|
||||||
|
|
||||||
|
const modelRegistry = new ModelRegistry()
|
||||||
|
|
||||||
|
// Register all of the different factories that we're going to need.
|
||||||
|
registerSimpleProduct( modelRegistry );
|
||||||
|
|
||||||
|
// Make sure to perform the initialization AFTER registering all of the factories, otherwise the adapters might be
|
||||||
|
// missed on subsequent registrations.
|
||||||
|
initializeUsingBasicAuth( modelRegistry,
|
||||||
|
config.get( 'url' ) + '/wp-json',
|
||||||
|
config.get( 'users.admin.username' ),
|
||||||
|
config.get( 'users.admin.password' )
|
||||||
|
);
|
||||||
|
modelRegistry.changeAllFactoryAdapters( AdapterTypes.API );
|
||||||
|
|
||||||
|
export default modelRegistry;
|
|
@ -12,7 +12,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
|
||||||
|
|
||||||
$variations = $product->get_available_variations();
|
$variations = $product->get_available_variations();
|
||||||
|
|
||||||
$this->assertIsArray( $variations[0] );
|
$this->assertTrue( is_array( $variations[0] ) );
|
||||||
$this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] );
|
$this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case {
|
||||||
|
|
||||||
$variations = $product->get_available_variations( 'array' );
|
$variations = $product->get_available_variations( 'array' );
|
||||||
|
|
||||||
$this->assertIsArray( $variations[0] );
|
$this->assertTrue( is_array( $variations[0] ) );
|
||||||
$this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] );
|
$this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"incremental": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"composite": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"references": [
|
||||||
|
{ "path": "tests/e2e/factories" }
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
Loading…
Reference in New Issue