initial e2e-env component, props @jeffstieler

- copied from https://github.com/woocommerce/woocommerce-admin/pull/3717
- 696ed0d1d9
This commit is contained in:
Ron Rennick 2020-04-02 10:41:06 -03:00 committed by Jeff Stieler
parent 18ff3960b7
commit c8d643b4ac
28 changed files with 1454 additions and 0 deletions

View File

@ -0,0 +1,15 @@
# WordPress container environment
WORDPRESS_DB_HOST=db
WORDPRESS_DB_NAME=testdb
WORDPRESS_DB_USER=wordpress
WORDPRESS_DB_PASSWORD=wordpress
WORDPRESS_TABLE_PREFIX=wp_
WORDPRESS_DEBUG=1
# WordPress CLI environment
WORDPRESS_PORT=8084
WORDPRESS_HOST=wordpress-www:80
WORDPRESS_TITLE=WooCommerce Core E2E Test Suite
WORDPRESS_LOGIN=admin
WORDPRESS_PASSWORD=password
WORDPRESS_EMAIL=admin@woocommercecoree2etestsuite.com

View File

@ -0,0 +1,15 @@
module.exports = {
extends: [
'plugin:jest/recommended',
],
env: {
'jest/globals': true,
},
globals: {
page: true,
browser: true,
context: true,
jestPuppeteer: true,
},
plugins: [ 'jest' ],
};

View File

@ -0,0 +1 @@
package-lock=false

View File

@ -0,0 +1,53 @@
version: ~> 1.0
language: php
dist: trusty
sudo: false
cache:
directories:
- vendor
- node_modules
- $HOME/.npm
- $HOME/.composer/cache
branches:
only:
- master
- /release\/.*/
before_install:
- timedatectl
- nvm install --latest-npm
before_script:
- export PATH="$HOME/.composer/vendor/bin:$PATH"
- npm install
- |
if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then
phpenv config-rm xdebug.ini
else
echo "xdebug.ini does not exist"
fi
- |
if [[ ! -z "$WP_VERSION" ]] ; then
composer install --no-dev
npm explore @woocommerce/e2e-env -- npm run install-wp-tests -- wc_e2e_tests root ' ' localhost $WP_VERSION
composer global require "phpunit/phpunit=4.8.*|5.7.*"
fi
- |
if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then
composer install
fi
jobs:
fast_finish: true
include:
- name: E2E Tests
script:
- npm explore @woocommerce/e2e-env -- npm run docker:up
- composer install
- npm run build
- npm explore @woocommerce/e2e-env -- npm run test:e2e

View File

@ -0,0 +1,3 @@
1.0.0 (unreleased)
- Initial release

View File

@ -0,0 +1,174 @@
# End to End Testing Environment
A reusable and extendable E2E testing environment for WooCommerce extensions.
## Installation
```bash
npm install @woocommerce/e2e-env --save
```
## Configuration
The `@woocommerce/e2e-env` package exports configuration objects that can be consumed in JavaScript config files in your project. Additionally, it contains several files to serve as the base for a Docker container and Travis CI setup.
### Babel Config
Extend your project's Babel config to contain the expected presets for E2E testing.
```js
const { babelConfig: e2eBabelConfig } = require( '@woocommerce/e2e-env' );
module.exports = function( api ) {
api.cache( true );
return {
...e2eBabelConfig,
presets: [
...e2eBabelConfig.presets,
'@wordpress/babel-preset-default',
],
....
};
};
```
### ES Lint Config
The E2E environment uses Puppeteer for headless browser testing, which uses certain globals variables. Avoid ES Lint errors by extending the config.
```js
const { esLintConfig: baseConfig } = require( '@woocommerce/e2e-env' );
module.exports = {
...baseConfig,
root: true,
parser: 'babel-eslint',
extends: [
...baseConfig.extends,
'wpcalypso/react',
'plugin:jsx-a11y/recommended',
],
plugins: [
...baseConfig.plugins,
'jsx-a11y',
],
env: {
...baseConfig.env,
browser: true,
node: true,
},
globals: {
...baseConfig.globals,
wp: true,
wpApiSettings: true,
wcSettings: true,
},
....
};
```
### Jest Config
The E2E environment uses Jest as a test runner. Extending the base config is needed in order for Jest to run your project's test files.
```js
const path = require( 'path' );
const { jestConfig: baseE2Econfig } = require( '@woocommerce/e2e-env' );
module.exports = {
...baseE2Econfig,
// Specify the path of your project's E2E tests here.
roots: [ path.resolve( __dirname, '../specs' ) ],
};
```
**NOTE:** Your project's Jest config file is expected to found at: `tests/e2e-tests/config/jest.config.js`.
### Webpack Config
The E2E environment provides a `@woocommerce/e2e-tests` alias for easy use of the WooCommerce E2E test helpers.
```js
const { webpackAlias: coreE2EAlias } = require( '@woocommerce/e2e-env' );
module.exports = {
....
resolve: {
alias: {
...coreE2EAlias,
....
},
},
};
```
### Docker Setup
The E2E environment will look for a `docker-compose.yaml` file in your project root. This will be combined with the base Docker config in the package. This is where you'll map your local project files into the Docker container(s).
```yaml
version: '3.3'
services:
wordpress-www:
volumes:
# This path is relative to the first config file
# which is in node_modules/@woocommerce/e2e-env
- "../../../:/var/www/html/wp-content/plugins/your-project-here"
wordpress-cli:
volumes:
- "../../../:/var/www/html/wp-content/plugins/your-project-here"
```
#### Docker Container Initialization Script
You can provide an initialization script that will run in the WP-CLI Docker container. Place an executable file at `tests/e2e-tests/docker/initialize.sh` in your project and it will be copied into the container and executed. While you can run any commands you wish, the intent here is to use WP-CLI to set up your testing environment. E.g.:
```
#!/bin/bash
echo "Initializing WooCommerce E2E"
wp plugin install woocommerce --activate
wp theme install twentynineteen --activate
wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html
wp post create --post_type=page --post_status=publish --post_title='Ready' --post_content='E2E-tests.'
```
### Travis CI
The E2E environment includes a base `.travis.yml` file that sets up a WordPress testing environment and defines a job for running E2E tests. Opt in to [Travis Build Config Imports](https://docs.travis-ci.com/user/build-config-imports/) using `version: ~> 1.0` in your config file.
```yaml
version: ~> 1.0
import:
- source: node_modules/@woocommerce/e2e-env/.travis.yml
mode: deep_merge_prepend # Merge the package config first.
....
```
## Usage
Start Docker
```bash
npm explore @woocommerce/e2e-env -- npm run docker:up
```
Run E2E Tests
```bash
npm explore @woocommerce/e2e-env -- npm run test:e2e
npm explore @woocommerce/e2e-env -- npm run test:e2e-dev
```
Stop Docker
```bash
npm explore @woocommerce/e2e-env -- npm run docker:down
```

View File

@ -0,0 +1,12 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};

View File

@ -0,0 +1,73 @@
#!/usr/bin/env node
const { spawnSync } = require( 'child_process' );
const program = require( 'commander' );
const path = require( 'path' );
const fs = require( 'fs' );
const getAppPath = require( '../utils/app-root' );
const dockerArgs = [];
let command = '';
program
.command( 'up', 'Start and build the Docker container' )
.command( 'down', 'Stop the Docker container and remove volumes' )
.action( ( cmd, options ) => {
if ( 'up' === options[ 0 ] ) {
command = 'up';
dockerArgs.push( 'up', '--build', '-d' );
}
if ( 'down' === options[ 0 ] ) {
command = 'down';
dockerArgs.push( 'down', '-v' );
}
} )
.parse( process.argv );
const appPath = getAppPath();
const envVars = {};
if ( appPath ) {
// Look for a Docker compose file in the dependent app's path.
const appDockerComposefile = path.resolve( appPath, 'docker-compose.yaml' );
// Specify the app's Docker compose file in our command.
if ( fs.existsSync( appDockerComposefile ) ) {
dockerArgs.unshift( '-f', appDockerComposefile );
}
if ( 'up' === command ) {
// Look for an initialization script in the dependent app.
const appInitFile = path.resolve( appPath, 'tests/e2e-tests/docker/initialize.sh' );
// If found, copy it into the wp-cli Docker context so
// it gets picked up by the entrypoint script.
if ( fs.existsSync( appInitFile ) ) {
fs.copyFileSync(
appInitFile,
path.resolve( __dirname, '../docker/wp-cli/initialize.sh' )
);
}
}
// Provide an "app name" to use in Docker container names.
envVars.APP_NAME = path.basename( appPath );
}
// Ensure that the first Docker compose file loaded is from our local env.
dockerArgs.unshift( '-f', path.resolve( __dirname, '../docker-compose.yaml' ) );
const dockerProcess = spawnSync(
'docker-compose',
dockerArgs,
{
stdio: 'inherit',
env: Object.assign( {}, process.env, envVars ),
}
);
console.log( 'Docker exit code: ' + dockerProcess.status );
// Pass Docker exit code to npm
process.exit( dockerProcess.status );

View File

@ -0,0 +1,71 @@
#!/usr/bin/env node
const { spawnSync } = require( 'child_process' );
const program = require( 'commander' );
const path = require( 'path' );
const fs = require( 'fs' );
const getAppPath = require( '../utils/app-root' );
program
.usage( '<file ...> [options]' )
.option( '--dev', 'Development mode' )
.parse( process.argv );
const testEnvVars = {
NODE_ENV: 'test:e2e',
JEST_PUPPETEER_CONFIG: path.resolve(
__dirname,
'../config/jest-puppeteer.config.js'
),
NODE_CONFIG_DIR: path.resolve(
__dirname,
'../config'
),
};
let jestCommand = 'jest';
const jestArgs = [
'--maxWorkers=1',
'--rootDir=./',
'--verbose',
...program.args,
];
if ( program.dev ) {
testEnvVars.JEST_PUPPETEER_CONFIG = path.resolve(
__dirname,
'../config/jest-puppeteer.dev.config.js'
);
jestCommand = 'npx';
jestArgs.unshift( 'ndb', 'jest' );
}
const envVars = Object.assign( {}, process.env, testEnvVars );
const appPath = getAppPath();
let configPath = path.resolve( __dirname, '../config/jest.config.js' );
// Look for a Jest config in the dependent app's path.
if ( appPath ) {
const appConfig = path.resolve( appPath, 'tests/e2e-tests/config/jest.config.js' );
if ( fs.existsSync( appConfig ) ) {
configPath = appConfig;
}
}
jestArgs.push( '--config=' + configPath );
const jestProcess = spawnSync(
jestCommand,
jestArgs,
{
stdio: 'inherit',
env: envVars,
}
);
console.log( 'Jest exit code: ' + jestProcess.status );
// Pass Jest exit code to npm
process.exit( jestProcess.status );

View File

@ -0,0 +1,179 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]"
exit 1
fi
DB_NAME=$1
DB_USER=$2
# Trim whitespace to work around an issue supplying an empty string through both `npm explore` and `npm run`.
DB_PASS=${3//[[:blank:]]/}
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
SKIP_DB_CREATE=${6-false}
# directories
TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
WP_TESTS_TAG="tags/${WP_VERSION%??}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
# http serves a single offer, whereas https serves multiple. we only want one
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
if [[ -z "$LATEST_VERSION" ]]; then
echo "Latest WordPress version could not be found"
exit 1
fi
WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
set -ex
install_wp() {
if [ -d $WP_CORE_DIR ]; then
return;
fi
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $TMPDIR/wordpress-nightly
download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
# https serves multiple offers, whereas http serves single.
download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
LATEST_VERSION=${WP_VERSION%??}
else
# otherwise, scan the releases and get the most up to date minor version of the major release
local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
fi
if [[ -z "$LATEST_VERSION" ]]; then
local ARCHIVE_NAME="wordpress-$WP_VERSION"
else
local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_test_suite() {
# portable in-place argument for both GNU sed and Mac OSX sed
if [[ $(uname -s) == 'Darwin' ]]; then
local ioption='-i .bak'
else
local ioption='-i'
fi
# set up testing suite if it doesn't yet exist
if [ ! -d $WP_TESTS_DIR ]; then
# set up testing suite
mkdir -p $WP_TESTS_DIR
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
fi
}
install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi
# parse DB_HOST for port or socket references
local PARTS=(${DB_HOST//\:/ })
local DB_HOSTNAME=${PARTS[0]};
local DB_SOCK_OR_PORT=${PARTS[1]};
local EXTRA=""
if ! [ -z $DB_HOSTNAME ] ; then
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
EXTRA=" --socket=$DB_SOCK_OR_PORT"
elif ! [ -z $DB_HOSTNAME ] ; then
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
fi
fi
# create database
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
}
install_deps() {
# Script Variables
BRANCH=$TRAVIS_BRANCH
REPO=$TRAVIS_REPO_SLUG
WORKING_DIR="$PWD"
if [ "$TRAVIS_PULL_REQUEST_BRANCH" != "" ]; then
BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
REPO=$TRAVIS_PULL_REQUEST_SLUG
fi
# checkout dev version of woocommerce
cd "$WP_CORE_DIR/wp-content/plugins"
git clone --depth 1 "https://github.com/woocommerce/woocommerce.git"
# install dependencies
cd woocommerce
npm install
composer install --no-dev
# Back to original dir
cd "$WORKING_DIR"
}
install_wp
install_test_suite
install_db
install_deps

View File

@ -0,0 +1,62 @@
{
"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 Variations"
}
},
"addresses": {
"admin": {
"store": {
"firstname": "John",
"lastname": "Doe",
"company": "Automattic",
"country": "United States (US)",
"addressfirstline": "addr 1",
"addresssecondline": "addr 2",
"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"
}
}
}
}

View File

@ -0,0 +1,5 @@
global.process.env = {
...global.process.env,
// Gutenberg test util functions expect the test url to be at :8889, we change it to 8084.
WP_BASE_URL: 'http://localhost:8084',
};

View File

@ -0,0 +1,14 @@
const Sequencer = require( '@jest/test-sequencer' ).default;
class CustomSequencer extends Sequencer {
sort( tests ) {
// Test structure information
// https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21
const copyTests = Array.from( tests );
return copyTests.sort( ( testA, testB ) =>
testA.path > testB.path ? 1 : -1
);
}
}
module.exports = CustomSequencer;

View File

@ -0,0 +1,8 @@
/** @format */
module.exports = {
launch: {
// Required for the logged out and logged in tests so they don't share app state/token.
browserContext: 'incognito',
},
};

View File

@ -0,0 +1,17 @@
/** @format */
module.exports = {
launch: {
slowMo: process.env.PUPPETEER_SLOWMO ? false : 50,
headless: process.env.PUPPETEER_HEADLESS || false,
ignoreHTTPSErrors: true,
args: [ '--window-size=1920,1080', '--user-agent=chrome' ],
devtools: true,
defaultViewport: {
width: 1280,
height: 800,
},
// Required for the logged out and logged in tests so they don't share app state/token.
browserContext: 'incognito',
},
};

View File

@ -0,0 +1,30 @@
module.exports = {
// Automatically clear mock calls and instances between every test
clearMocks: true,
// An array of file extensions your modules use
moduleFileExtensions: [ 'js' ],
moduleNameMapper: {
'@woocommerce/e2e-tests/(.*)':
'<rootDir>/node_modules/woocommerce/tests/e2e-tests/$1',
},
preset: 'jest-puppeteer',
setupFiles: [ '<rootDir>/config/env.setup.js' ],
// A list of paths to modules that run some code to configure or set up the testing framework
// before each test
setupFilesAfterEnv: [
'<rootDir>/config/jest.setup.js',
'expect-puppeteer',
],
// The glob patterns Jest uses to detect test files
testMatch: [ '**/*.(test|spec).js' ],
// Sort test path alphabetically. This is needed so that `activate-and-setup` tests run first
testSequencer: '<rootDir>/config/jest-custom-sequencer.js',
transformIgnorePatterns: [ 'node_modules/(?!(woocommerce)/)' ],
};

View File

@ -0,0 +1,271 @@
/**
* External dependencies
*/
import { get } from 'lodash';
import {
clearLocalStorage,
enablePageDialogAccept,
isOfflineMode,
setBrowserViewport,
switchUserToAdmin,
switchUserToTest,
visitAdminPage,
} from '@wordpress/e2e-test-utils';
//Set the default test timeout to 30s
let jestTimeoutInMilliSeconds = 30000;
// When running test in the Development mode, the test timeout is increased to 2 minutes which allows for errors to be inspected.
// Use `await jestPuppeteer.debug()` in test code to pause execution.
if (
process.env.JEST_PUPPETEER_CONFIG ===
'tests/e2e-tests/config/jest-puppeteer.dev.config.js'
) {
jestTimeoutInMilliSeconds = 120000;
}
jest.setTimeout( jestTimeoutInMilliSeconds );
/**
* Array of page event tuples of [ eventName, handler ].
*
* @type {Array}
*/
const pageEvents = [];
/**
* Set of console logging types observed to protect against unexpected yet
* handled (i.e. not catastrophic) errors or warnings. Each key corresponds
* to the Puppeteer ConsoleMessage type, its value the corresponding function
* on the console global object.
*
* @type {Object<string,string>}
*/
const OBSERVED_CONSOLE_MESSAGE_TYPES = {
warning: 'warn',
error: 'error',
};
async function setupBrowser() {
await clearLocalStorage();
await setBrowserViewport( 'large' );
}
/**
* Navigates to the post listing screen and bulk-trashes any posts which exist.
*
* @return {Promise} Promise resolving once posts have been trashed.
*/
async function trashExistingPosts() {
await switchUserToAdmin();
// Visit `/wp-admin/edit.php` so we can see a list of posts and delete them.
await visitAdminPage( 'edit.php' );
// If this selector doesn't exist there are no posts for us to delete.
const bulkSelector = await page.$( '#bulk-action-selector-top' );
if ( ! bulkSelector ) {
return;
}
// Select all posts.
await page.waitForSelector( '#cb-select-all-1' );
await page.click( '#cb-select-all-1' );
// Select the "bulk actions" > "trash" option.
await page.select( '#bulk-action-selector-top', 'trash' );
// Submit the form to send all draft/scheduled/published posts to the trash.
await page.click( '#doaction' );
await page.waitForXPath(
'//*[contains(@class, "updated notice")]/p[contains(text(), "moved to the Trash.")]'
);
await switchUserToTest();
}
/**
* Navigates to the product listing screen and bulk-trashes any product which exist.
*
* @return {Promise} Promise resolving once products have been trashed.
*/
async function trashExistingProducts() {
await switchUserToAdmin();
// Visit `/wp-admin/edit.php?post_type=product` so we can see a list of products and delete them.
await visitAdminPage( 'edit.php', 'post_type=product' );
// If this selector doesn't exist there are no products for us to delete.
const bulkSelector = await page.$( '#bulk-action-selector-top' );
if ( ! bulkSelector ) {
return;
}
// Select all products.
await page.waitForSelector( '#cb-select-all-1' );
await page.click( '#cb-select-all-1' );
// Select the "bulk actions" > "trash" option.
await page.select( '#bulk-action-selector-top', 'trash' );
// Submit the form to send all draft/scheduled/published posts to the trash.
await page.click( '#doaction' );
await page.waitForXPath(
'//*[contains(@class, "updated notice")]/p[contains(text(), "moved to the Trash.")]'
);
await switchUserToTest();
}
/**
* Navigates to woocommerce import page and imports sample products.
*
* @return {Promise} Promise resolving once products have been imported.
*/
async function importSampleProducts() {
await switchUserToAdmin();
// Visit Import Products page.
await visitAdminPage(
'edit.php',
'post_type=product&page=product_importer'
);
await page.click( 'a.woocommerce-importer-toggle-advanced-options' );
await page.focus( '#woocommerce-importer-file-url' );
// local path for sample data that is included with woo.
await page.keyboard.type(
'wp-content/plugins/woocommerce/sample-data/sample_products.csv'
);
await page.click( '.wc-actions .button-next' );
await page.waitForSelector( '.wc-importer-mapping-table' );
await page.select(
'.wc-importer-mapping-table tr:nth-child(29) select',
''
);
await page.click( '.wc-actions .button-next' );
await page.waitForXPath(
"//*[@class='woocommerce-importer-done' and contains(., 'Import complete! ')]"
);
await switchUserToTest();
}
/**
* Adds an event listener to the page to handle additions of page event
* handlers, to assure that they are removed at test teardown.
*/
function capturePageEventsForTearDown() {
page.on( 'newListener', ( eventName, listener ) => {
pageEvents.push( [ eventName, listener ] );
} );
}
/**
* Removes all bound page event handlers.
*/
function removePageEvents() {
pageEvents.forEach( ( [ eventName, handler ] ) => {
page.removeListener( eventName, handler );
} );
}
/**
* Adds a page event handler to emit uncaught exception to process if one of
* the observed console logging types is encountered.
*/
function observeConsoleLogging() {
page.on( 'console', ( message ) => {
const type = message.type();
if ( ! OBSERVED_CONSOLE_MESSAGE_TYPES.hasOwnProperty( type ) ) {
return;
}
let text = message.text();
// An exception is made for _blanket_ deprecation warnings: Those
// which log regardless of whether a deprecated feature is in use.
if ( text.includes( 'This is a global warning' ) ) {
return;
}
// A chrome advisory warning about SameSite cookies is informational
// about future changes, tracked separately for improvement in core.
//
// See: https://core.trac.wordpress.org/ticket/37000
// See: https://www.chromestatus.com/feature/5088147346030592
// See: https://www.chromestatus.com/feature/5633521622188032
if (
text.includes( 'A cookie associated with a cross-site resource' )
) {
return;
}
// Viewing posts on the front end can result in this error, which
// has nothing to do with Gutenberg.
if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
return;
}
// Network errors are ignored only if we are intentionally testing
// offline mode.
if (
text.includes( 'net::ERR_INTERNET_DISCONNECTED' ) &&
isOfflineMode()
) {
return;
}
// As of WordPress 5.3.2 in Chrome 79, navigating to the block editor
// (Posts > Add New) will display a console warning about
// non - unique IDs.
// See: https://core.trac.wordpress.org/ticket/23165
if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) {
return;
}
// As of WordPress 5.3.2 in Chrome 79, navigating to the block editor
// (Posts > Add New) will display a console warning about
// non - unique IDs.
// See: https://core.trac.wordpress.org/ticket/23165
if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) {
return;
}
const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ];
// As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of
// type JSHandle for error logging, instead of the expected string.
//
// See: https://github.com/GoogleChrome/puppeteer/issues/3397
//
// The recommendation there to asynchronously resolve the error value
// upon a console event may be prone to a race condition with the test
// completion, leaving a possibility of an error not being surfaced
// correctly. Instead, the logic here synchronously inspects the
// internal object shape of the JSHandle to find the error text. If it
// cannot be found, the default text value is used instead.
text = get(
message.args(),
[ 0, '_remoteObject', 'description' ],
text
);
// Disable reason: We intentionally bubble up the console message
// which, unless the test explicitly anticipates the logging via
// @wordpress/jest-console matchers, will cause the intended test
// failure.
// eslint-disable-next-line no-console
console[ logFunction ]( text );
} );
}
// 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 () => {
capturePageEventsForTearDown();
enablePageDialogAccept();
observeConsoleLogging();
await trashExistingPosts();
await trashExistingProducts();
await setupBrowser();
await importSampleProducts();
} );
afterEach( async () => {
await setupBrowser();
} );
afterAll( () => {
removePageEvents();
} );

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,58 @@
version: '3.3'
services:
db:
container_name: "${APP_NAME}_db"
image: mariadb:10.4
restart: on-failure
environment:
MYSQL_DATABASE: testdb
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
volumes:
- db:/var/lib/mysql
wordpress-www:
container_name: "${APP_NAME}_wordpress-www"
depends_on:
- db
build:
context: ./docker/wordpress
ports:
- ${WORDPRESS_PORT}:80
restart: on-failure
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: testdb
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_TABLE_PREFIX: "wp_"
WORDPRESS_DEBUG: 1
volumes:
- wordpress:/var/www/html
wordpress-cli:
container_name: "${APP_NAME}_wordpress-cli"
depends_on:
- db
- wordpress-www
build:
context: ./docker/wp-cli
restart: on-failure
environment:
WORDPRESS_PORT: 8084
WORDPRESS_HOST: wordpress-www:80
WORDPRESS_TITLE: "WooCommerce Core E2E Test Suite"
WORDPRESS_LOGIN: admin
WORDPRESS_PASSWORD: password
WORDPRESS_EMAIL: "admin@woocommercecoree2etestsuite.com"
DOMAIN_NAME:
volumes:
- wordpress:/var/www/html
volumes:
db:
wordpress:

View File

@ -0,0 +1 @@
FROM wordpress:5.3

View File

@ -0,0 +1,22 @@
FROM wordpress:cli-php7.4
USER root
COPY wait-for-it.sh /usr/local/bin/wait-for-it
RUN chown xfs:xfs /usr/local/bin/wait-for-it && \
chmod +x /usr/local/bin/wait-for-it
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chown xfs:xfs /usr/local/bin/entrypoint.sh && \
chmod +x /usr/local/bin/entrypoint.sh
RUN chown xfs:xfs /home/www-data
COPY initialize.sh /usr/local/bin/initialize.sh
RUN chown xfs:xfs /usr/local/bin/initialize.sh && \
chmod +x /usr/local/bin/initialize.sh
USER xfs
RUN mkdir /home/www-data/.wp-cli && echo "path: /var/www/html" > /home/www-data/.wp-cli/config.yml
USER root
ENTRYPOINT ["entrypoint.sh"]

View File

@ -0,0 +1,65 @@
#!/bin/bash
set -eu
declare -p WORDPRESS_HOST
wait-for-it ${WORDPRESS_HOST} -t 120
## if file exists then exit early because initialization already happened.
if [ -f /var/www/html/.initialized ]
then
echo "The environment has already been initialized."
exit 0
fi
chown xfs:xfs /var/www/html/wp-content
chown xfs:xfs /var/www/html/wp-content/plugins
## switch user
if [ $UID -eq 0 ]; then
user=xfs
dir=/var/www/html
cd "$dir"
exec su -s /bin/bash "$user" "$0" -- "$@"
# nothing will be executed beyond that line,
# because exec replaces running process with the new one
fi
declare -p WORDPRESS_PORT
[[ "${WORDPRESS_PORT}" == 80 ]] && \
URL="http://localhost" || \
URL="http://localhost:${WORDPRESS_PORT}"
if $(wp core is-installed);
then
echo "Wordpress is already installed..."
else
declare -p WORDPRESS_TITLE >/dev/null
declare -p WORDPRESS_LOGIN >/dev/null
declare -p WORDPRESS_PASSWORD >/dev/null
declare -p WORDPRESS_EMAIL >/dev/null
echo "Installing wordpress..."
wp core install \
--url=${URL} \
--title="$WORDPRESS_TITLE" \
--admin_user=${WORDPRESS_LOGIN} \
--admin_password=${WORDPRESS_PASSWORD} \
--admin_email=${WORDPRESS_EMAIL} \
--skip-email
fi
## Check for an initialization script.
declare -r INIT_SCRIPT=$(command -v initialize.sh)
if [[ -x ${INIT_SCRIPT} ]]; then
. "$INIT_SCRIPT"
fi
declare -r CURRENT_DOMAIN=$(wp option get siteurl)
if ! [[ ${CURRENT_DOMAIN} == ${URL} ]]; then
echo "Replacing ${CURRENT_DOMAIN} with ${URL} in database..."
wp search-replace ${CURRENT_DOMAIN} ${URL}
fi
echo "Visit $(wp option get siteurl)"
touch /var/www/html/.initialized

View File

@ -0,0 +1,208 @@
#!/usr/bin/env bash
#source https://github.com/vishnubob/wait-for-it/pull/81
#The MIT License (MIT)
#
#Original work Copyright (c) 2016 Giles Hall: wait-for-it.sh
#Modified work Copyright (c) 2019 iturgeon: wait-for-it.sh
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of
#this software and associated documentation files (the "Software"), to deal in
#the Software without restriction, including without limitation the rights to
#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
#of the Software, and to permit persons to whom the Software is furnished to do
#so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
WAITFORIT_ISBUSY=0
WAITFORIT_BUSYTIMEFLAG=""
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
# check to see if we're using busybox?
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
fi
# see if timeout.c args have been updated in busybox v1.30.0 or newer
# note: this requires the use of bash on Alpine
if [[ $WAITFORIT_ISBUSY && $(busybox | head -1) =~ ^.*v([[:digit:]]+)\.([[:digit:]]+)\..+$ ]]; then
if [[ ${BASH_REMATCH[1]} -le 1 && ${BASH_REMATCH[2]} -lt 30 ]]; then
# using pre 1.30.0 version with `-t SEC` arg
WAITFORIT_BUSYTIMEFLAG="-t"
fi
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi

View File

@ -0,0 +1,12 @@
// Internal dependencies
const babelConfig = require( './babel.config' );
const esLintConfig = require( './.eslintrc.js' );
const jestConfig = require( './config/jest.config.js' );
const webpackAlias = require( './webpack-alias' );
module.exports = {
babelConfig,
esLintConfig,
jestConfig,
webpackAlias,
};

View File

@ -0,0 +1,49 @@
{
"name": "@woocommerce/e2e-env",
"version": "1.0.0",
"description": "WooCommerce End to End Testing Environment Configuration.",
"author": "Automattic",
"license": "GPL-3.0-or-later",
"keywords": [
"wordpress",
"woocommerce",
"e2e",
"puppeteer"
],
"homepage": "https://github.com/woocommerce/woocommerce-admin/tree/master/packages/e2e-env/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git"
},
"bugs": {
"url": "https://github.com/woocommerce/woocommerce-admin/issues"
},
"main": "index.js",
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/polyfill": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@wordpress/e2e-test-utils": "^4.3.0",
"@wordpress/eslint-plugin": "^4.0.0",
"@wordpress/jest-preset-default": "^5.4.0",
"app-root-path": "^3.0.0",
"jest": "^25.1.0",
"jest-puppeteer": "^4.4.0",
"ndb": "^1.1.5",
"puppeteer": "^2.1.1",
"woocommerce": "git+https://github.com/woocommerce/woocommerce.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"docker:up": "./bin/docker-compose.js up",
"docker:down": "./bin/docker-compose.js down",
"docker:clear-all": "docker rmi --force $(docker images -q)",
"docker:ssh": "docker exec -it woo-blocks_wordpress-www_1 /bin/bash",
"install-wp-tests": "./bin/install-wp-tests.sh",
"test:e2e": "./bin/e2e-test-integration.js",
"test:e2e-dev": "./bin/e2e-test-integration.js --dev"
}
}

View File

@ -0,0 +1,19 @@
const path = require( 'path' );
const getAppRoot = () => {
// Figure out where we're installed.
// Typically will be in node_modules/, but WooCommerce Admin
// uses a local file path (packages/e2e-env).
let appPath = false;
const moduleDir = path.dirname( require.resolve( '@woocommerce/e2e-env' ) );
if ( -1 < moduleDir.indexOf( 'node_modules' ) ) {
appPath = moduleDir.split( 'node_modules' )[ 0 ];
} else if ( -1 < moduleDir.indexOf( 'packages/e2e-env' ) ) {
appPath = moduleDir.split( 'packages/e2e-env' )[ 0 ];
}
return appPath;
};
module.exports = getAppRoot;

View File

@ -0,0 +1,5 @@
const getAppRoot = require( './app-root' );
module.exports = {
getAppRoot,
};

View File

@ -0,0 +1,11 @@
/**
* External dependencies
*/
const path = require( 'path' );
module.exports = {
'@woocommerce/e2e-tests': path.resolve(
__dirname,
'node_modules/woocommerce/tests/e2e-tests'
),
};