diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 893aacd2f..84f9fac5f 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -1,12 +1,10 @@ const fs = require('fs') -const path = require('path') const mkdirp = require('mkdirp') const pify = require('pify') const assert = require('assert') -const os = require('os') -const { By, Builder, until } = require('selenium-webdriver') -const { Command } = require('selenium-webdriver/lib/command') +const { until } = require('selenium-webdriver') +const { buildWebDriver } = require('./webdriver') const fetchMockResponses = require('./fetch-mocks.json') const tinyDelayMs = 200 @@ -33,30 +31,10 @@ module.exports = { async function prepareExtensionForTesting ({ responsive } = {}) { - let driver, extensionId, extensionUrl - const targetBrowser = process.env.SELENIUM_BROWSER - switch (targetBrowser) { - case 'chrome': { - const extPath = path.resolve('dist/chrome') - driver = buildChromeWebDriver(extPath, { responsive }) - await delay(largeDelayMs) - extensionId = await getExtensionIdChrome(driver) - extensionUrl = `chrome-extension://${extensionId}/home.html` - break - } - case 'firefox': { - const extPath = path.resolve('dist/firefox') - driver = buildFirefoxWebdriver({ responsive }) - await installWebExt(driver, extPath) - await delay(largeDelayMs) - extensionId = await getExtensionIdFirefox(driver) - extensionUrl = `moz-extension://${extensionId}/home.html` - break - } - default: { - throw new Error(`prepareExtensionForTesting - unable to prepare extension for unknown browser "${targetBrowser}"`) - } - } + const browser = process.env.SELENIUM_BROWSER + const extensionPath = `dist/${browser}` + const { driver, extensionId, extensionUrl } = await buildWebDriver({ browser, extensionPath, responsive }) + // Depending on the state of the application built into the above directory (extPath) and the value of // METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we // are closing any extraneous windows to reset us to a single window before continuing. @@ -104,56 +82,6 @@ async function setupFetchMocking (driver) { await driver.executeScript(`(${fetchMocking})(${fetchMockResponsesJson})`) } -function buildChromeWebDriver (extPath, opts = {}) { - const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile')) - const args = [ - `load-extension=${extPath}`, - `user-data-dir=${tmpProfile}`, - ] - if (opts.responsive) { - args.push('--auto-open-devtools-for-tabs') - } - return new Builder() - .withCapabilities({ - chromeOptions: { - args, - binary: process.env.SELENIUM_CHROME_BINARY, - }, - }) - .build() -} - -function buildFirefoxWebdriver (opts = {}) { - const driver = new Builder().build() - if (opts.responsive) { - driver.manage().window().setSize(320, 600) - } - return driver -} - -async function getExtensionIdChrome (driver) { - await driver.get('chrome://extensions') - const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")') - return extensionId -} - -async function getExtensionIdFirefox (driver) { - await driver.get('about:debugging#addons') - const extensionId = await driver.wait(until.elementLocated(By.xpath('//dl/div[contains(., \'Internal UUID\')]/dd')), 1000).getText() - return extensionId -} - -async function installWebExt (driver, extension) { - const cmd = await new Command('moz-install-web-ext') - .setParameter('path', path.resolve(extension)) - .setParameter('temporary', true) - - await driver.getExecutor() - .defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install') - - return await driver.schedule(cmd, 'installWebExt(' + extension + ')') -} - async function checkBrowserForConsoleErrors (driver) { const ignoredLogTypes = ['WARNING'] const ignoredErrorMessages = [ diff --git a/test/e2e/webdriver/chrome.js b/test/e2e/webdriver/chrome.js new file mode 100644 index 000000000..08e6aa98d --- /dev/null +++ b/test/e2e/webdriver/chrome.js @@ -0,0 +1,63 @@ +const { Builder } = require('selenium-webdriver') +const chrome = require('selenium-webdriver/chrome') + +/** + * A wrapper around a {@code WebDriver} instance exposing Chrome-specific functionality + */ +class ChromeDriver { + static async build ({ extensionPath, responsive }) { + const args = [ + `load-extension=${extensionPath}`, + ] + if (responsive) { + args.push('--auto-open-devtools-for-tabs') + } + const options = new chrome.Options() + .addArguments(args) + const driver = new Builder() + .forBrowser('chrome') + .setChromeOptions(options) + .build() + const chromeDriver = new ChromeDriver(driver) + const extensionId = await chromeDriver.getExtensionIdByName('MetaMask') + + return { + driver, + extensionUrl: `chrome-extension://${extensionId}/home.html`, + } + } + + /** + * @constructor + * @param {!ThenableWebDriver} driver a {@code WebDriver} instance + */ + constructor (driver) { + this._driver = driver + } + + /** + * Returns the extension ID for the given extension name + * @param {string} extensionName the extension name + * @return {Promise} the extension ID + */ + async getExtensionIdByName (extensionName) { + await this._driver.get('chrome://extensions') + return await this._driver.executeScript(` + const extensions = document.querySelector("extensions-manager").shadowRoot + .querySelector("extensions-item-list").shadowRoot + .querySelectorAll("extensions-item") + + for (let i = 0; i < extensions.length; i++) { + const extension = extensions[i].shadowRoot + const name = extension.querySelector('#name').textContent + if (name === "${extensionName}") { + return extensions[i].getAttribute("id") + } + } + + return undefined + `) + } +} + +module.exports = ChromeDriver diff --git a/test/e2e/webdriver/firefox.js b/test/e2e/webdriver/firefox.js new file mode 100644 index 000000000..747b77159 --- /dev/null +++ b/test/e2e/webdriver/firefox.js @@ -0,0 +1,99 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') +const { Builder, By, until } = require('selenium-webdriver') +const firefox = require('selenium-webdriver/firefox') +const { Command } = require('selenium-webdriver/lib/command') + +/** + * The prefix for temporary Firefox profiles. All Firefox profiles used for e2e tests + * will be created as random directories inside this. + * @type {string} + */ +const TEMP_PROFILE_PATH_PREFIX = path.join(os.tmpdir(), 'MetaMask-Fx-Profile') + +const GeckoDriverCommand = { + INSTALL_ADDON: 'install addon', +} + +/** + * A wrapper around a {@code WebDriver} instance exposing Firefox-specific functionality + */ +class FirefoxDriver { + /** + * Builds a {@link FirefoxDriver} instance + * @param {{extensionPath: string}} options the options for the build + * @return {Promise<{driver: !ThenableWebDriver, extensionUrl: string, extensionId: string}>} + */ + static async build ({ extensionPath, responsive }) { + const templateProfile = fs.mkdtempSync(TEMP_PROFILE_PATH_PREFIX) + const profile = new firefox.Profile(templateProfile) + const options = new firefox.Options() + .setProfile(profile) + const driver = new Builder() + .forBrowser('firefox') + .setFirefoxOptions(options) + .build() + const fxDriver = new FirefoxDriver(driver) + + await fxDriver.init() + + const extensionId = await fxDriver.installExtension(extensionPath) + const internalExtensionId = await fxDriver.getInternalId() + + if (responsive) { + driver.manage().window().setSize(320, 600) + } + + return { + driver, + extensionId, + extensionUrl: `moz-extension://${internalExtensionId}/home.html`, + } + } + + /** + * @constructor + * @param {!ThenableWebDriver} driver a {@code WebDriver} instance + */ + constructor (driver) { + this._driver = driver + } + + /** + * Initializes the driver + * @return {Promise} + */ + async init () { + await this._driver.getExecutor() + .defineCommand( + GeckoDriverCommand.INSTALL_ADDON, + 'POST', + '/session/:sessionId/moz/addon/install', + ) + } + + /** + * Installs the extension at the given path + * @param {string} addonPath the path to the unpacked extension or XPI + * @return {Promise} the extension ID + */ + async installExtension (addonPath) { + const cmd = new Command(GeckoDriverCommand.INSTALL_ADDON) + .setParameter('path', path.resolve(addonPath)) + .setParameter('temporary', true) + + return await this._driver.schedule(cmd) + } + + /** + * Returns the Internal UUID for the given extension + * @return {Promise} the Internal UUID for the given extension + */ + async getInternalId () { + await this._driver.get('about:debugging#addons') + return await this._driver.wait(until.elementLocated(By.xpath('//dl/div[contains(., \'Internal UUID\')]/dd')), 1000).getText() + } +} + +module.exports = FirefoxDriver diff --git a/test/e2e/webdriver/index.js b/test/e2e/webdriver/index.js new file mode 100644 index 000000000..cda00a308 --- /dev/null +++ b/test/e2e/webdriver/index.js @@ -0,0 +1,33 @@ +const { Browser } = require('selenium-webdriver') +const ChromeDriver = require('./chrome') +const FirefoxDriver = require('./firefox') + +const buildWebDriver = async function buildWebDriver ({ browser, extensionPath, responsive }) { + switch (browser) { + case Browser.CHROME: { + const { driver, extensionId, extensionUrl } = await ChromeDriver.build({ extensionPath, responsive }) + + return { + driver, + extensionId, + extensionUrl, + } + } + case Browser.FIREFOX: { + const { driver, extensionId, extensionUrl } = await FirefoxDriver.build({ extensionPath, responsive }) + + return { + driver, + extensionId, + extensionUrl, + } + } + default: { + throw new Error(`Unrecognized browser: ${browser}`) + } + } +} + +module.exports = { + buildWebDriver, +}