Merge pull request #30291 from woocommerce/add/plugin-upload-flow

Added plugin upload functionality
This commit is contained in:
Greg 2021-07-22 13:06:56 -06:00 committed by GitHub
commit e382e03f58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 303 additions and 3 deletions

View File

@ -38,5 +38,7 @@ jobs:
E2E_RETEST: 1 E2E_RETEST: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }} E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }} E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
UPDATE_WC: 1
run: | run: |
npx wc-e2e test:e2e ./tests/e2e/specs/smoke-tests/update-woocommerce.js
npx wc-e2e test:e2e npx wc-e2e test:e2e

1
.gitignore vendored
View File

@ -52,6 +52,7 @@ tests/cli/vendor
/tests/e2e/env/build/ /tests/e2e/env/build/
/tests/e2e/env/build-module/ /tests/e2e/env/build-module/
/tests/e2e/screenshots /tests/e2e/screenshots
/tests/e2e/plugins
/tests/e2e/utils/build/ /tests/e2e/utils/build/
/tests/e2e/utils/build-module/ /tests/e2e/utils/build-module/

10
package-lock.json generated
View File

@ -9232,7 +9232,9 @@
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-each": "25.5.0", "jest-each": "25.5.0",
"jest-puppeteer": "^4.4.0" "jest-puppeteer": "^4.4.0",
"node-stream-zip": "^1.13.6",
"request": "^2.88.2"
}, },
"dependencies": { "dependencies": {
"@automattic/puppeteer-utils": { "@automattic/puppeteer-utils": {
@ -33239,6 +33241,12 @@
} }
} }
}, },
"node-stream-zip": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.13.6.tgz",
"integrity": "sha512-c7tRSVkLNOHvasWgmZ2d86cDgTWEygnkuuHNOY9c0mR3yLZtQTTrGvMaJ/fPs6+LOJn3240y30l5sjLaXFtcvw==",
"dev": true
},
"nopt": { "nopt": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",

View File

@ -1,6 +1,11 @@
# Unreleased # Unreleased
- `updateReadyPageStatus` utility to update the status of the ready page - `updateReadyPageStatus` utility to update the status of the ready page
- Added plugin upload functionality util that provides a method to pull a plugin zip from a remote location
- `getRemotePluginZip( fileUrl )` to get the remote zip. Returns the filepath of the zip location.
- Added plugin zip utility functions:
- `checkNestedZip( zipFilePath, savePath )` checks a plugin zip file for any nested zip files. If one is found, it is extracted. Returns the path where the zip file is located.
- `downloadZip( fileUrl, downloadPath )` downloads a plugin zip file from a remote location to the provided path.
# 0.2.2 # 0.2.2

View File

@ -180,6 +180,22 @@ To implement the Slackbot in your CI:
To test your setup, create a pull request that triggers an error in the E2E tests. To test your setup, create a pull request that triggers an error in the E2E tests.
## Plugin functions
Depending on the testing scenario, you may wish to upload a plugin that can be used in the tests from a remote location.
To download a zip file, you can use `getRemotePluginZip( fileUrl )` to get the remote zip. This returns the filepath of the location where the zip file was downloaded to. For example, you could use this method to download the latest nightly version of WooCommerce:
```javascript
const pluginZipUrl = 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip';
await getRemotePluginZip( pluginZipUrl );
```
The above method also makes use of the following utility methods which can also be used:
- `checkNestedZip( zipFilePath, savePath )` used to check a plugin zip file for any nested zip files. If one is found, it is extracted. Returns the path where the zip file is located.
- `downloadZip( fileUrl, downloadPath )` can be used to directly download a plugin zip file from a remote location to the provided path.
## Additional information ## Additional information
Refer to [`tests/e2e/core-tests`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/core-tests) for some test examples, and [`tests/e2e`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e) for general information on e2e tests. Refer to [`tests/e2e/core-tests`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e/core-tests) for some test examples, and [`tests/e2e`](https://github.com/woocommerce/woocommerce/tree/trunk/tests/e2e) for general information on e2e tests.

View File

@ -30,7 +30,9 @@
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-each": "25.5.0", "jest-each": "25.5.0",
"jest-puppeteer": "^4.4.0" "jest-puppeteer": "^4.4.0",
"request": "^2.88.2",
"node-stream-zip": "^1.13.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.12.0", "@babel/cli": "7.12.0",

85
tests/e2e/env/utils/get-plugin-zip.js vendored Normal file
View File

@ -0,0 +1,85 @@
const path = require( 'path' );
const getAppRoot = require( './app-root' );
const fs = require('fs');
const mkdirp = require( 'mkdirp' );
const request = require('request');
const StreamZip = require('node-stream-zip');
/**
* Upload a plugin zip from a remote location, such as a GitHub URL or other hosted location.
*
* @param {string} fileUrl The URL where the zip file is located.
* @returns {string} The path where the zip file is located.
*/
const getRemotePluginZip = async ( fileUrl ) => {
const appPath = getAppRoot();
const savePath = path.resolve( appPath, 'tests/e2e/plugins' );
mkdirp.sync( savePath );
// Pull the filename from the end of the URL
let fileName = fileUrl.split('/').pop();
let filePath = path.join( savePath, fileName );
// First, download the zip file
await downloadZip( fileUrl, filePath );
// Check for a nested zip and update the filepath
filePath = await checkNestedZip( filePath, savePath );
return filePath;
};
/**
* Check the zip file for any nested zips. If one is found, extract it.
*
* @param {string} zipFilePath The location of the zip file.
* @param {string} savePath The location where to save a nested zip if found.
* @returns {string} The path where the zip file is located.
*/
const checkNestedZip = async ( zipFilePath, savePath ) => {
const zip = new StreamZip.async( { file: zipFilePath } );
const entries = await zip.entries();
for (const entry of Object.values( entries )) {
if ( entry.name.match( /.zip/ )) {
await zip.extract( null, savePath );
await zip.close();
return path.join( savePath, entry.name );
}
}
return zipFilePath;
}
/**
* Download the zip file from a remote location.
*
* @param {string} fileUrl The URL where the zip file is located.
* @param {string} downloadPath The location where to download the zip to.
* @returns {Promise<void>}
*/
const downloadZip = async ( fileUrl, downloadPath ) => {
const options = {
url: fileUrl,
method: 'GET',
encoding: null,
};
// Wrap in a promise to make the request async
return new Promise( function( resolve, reject ) {
request.get(options, function( err, resp, body ) {
if ( err ) {
reject( err );
} else {
resolve( body );
}
})
.pipe( fs.createWriteStream( downloadPath ) );
});
};
module.exports = {
getRemotePluginZip,
checkNestedZip,
downloadZip,
};

View File

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

View File

@ -0,0 +1,37 @@
/**
* Internal dependencies
*/
const { merchant, utils } = require( '@woocommerce/e2e-utils' );
const { getRemotePluginZip } = require( '@woocommerce/e2e-environment' );
/**
* External dependencies
*/
const {
it,
beforeAll,
} = require( '@jest/globals' );
const { UPDATE_WC } = process.env;
const nightlyZip = 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip';
const pluginName = 'WooCommerce';
let pluginPath;
utils.describeIf( UPDATE_WC )( 'WooCommerce plugin can be uploaded and activated', () => {
beforeAll( async () => {
pluginPath = await getRemotePluginZip( nightlyZip );
await merchant.login();
});
afterAll( async () => {
await merchant.logout();
});
it( 'can upload and activate the WooCommerce plugin', async () => {
await merchant.uploadAndActivatePlugin( pluginPath, pluginName );
});
});

View File

@ -8,6 +8,10 @@
- Added new merchant flows: - Added new merchant flows:
- `openWordPressUpdatesPage()` - `openWordPressUpdatesPage()`
- `installAllUpdates()` - `installAllUpdates()`
- Added `getSlug()` helper to return the slug string for a provided string
- Added `describeIf()` to conditionally run a test suite
- Added `itIf()` to conditionally run a test case.
- Added merchant workflows around plugins: `uploadAndActivatePlugin()`, `activatePlugin()`, `deactivatePlugin()`, `deletePlugin()`
# 0.1.5 # 0.1.5

View File

@ -64,6 +64,7 @@ This package provides support for enabling retries in tests:
- `WP_ADMIN_WC_SETTINGS` - WooCommerce settings page root - `WP_ADMIN_WC_SETTINGS` - WooCommerce settings page root
- `WP_ADMIN_NEW_SHIPPING_ZONE` - WooCommerce new shipping zone - `WP_ADMIN_NEW_SHIPPING_ZONE` - WooCommerce new shipping zone
- `WP_ADMIN_WC_EXTENSIONS` - WooCommerce extensions page - `WP_ADMIN_WC_EXTENSIONS` - WooCommerce extensions page
- `WP_ADMIN_PLUGIN_INSTALL` - WordPress plugin install page
#### Front end #### Front end

View File

@ -12,8 +12,10 @@ export const WP_ADMIN_LOGIN = baseUrl + 'wp-login.php';
export const WP_ADMIN_DASHBOARD = baseUrl + 'wp-admin/'; export const WP_ADMIN_DASHBOARD = baseUrl + 'wp-admin/';
export const WP_ADMIN_WP_UPDATES = WP_ADMIN_DASHBOARD + 'update-core.php'; export const WP_ADMIN_WP_UPDATES = WP_ADMIN_DASHBOARD + 'update-core.php';
export const WP_ADMIN_PLUGINS = WP_ADMIN_DASHBOARD + 'plugins.php'; export const WP_ADMIN_PLUGINS = WP_ADMIN_DASHBOARD + 'plugins.php';
export const WP_ADMIN_PLUGIN_INSTALL = WP_ADMIN_DASHBOARD + 'plugin-install.php';
export const WP_ADMIN_PERMALINK_SETTINGS = WP_ADMIN_DASHBOARD + 'options-permalink.php'; export const WP_ADMIN_PERMALINK_SETTINGS = WP_ADMIN_DASHBOARD + 'options-permalink.php';
export const WP_ADMIN_ALL_USERS_VIEW = WP_ADMIN_DASHBOARD + 'users.php'; export const WP_ADMIN_ALL_USERS_VIEW = WP_ADMIN_DASHBOARD + 'users.php';
/** /**
* WooCommerce core post type pages. * WooCommerce core post type pages.
* @type {string} * @type {string}
@ -27,6 +29,7 @@ export const WP_ADMIN_NEW_ORDER = WP_ADMIN_NEW_POST_TYPE + 'shop_order';
export const WP_ADMIN_ALL_PRODUCTS_VIEW = WP_ADMIN_POST_TYPE + 'product'; export const WP_ADMIN_ALL_PRODUCTS_VIEW = WP_ADMIN_POST_TYPE + 'product';
export const WP_ADMIN_NEW_PRODUCT = WP_ADMIN_NEW_POST_TYPE + 'product'; export const WP_ADMIN_NEW_PRODUCT = WP_ADMIN_NEW_POST_TYPE + 'product';
export const WP_ADMIN_IMPORT_PRODUCTS = WP_ADMIN_ALL_PRODUCTS_VIEW + '&page=product_importer'; export const WP_ADMIN_IMPORT_PRODUCTS = WP_ADMIN_ALL_PRODUCTS_VIEW + '&page=product_importer';
/** /**
* WooCommerce settings pages. * WooCommerce settings pages.
* @type {string} * @type {string}
@ -38,6 +41,7 @@ export const WP_ADMIN_ANALYTICS_PAGES = WP_ADMIN_WC_HOME + '&path=%2Fanalytics%2
export const WP_ADMIN_WC_SETTINGS = WP_ADMIN_PLUGIN_PAGE + 'wc-settings&tab='; export const WP_ADMIN_WC_SETTINGS = WP_ADMIN_PLUGIN_PAGE + 'wc-settings&tab=';
export const WP_ADMIN_NEW_SHIPPING_ZONE = WP_ADMIN_WC_SETTINGS + 'shipping&zone_id=new'; export const WP_ADMIN_NEW_SHIPPING_ZONE = WP_ADMIN_WC_SETTINGS + 'shipping&zone_id=new';
export const WP_ADMIN_WC_EXTENSIONS = WP_ADMIN_PLUGIN_PAGE + 'wc-addons'; export const WP_ADMIN_WC_EXTENSIONS = WP_ADMIN_PLUGIN_PAGE + 'wc-addons';
/** /**
* Shop pages. * Shop pages.
* @type {string} * @type {string}
@ -47,6 +51,7 @@ export const SHOP_PRODUCT_PAGE = baseUrl + '?p=';
export const SHOP_CART_PAGE = baseUrl + 'cart'; export const SHOP_CART_PAGE = baseUrl + 'cart';
export const SHOP_CHECKOUT_PAGE = baseUrl + 'checkout/'; export const SHOP_CHECKOUT_PAGE = baseUrl + 'checkout/';
export const SHOP_MY_ACCOUNT_PAGE = baseUrl + 'my-account/'; export const SHOP_MY_ACCOUNT_PAGE = baseUrl + 'my-account/';
/** /**
* Customer account pages. * Customer account pages.
* @type {string} * @type {string}

View File

@ -6,6 +6,7 @@ const flowExpressions = require( './expressions' );
const merchant = require( './merchant' ); const merchant = require( './merchant' );
const shopper = require( './shopper' ); const shopper = require( './shopper' );
const { withRestApi } = require( './with-rest-api' ); const { withRestApi } = require( './with-rest-api' );
const utils = require( './utils' );
module.exports = { module.exports = {
...flowConstants, ...flowConstants,
@ -13,4 +14,5 @@ module.exports = {
merchant, merchant,
shopper, shopper,
withRestApi, withRestApi,
utils,
}; };

View File

@ -25,10 +25,13 @@ const {
WP_ADMIN_ANALYTICS_PAGES, WP_ADMIN_ANALYTICS_PAGES,
WP_ADMIN_ALL_USERS_VIEW, WP_ADMIN_ALL_USERS_VIEW,
WP_ADMIN_IMPORT_PRODUCTS, WP_ADMIN_IMPORT_PRODUCTS,
WP_ADMIN_PLUGIN_INSTALL,
WP_ADMIN_WP_UPDATES, WP_ADMIN_WP_UPDATES,
IS_RETEST_MODE, IS_RETEST_MODE,
} = require( './constants' ); } = require( './constants' );
const { getSlug } = require('./utils');
const baseUrl = config.get( 'url' ); const baseUrl = config.get( 'url' );
const WP_ADMIN_SINGLE_CPT_VIEW = ( postId ) => baseUrl + `wp-admin/post.php?post=${ postId }&action=edit`; const WP_ADMIN_SINGLE_CPT_VIEW = ( postId ) => baseUrl + `wp-admin/post.php?post=${ postId }&action=edit`;
@ -276,7 +279,101 @@ const merchant = {
page.waitForNavigation( { waitUntil: 'networkidle0' } ), page.waitForNavigation( { waitUntil: 'networkidle0' } ),
]); ]);
} }
},
/* Uploads and activates a plugin located at the provided file path. This will also deactivate and delete the plugin if it exists.
*
* @param {string} pluginFilePath The location of the plugin zip file to upload.
* @param {string} pluginName The name of the plugin. For example, `WooCommerce`.
*/
uploadAndActivatePlugin: async ( pluginFilePath, pluginName ) => {
await merchant.openPlugins();
// Deactivate and delete the plugin if it exists
let pluginSlug = getSlug( pluginName );
if ( await page.$( `a#deactivate-${pluginSlug}` ) !== null ) {
await merchant.deactivatePlugin( pluginName, true );
} }
// Open the plugin install page
await page.goto( WP_ADMIN_PLUGIN_INSTALL, {
waitUntil: 'networkidle0',
} );
// Upload the plugin zip
await page.click( 'a.upload-view-toggle' );
await expect( page ).toMatchElement(
'p.install-help',
{
text: 'If you have a plugin in a .zip format, you may install or update it by uploading it here.'
}
);
const uploader = await page.$( 'input[type=file]' );
await uploader.uploadFile( pluginFilePath );
// Manually update the button to `enabled` so we can submit the file
await page.evaluate(() => {
document.getElementById( 'install-plugin-submit' ).disabled = false;
});
// Click to upload the file
await page.click( '#install-plugin-submit' );
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
// Click to activate the plugin
await page.click( '.button-primary' );
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
},
/**
* Activate a given plugin by the plugin's name.
*
* @param {string} pluginName The name of the plugin to activate. For example, `WooCommerce`.
*/
activatePlugin: async ( pluginName ) => {
let pluginSlug = getSlug( pluginName );
await expect( page ).toClick( `a#activate-${pluginSlug}` );
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
},
/**
* Deactivate a plugin by the plugin's name with the option to delete the plugin as well.
*
* @param {string} pluginName The name of the plugin to deactivate. For example, `WooCommerce`.
* @param {Boolean} deletePlugin Pass in `true` to delete the plugin. Defaults to `false`.
*/
deactivatePlugin: async ( pluginName, deletePlugin = false ) => {
let pluginSlug = getSlug( pluginName );
await expect( page ).toClick( `a#deactivate-${pluginSlug}` );
await page.waitForNavigation( { waitUntil: 'networkidle0' } );
if ( deletePlugin ) {
await merchant.deletePlugin( pluginName );
}
},
/**
* Delete a plugin by the plugin's name.
*
* @param {string} pluginName The name of the plugin to delete. For example, `WooCommerce`.
*/
deletePlugin: async ( pluginName ) => {
let pluginSlug = getSlug( pluginName );
await expect( page ).toClick( `a#delete-${pluginSlug}` );
// Wait for Ajax calls to finish
await page.waitForResponse( response => response.status() === 200 );
},
}; };
module.exports = merchant; module.exports = merchant;

View File

@ -0,0 +1,33 @@
/**
* Take a string name and generate the slug for it.
* Example: 'My plugin' => 'my-plugin'
*
* Sourced from: https://gist.github.com/spyesx/561b1d65d4afb595f295
**/
export const getSlug = ( text ) => {
text = text.trim().toLowerCase();
// remove accents, swap ñ for n, etc
const from = 'åàáãäâèéëêìíïîòóöôùúüûñç·/_,:;';
const to = 'aaaaaaeeeeiiiioooouuuunc------';
for (let i = 0, l = from.length; i < l; i++) {
text = text.replace(new RegExp(from.charAt(i), "g"), to.charAt(i));
}
return text
.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-') // collapse dashes
.replace(/^-+/, '') // trim - from start of text
.replace(/-+$/, '') // trim - from end of text
.replace(/-/g, '-');
};
// Conditionally determine whether or not to skip a test suite
export const describeIf = ( condition ) =>
condition ? describe : describe.skip;
// Conditionally determine whether or not to skip a test case
export const itIf = ( condition ) =>
condition ? it : it.skip;