Added plugin upload functionality

This commit is contained in:
Greg 2021-07-15 14:39:32 -06:00
parent 7d3ac49f64
commit 268c07118e
14 changed files with 281 additions and 3 deletions

View File

@ -39,4 +39,5 @@ jobs:
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 }}
run: | run: |
npx wc-e2e test:e2e ./tests/e2e/specs/smoke-tests/update-woocommerce.test.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,8 @@
# 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.
# 0.2.2 # 0.2.2

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",

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

@ -0,0 +1,83 @@
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 checkZip( 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 checkZip = 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,
};

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 } = require( '@woocommerce/e2e-utils' );
const { getRemotePluginZip } = require( '@woocommerce/e2e-environment' );
/**
* External dependencies
*/
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
const nightlyZip = 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip';
const pluginName = 'WooCommerce';
let pluginPath;
describe( '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;