Merge pull request #30291 from woocommerce/add/plugin-upload-flow
Added plugin upload functionality
This commit is contained in:
commit
e382e03f58
|
@ -38,5 +38,7 @@ jobs:
|
|||
E2E_RETEST: 1
|
||||
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
|
||||
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
|
||||
UPDATE_WC: 1
|
||||
run: |
|
||||
npx wc-e2e test:e2e ./tests/e2e/specs/smoke-tests/update-woocommerce.js
|
||||
npx wc-e2e test:e2e
|
||||
|
|
|
@ -52,6 +52,7 @@ tests/cli/vendor
|
|||
/tests/e2e/env/build/
|
||||
/tests/e2e/env/build-module/
|
||||
/tests/e2e/screenshots
|
||||
/tests/e2e/plugins
|
||||
/tests/e2e/utils/build/
|
||||
/tests/e2e/utils/build-module/
|
||||
|
||||
|
|
|
@ -9232,7 +9232,9 @@
|
|||
"app-root-path": "^3.0.0",
|
||||
"jest": "^25.1.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": {
|
||||
"@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": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# Unreleased
|
||||
|
||||
- `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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
"app-root-path": "^3.0.0",
|
||||
"jest": "^25.1.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": {
|
||||
"@babel/cli": "7.12.0",
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
const getAppRoot = require( './app-root' );
|
||||
const { getAppName, getAppBase } = require( './app-name' );
|
||||
const { getTestConfig, getAdminConfig } = require( './test-config' );
|
||||
const { getRemotePluginZip } = require('./get-plugin-zip');
|
||||
const takeScreenshotFor = require( './take-screenshot' );
|
||||
const updateReadyPageStatus = require('./update-ready-page');
|
||||
const consoleUtils = require( './filter-console' );
|
||||
|
@ -11,6 +12,7 @@ module.exports = {
|
|||
getAppName,
|
||||
getTestConfig,
|
||||
getAdminConfig,
|
||||
getRemotePluginZip,
|
||||
takeScreenshotFor,
|
||||
updateReadyPageStatus,
|
||||
...consoleUtils,
|
||||
|
|
|
@ -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 );
|
||||
});
|
||||
|
||||
});
|
|
@ -8,6 +8,10 @@
|
|||
- Added new merchant flows:
|
||||
- `openWordPressUpdatesPage()`
|
||||
- `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
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ This package provides support for enabling retries in tests:
|
|||
- `WP_ADMIN_WC_SETTINGS` - WooCommerce settings page root
|
||||
- `WP_ADMIN_NEW_SHIPPING_ZONE` - WooCommerce new shipping zone
|
||||
- `WP_ADMIN_WC_EXTENSIONS` - WooCommerce extensions page
|
||||
- `WP_ADMIN_PLUGIN_INSTALL` - WordPress plugin install page
|
||||
|
||||
#### Front end
|
||||
|
||||
|
|
|
@ -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_WP_UPDATES = WP_ADMIN_DASHBOARD + 'update-core.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_ALL_USERS_VIEW = WP_ADMIN_DASHBOARD + 'users.php';
|
||||
|
||||
/**
|
||||
* WooCommerce core post type pages.
|
||||
* @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_NEW_PRODUCT = WP_ADMIN_NEW_POST_TYPE + 'product';
|
||||
export const WP_ADMIN_IMPORT_PRODUCTS = WP_ADMIN_ALL_PRODUCTS_VIEW + '&page=product_importer';
|
||||
|
||||
/**
|
||||
* WooCommerce settings pages.
|
||||
* @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_NEW_SHIPPING_ZONE = WP_ADMIN_WC_SETTINGS + 'shipping&zone_id=new';
|
||||
export const WP_ADMIN_WC_EXTENSIONS = WP_ADMIN_PLUGIN_PAGE + 'wc-addons';
|
||||
|
||||
/**
|
||||
* Shop pages.
|
||||
* @type {string}
|
||||
|
@ -47,6 +51,7 @@ export const SHOP_PRODUCT_PAGE = baseUrl + '?p=';
|
|||
export const SHOP_CART_PAGE = baseUrl + 'cart';
|
||||
export const SHOP_CHECKOUT_PAGE = baseUrl + 'checkout/';
|
||||
export const SHOP_MY_ACCOUNT_PAGE = baseUrl + 'my-account/';
|
||||
|
||||
/**
|
||||
* Customer account pages.
|
||||
* @type {string}
|
||||
|
|
|
@ -6,6 +6,7 @@ const flowExpressions = require( './expressions' );
|
|||
const merchant = require( './merchant' );
|
||||
const shopper = require( './shopper' );
|
||||
const { withRestApi } = require( './with-rest-api' );
|
||||
const utils = require( './utils' );
|
||||
|
||||
module.exports = {
|
||||
...flowConstants,
|
||||
|
@ -13,4 +14,5 @@ module.exports = {
|
|||
merchant,
|
||||
shopper,
|
||||
withRestApi,
|
||||
utils,
|
||||
};
|
||||
|
|
|
@ -25,10 +25,13 @@ const {
|
|||
WP_ADMIN_ANALYTICS_PAGES,
|
||||
WP_ADMIN_ALL_USERS_VIEW,
|
||||
WP_ADMIN_IMPORT_PRODUCTS,
|
||||
WP_ADMIN_PLUGIN_INSTALL,
|
||||
WP_ADMIN_WP_UPDATES,
|
||||
IS_RETEST_MODE,
|
||||
} = require( './constants' );
|
||||
|
||||
const { getSlug } = require('./utils');
|
||||
|
||||
const baseUrl = config.get( 'url' );
|
||||
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' } ),
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
/* 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;
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue