From 66c96542440a3f63b229487b4811d9f06f57d5cd Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 20 Jun 2023 11:17:08 +0100 Subject: [PATCH] Implement tests for multiple service worker restarts on the mv3 build (#19293) * implement multiple restart tests * remove console.logs * fix * wip * wip * wip * wip * wip * wip * wip * wip * close stale prs * revert chromedriver version * delete code leftover * remove unlockWallet method --------- Co-authored-by: Brad Decker --- development/build/index.js | 1 + test/e2e/helpers.js | 96 +++- test/e2e/mv3/multiple-restarts.spec.js | 427 ++++++++++++++++++ .../mv3/phishing-warning-sw-restart.spec.js | 13 +- test/e2e/tests/eth-sign.spec.js | 6 +- test/e2e/webdriver/driver.js | 5 + yarn.lock | 12 +- 7 files changed, 535 insertions(+), 25 deletions(-) create mode 100644 test/e2e/mv3/multiple-restarts.spec.js diff --git a/development/build/index.js b/development/build/index.js index c2fdf5095..1ca565043 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -126,6 +126,7 @@ async function defineAndRunBuildTasks() { 'Promise', 'JSON', 'Date', + 'Proxy', // globals sentry needs to function '__SENTRY__', 'appState', diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index f70d9d51a..51d333062 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -530,32 +530,97 @@ const locateAccountBalanceDOM = async (driver, ganacheServer) => { text: `${balance} ETH`, }); }; +const DEFAULT_PRIVATE_KEY = + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC'; +const WALLET_PASSWORD = 'correct horse battery staple'; -const restartServiceWorker = async (driver) => { - const serviceWorkerElements = await driver.findElements({ - text: 'terminate', - tag: 'span', - }); - // 1st one is app-init.js; while 2nd one is service-worker.js - await serviceWorkerElements[1].click(); +const DEFAULT_GANACHE_OPTIONS = { + accounts: [ + { + secretKey: DEFAULT_PRIVATE_KEY, + balance: generateETHBalance(25), + }, + ], }; +const generateGanacheOptions = (overrides) => ({ + ...DEFAULT_GANACHE_OPTIONS, + ...overrides, +}); + async function waitForAccountRendered(driver) { await driver.waitForSelector( '[data-testid="eth-overview__primary-currency"]', ); } +const WINDOW_TITLES = Object.freeze({ + ExtensionInFullScreenView: 'MetaMask', + TestDApp: 'E2E Test Dapp', + Notification: 'MetaMask Notification', + ServiceWorkerSettings: 'Inspect with Chrome Developer Tools', + InstalledExtensions: 'Extensions', +}); -const login = async (driver) => { +const unlockWallet = async (driver) => { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); }; const logInWithBalanceValidation = async (driver, ganacheServer) => { - await login(driver); + await unlockWallet(driver); await assertAccountBalanceForDOM(driver, ganacheServer); }; +function roundToXDecimalPlaces(number, decimalPlaces) { + return Math.round(number * 10 ** decimalPlaces) / 10 ** decimalPlaces; +} + +function generateRandNumBetween(x, y) { + const min = Math.min(x, y); + const max = Math.max(x, y); + const randomNumber = Math.random() * (max - min) + min; + + return randomNumber; +} + +async function switchToWindow(driver, windowTitle) { + const windowHandles = await driver.getAllWindowHandles(); + + return await driver.switchToWindowWithTitle(windowTitle, windowHandles); +} + +async function sleepSeconds(sec) { + return new Promise((resolve) => setTimeout(resolve, sec * 1000)); +} + +async function terminateServiceWorker(driver) { + await driver.openNewPage(SERVICE_WORKER_URL); + + await driver.waitForSelector({ + text: 'Service workers', + tag: 'button', + }); + await driver.clickElement({ + text: 'Service workers', + tag: 'button', + }); + + const serviceWorkerElements = await driver.findElements({ + text: 'terminate', + tag: 'span', + }); + + // 1st one is app-init.js; while 2nd one is service-worker.js + await serviceWorkerElements[serviceWorkerElements.length - 1].click(); + + const serviceWorkerTab = await switchToWindow( + driver, + WINDOW_TITLES.ServiceWorkerSettings, + ); + + await driver.closeWindowHandle(serviceWorkerTab); +} + module.exports = { DAPP_URL, DAPP_ONE_URL, @@ -583,10 +648,19 @@ module.exports = { defaultGanacheOptions, sendTransaction, findAnotherAccountFromAccountList, - login, + unlockWallet, logInWithBalanceValidation, assertAccountBalanceForDOM, locateAccountBalanceDOM, - restartServiceWorker, waitForAccountRendered, + generateGanacheOptions, + WALLET_PASSWORD, + WINDOW_TITLES, + DEFAULT_GANACHE_OPTIONS, + generateETHBalance, + roundToXDecimalPlaces, + generateRandNumBetween, + switchToWindow, + sleepSeconds, + terminateServiceWorker, }; diff --git a/test/e2e/mv3/multiple-restarts.spec.js b/test/e2e/mv3/multiple-restarts.spec.js new file mode 100644 index 000000000..91f34896d --- /dev/null +++ b/test/e2e/mv3/multiple-restarts.spec.js @@ -0,0 +1,427 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + openDapp, + generateGanacheOptions, + WALLET_PASSWORD, + WINDOW_TITLES, + DEFAULT_GANACHE_OPTIONS, + generateETHBalance, + roundToXDecimalPlaces, + generateRandNumBetween, + switchToWindow, + sleepSeconds, + terminateServiceWorker, + unlockWallet, +} = require('../helpers'); +const FixtureBuilder = require('../fixture-builder'); + +describe('MV3 - Restart service worker multiple times', function () { + it('Simple simple send flow within full screen view should still be usable', async function () { + const initialBalance = roundToXDecimalPlaces( + generateRandNumBetween(10, 100), + 4, + ); + + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: generateGanacheOptions({ + accounts: [ + { + secretKey: DEFAULT_GANACHE_OPTIONS.accounts[0].secretKey, + balance: generateETHBalance(initialBalance), + }, + ], + }), + title: this.test.title, + driverOptions: { openDevToolsForTabs: true }, + }, + async ({ driver }) => { + await driver.navigate(); + + await unlockWallet(driver, WALLET_PASSWORD); + + await assertETHBalance(driver, initialBalance); + + // first send ETH and then terminate SW + const RECIPIENT_ADDRESS = '0x985c30949c92df7a0bd42e0f3e3d539ece98db24'; + const amountFirstTx = roundToXDecimalPlaces( + generateRandNumBetween(0.5, 2), + 4, + ); + + const gasFeesFirstTx = await simpleSendETH( + driver, + amountFirstTx, + RECIPIENT_ADDRESS, + ); + const totalAfterFirstTx = roundToXDecimalPlaces( + initialBalance - amountFirstTx - gasFeesFirstTx, + 4, + ); + + await terminateServiceWorker(driver); + + await assertETHBalance(driver, totalAfterFirstTx); + + // first send ETH #2 and then terminate SW + const amountSecondTx = roundToXDecimalPlaces( + generateRandNumBetween(0.5, 2), + 4, + ); + const gasFeesSecondTx = await simpleSendETH( + driver, + amountSecondTx, + RECIPIENT_ADDRESS, + ); + const totalAfterSecondTx = roundToXDecimalPlaces( + initialBalance - + amountFirstTx - + gasFeesFirstTx - + amountSecondTx - + gasFeesSecondTx, + 4, + ); + + await terminateServiceWorker(driver); + + await assertETHBalance(driver, totalAfterSecondTx); + + // first terminate SW and then send ETH + const amountThirdTx = roundToXDecimalPlaces( + generateRandNumBetween(0.5, 2), + 4, + ); + const gasFeesThirdTx = await simpleSendETH( + driver, + amountThirdTx, + RECIPIENT_ADDRESS, + ); + const totalAfterThirdTx = roundToXDecimalPlaces( + initialBalance - + amountFirstTx - + gasFeesFirstTx - + amountSecondTx - + gasFeesSecondTx - + amountThirdTx - + gasFeesThirdTx, + 4, + ); + + await assertETHBalance(driver, totalAfterThirdTx); + }, + ); + + async function simpleSendETH(driver, value, recipient) { + await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView); + + await driver.clickElement('[data-testid="eth-overview-send"]'); + await driver.fill('[data-testid="ens-input"]', recipient); + const formattedValue = `${value}`.replace('.', ','); + await driver.fill('.unit-input__input', formattedValue); + + await driver.clickElement('[data-testid="page-container-footer-next"]'); + + const gasFeesEl = await driver.findElement( + '.transaction-detail-item__detail-values .currency-display-component', + ); + const gasFees = await gasFeesEl.getText(); + + await driver.clickElement('[data-testid="page-container-footer-next"]'); + await driver.clickElement('[data-testid="home__activity-tab"]'); + await driver.findElement('.transaction-list-item'); + // reset view to assets tab + await driver.clickElement('[data-testid="home__asset-tab"]'); + + return gasFees; + } + + async function assertETHBalance(driver, expectedBalance) { + await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView); + + const isETHBalanceOverviewPresentAndVisible = + await driver.isElementPresentAndVisible({ + css: '[data-testid="eth-overview__primary-currency"]', + text: `${expectedBalance} ETH`, + }); + + assert.equal( + isETHBalanceOverviewPresentAndVisible, + true, + `Balance DOM element should be visible and match ${expectedBalance} ETH.`, + ); + } + }); + + it('Should continue to support add network dApp interactions after service worker re-starts multiple times', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: generateGanacheOptions({ + concurrent: { port: 8546, chainId: 1338 }, + }), + title: this.test.title, + driverOptions: { openDevToolsForTabs: true }, + }, + async ({ driver }) => { + await driver.navigate(); + + await unlockWallet(driver, WALLET_PASSWORD); + + await openDapp(driver); + + // Click add Ethereum chain + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await driver.clickElement('#addEthereumChain'); + await driver.waitUntilXWindowHandles(2); + + // Notification pop up opens + await switchToWindow(driver, WINDOW_TITLES.Notification); + let notification = await driver.isElementPresent({ + text: 'Allow this site to add a network?', + tag: 'h3', + }); + assert.ok(notification, 'Dapp action does not appear in Metamask'); + + // Cancel Notification + await driver.clickElement({ text: 'Cancel', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + + // Terminate Service Worker + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await terminateServiceWorker(driver); + + // Click add Ethereum chain #2 + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await driver.clickElement('#addEthereumChain'); + await driver.waitUntilXWindowHandles(2); + + // Notification pop up opens + await switchToWindow(driver, WINDOW_TITLES.Notification); + notification = await driver.isElementPresent({ + text: 'Allow this site to add a network?', + tag: 'h3', + }); + assert.ok(notification, 'Dapp action does not appear in Metamask'); + + // Cancel Notification + await driver.clickElement({ text: 'Cancel', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + + // Terminate Service Worker + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await terminateServiceWorker(driver); + + // Click add Ethereum chain #3 + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await driver.clickElement('#addEthereumChain'); + await driver.waitUntilXWindowHandles(2); + + // Notification pop up opens + await switchToWindow(driver, WINDOW_TITLES.Notification); + notification = await driver.isElementPresent({ + text: 'Allow this site to add a network?', + tag: 'h3', + }); + assert.ok(notification, 'Dapp action does not appear in Metamask'); + + // Accept Notification + await driver.clickElement({ text: 'Approve', tag: 'button' }); + await driver.clickElement({ text: 'Switch network', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + }, + ); + }); + + it('Should continue to support send ETH dApp interactions after service worker re-starts multiple times', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: generateGanacheOptions({ + concurrent: { port: 8546, chainId: 1338 }, + }), + title: this.test.title, + driverOptions: { openDevToolsForTabs: true }, + }, + async ({ driver }) => { + await driver.navigate(); + + await unlockWallet(driver, WALLET_PASSWORD); + + await openDapp(driver); + + await clickSendButton(driver); + await driver.waitUntilXWindowHandles(2); + + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await terminateServiceWorker(driver); + await driver.waitUntilXWindowHandles(2); + + await clickSendButton(driver); + await driver.waitUntilXWindowHandles(2); + + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + await terminateServiceWorker(driver); + + await clickSendButton(driver); + await driver.waitUntilXWindowHandles(2); + + await assertNumberOfTransactionsInPopUp(driver, 3); + + await confirmETHSendNotification(driver, 1); + + await assertNumberOfTransactionsInPopUp(driver, 2); + + await confirmETHSendNotification(driver, 1); + + await confirmETHSendNotification(driver, 1); + }, + ); + + async function clickSendButton(driver) { + // Click send button + await switchToWindow(driver, WINDOW_TITLES.TestDApp); + + await driver.waitForSelector({ + css: '#sendButton', + text: 'Send', + }); + await driver.clickElement('#sendButton'); + } + + async function confirmETHSendNotification(driver, amount) { + await switchToWindow(driver, WINDOW_TITLES.Notification); + + await driver.clickElement({ + text: 'Edit', + tag: 'span', + }); + + await driver.fill('[data-testid="currency-input"]', amount); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + }); + } + + async function assertNumberOfTransactionsInPopUp(driver, number) { + await switchToWindow(driver, WINDOW_TITLES.Notification); + const navEl = await driver.findElement( + '.confirm-page-container-navigation__navtext', + ); + + const notificationProgress = await navEl.getText(); + + assert.ok(notificationProgress, `1 of ${number}`); + } + }); + + it('Should lock wallet when a browser session ends (after turning off the extension)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: generateGanacheOptions({ + concurrent: { port: 8546, chainId: 1338 }, + }), + title: this.test.title, + }, + async ({ driver }) => { + const { extensionUrl } = driver; + const extensionId = extensionUrl.split('//')[1]; + + await driver.navigate(); + + await unlockWallet(driver, WALLET_PASSWORD); + + await reloadExtension(driver, extensionId); + + // ensure extension finishes reloading before reopening full screen extension + await sleepSeconds(0.1); + + await driver.openNewPage(`${extensionUrl}/home.html`); + + const passwordField = await driver.isElementPresent('#password'); + assert.ok( + passwordField, + 'Password screen is not visible. Wallet should have been locked.', + ); + }, + ); + + async function reloadExtension(driver, extensionId) { + await switchToWindow(driver, WINDOW_TITLES.ExtensionInFullScreenView); + + await driver.openNewPage('chrome://extensions/'); + + // extensions-manager + const extensionsManager = await driver.findElement('extensions-manager'); + + // shadowRoot + const extensionsManagerShadowRoot = await driver.executeScript( + 'return arguments[0][0].shadowRoot', + extensionsManager, + ); + + // cr-view-manager + const viewManager = await extensionsManagerShadowRoot.findElement({ + css: '#viewManager', + }); + + // extensions-item-list + const itemList = await viewManager.findElement({ + css: '#items-list', + }); + + // shadowRoot + const itemListShadowRoot = await driver.executeScript( + 'return arguments[0][0].shadowRoot', + itemList, + ); + + // extension-item + const extensionItem = await await itemListShadowRoot.findElement({ + css: `#${extensionId}`, + }); + + // shadowRoot + const extensionItemShadowRoot = await driver.executeScript( + 'return arguments[0][0].shadowRoot', + extensionItem, + ); + + // cr-icon-button + const devReloadButton = await extensionItemShadowRoot.findElement({ + css: '#dev-reload-button', + }); + + // shadowRoot + const devReloadButtonShadowRoot = await driver.executeScript( + 'return arguments[0][0].shadowRoot', + devReloadButton, + ); + + // cr-icon-button + const reloadBtn = await devReloadButtonShadowRoot.findElement({ + css: '#maskedImage', + }); + + await reloadBtn.click(); + } + }); +}); diff --git a/test/e2e/mv3/phishing-warning-sw-restart.spec.js b/test/e2e/mv3/phishing-warning-sw-restart.spec.js index de01139fe..2e10f35b2 100644 --- a/test/e2e/mv3/phishing-warning-sw-restart.spec.js +++ b/test/e2e/mv3/phishing-warning-sw-restart.spec.js @@ -5,9 +5,11 @@ const { openDapp, defaultGanacheOptions, assertAccountBalanceForDOM, - restartServiceWorker, SERVICE_WORKER_URL, regularDelayMs, + WALLET_PASSWORD, + unlockWallet, + terminateServiceWorker, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -28,12 +30,12 @@ describe('Phishing warning page', function () { }, async ({ driver, ganacheServer }) => { await driver.navigate(); - // log in wallet - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); + + await unlockWallet(driver, WALLET_PASSWORD); // DAPP is detected as phishing page await openDapp(driver); + const phishingPageHeader = await driver.findElements({ text: 'Deceptive site ahead', tag: 'h1', @@ -42,7 +44,7 @@ describe('Phishing warning page', function () { // Restart service worker await driver.openNewPage(SERVICE_WORKER_URL); - await restartServiceWorker(driver); + await terminateServiceWorker(driver); await driver.delay(regularDelayMs); // wait until extension is reloaded @@ -55,6 +57,7 @@ describe('Phishing warning page', function () { await openDapp(driver); // - extension, dapp, service worker and new dapp await driver.waitUntilXWindowHandles(4); + const newPhishingPageHeader = await driver.findElements({ text: 'Deceptive site ahead', tag: 'h1', diff --git a/test/e2e/tests/eth-sign.spec.js b/test/e2e/tests/eth-sign.spec.js index 4cde44f32..ce8da87bb 100644 --- a/test/e2e/tests/eth-sign.spec.js +++ b/test/e2e/tests/eth-sign.spec.js @@ -3,8 +3,8 @@ const { withFixtures, openDapp, DAPP_URL, - login, defaultGanacheOptions, + unlockWallet, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -21,7 +21,7 @@ describe('Eth sign', function () { }, async ({ driver }) => { await driver.navigate(); - await login(driver); + await unlockWallet(driver); await openDapp(driver); await driver.clickElement('#ethSign'); @@ -56,7 +56,7 @@ describe('Eth sign', function () { }, async ({ driver }) => { await driver.navigate(); - await login(driver); + await unlockWallet(driver); await openDapp(driver); await driver.clickElement('#ethSign'); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index cebeae935..243337c59 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -407,6 +407,11 @@ class Driver { await this.driver.close(); } + async closeWindowHandle(windowHandle) { + await this.driver.switchTo().window(windowHandle); + await this.driver.close(); + } + // Close Alert Popup async closeAlertPopup() { return await this.driver.switchTo().alert().accept(); diff --git a/yarn.lock b/yarn.lock index 56345e62f..04b8eadba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10170,13 +10170,13 @@ __metadata: linkType: hard "axios@npm:^1.2.1": - version: 1.2.2 - resolution: "axios@npm:1.2.2" + version: 1.4.0 + resolution: "axios@npm:1.4.0" dependencies: follow-redirects: ^1.15.0 form-data: ^4.0.0 proxy-from-env: ^1.1.0 - checksum: 6e357491b38426c5720f7328ecbafca3c643b03952c052d787570672ce7a9365717c2d64db4ce97cfbee3f830fa405101e360e14d0857ef7f96a9f4d814c4e03 + checksum: 7fb6a4313bae7f45e89d62c70a800913c303df653f19eafec88e56cea2e3821066b8409bc68be1930ecca80e861c52aa787659df0ffec6ad4d451c7816b9386b languageName: node linkType: hard @@ -12570,9 +12570,9 @@ __metadata: linkType: hard "compare-versions@npm:^5.0.1": - version: 5.0.1 - resolution: "compare-versions@npm:5.0.1" - checksum: 302a4e46224b47b9280cf894c6c87d8df912671fa391dcdbf0e63438d9b0a69fe20dd747fb439e8d54c43af016ff4eaaf0a4c9d8e7ca358bcd12dadf4ad2935e + version: 5.0.3 + resolution: "compare-versions@npm:5.0.3" + checksum: f66a4bb6ef8ff32031cc92c04dea4bbead039e72a7f6c7df7ef05f5a42ddca9202f8875b7449add54181e73b89f039662a8760c8db0ab036c4e8f653a7cd29c1 languageName: node linkType: hard