diff --git a/commands/keplr.js b/commands/keplr.js new file mode 100644 index 000000000..1f7dca9a8 --- /dev/null +++ b/commands/keplr.js @@ -0,0 +1,146 @@ +const log = require('debug')('synpress:keplr'); +const playwright = require('./playwright-keplr'); +const { onboardingElements } = require('../pages/keplr/first-time-flow-page'); +const { + notificationPageElements, +} = require('../pages/keplr/notification-page'); + +let extensionId; +let extensionVersion; + +const keplr = { + async resetState() { + log('Resetting state of keplr'); + extensionId = undefined; + extensionVersion = undefined; + }, + extensionId: () => { + return extensionId; + }, + extensionUrls: () => { + return { + extensionImportAccountUrl, + }; + }, + + async getExtensionDetails() { + const keplrExtensionData = (await playwright.getExtensionsData()).keplr; + + extensionId = keplrExtensionData.id; + extensionVersion = keplrExtensionData.version; + + return { + extensionId, + extensionVersion, + }; + }, + + async importWallet(secretWords, password) { + await playwright.waitAndClickByText( + onboardingElements.createWalletButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.importRecoveryPhraseButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.useRecoveryPhraseButton, + await playwright.keplrWindow(), + ); + await playwright.waitAndClickByText( + onboardingElements.phraseCount24, + await playwright.keplrWindow(), + ); + + for (const [index, word] of secretWords.split(' ').entries()) { + await playwright.waitAndTypeByLocator( + onboardingElements.textAreaSelector, + word, + index, + ); + } + + await playwright.waitAndClick( + onboardingElements.submitPhraseButton, + await playwright.keplrWindow(), + ); + + await playwright.waitAndType( + onboardingElements.walletInput, + onboardingElements.walletName, + ); + await playwright.waitAndType(onboardingElements.passwordInput, password); + await playwright.waitAndType( + onboardingElements.confirmPasswordInput, + password, + ); + + await playwright.waitAndClick( + onboardingElements.submitWalletDataButton, + await playwright.keplrWindow(), + { number: 1 }, + ); + + await playwright.waitForByText( + onboardingElements.phraseSelectChain, + await playwright.keplrWindow(), + ); + + await playwright.waitAndClick( + onboardingElements.submitChainButton, + await playwright.keplrWindow(), + ); + + await playwright.waitForByText( + onboardingElements.phraseAccountCreated, + await playwright.keplrWindow(), + ); + + await playwright.waitAndClick( + onboardingElements.finishButton, + await playwright.keplrWindow(), + { dontWait: true }, + ); + + return true; + }, + + async acceptAccess() { + const notificationPage = await playwright.switchToKeplrNotification(); + await playwright.waitAndClick( + notificationPageElements.approveButton, + notificationPage, + { waitForEvent: 'close' }, + ); + return true; + }, + + async confirmTransaction() { + const notificationPage = await playwright.switchToKeplrNotification(); + await playwright.waitAndClick( + notificationPageElements.approveButton, + notificationPage, + { waitForEvent: 'close' }, + ); + return true; + }, + + async initialSetup( + playwrightInstance, + { secretWordsOrPrivateKey, password }, + ) { + if (playwrightInstance) { + await playwright.init(playwrightInstance); + } else { + await playwright.init(); + } + + await playwright.assignWindows(); + await playwright.assignActiveTabName('keplr'); + await module.exports.getExtensionDetails(); + await module.exports.importWallet(secretWordsOrPrivateKey, password); + }, +}; + +module.exports = keplr; \ No newline at end of file diff --git a/commands/metamask.js b/commands/metamask.js index 6f24d067f..c7b34357a 100644 --- a/commands/metamask.js +++ b/commands/metamask.js @@ -1,5 +1,5 @@ const log = require('debug')('synpress:metamask'); -const playwright = require('./playwright'); +const playwright = require('./playwright-metamask'); const sleep = require('util').promisify(setTimeout); const { diff --git a/commands/playwright-keplr.js b/commands/playwright-keplr.js new file mode 100644 index 000000000..c7fc975c6 --- /dev/null +++ b/commands/playwright-keplr.js @@ -0,0 +1,378 @@ +const log = require('debug')('synpress:playwright'); +const fetch = require('node-fetch'); +const _ = require('underscore'); +const sleep = require('util').promisify(setTimeout); + +let browser; +let mainWindow; +let keplrWindow; +let keplrNotificationWindow; +let activeTabName; +let extensionsData = {}; +let retries = 0; + +module.exports = { + async resetState() { + log('Resetting state of playwright'); + browser = undefined; + mainWindow = undefined; + keplrWindow = undefined; + activeTabName = undefined; + keplrNotificationWindow = undefined; + retries = 0; + extensionsData = {}; + }, + + async init(playwrightInstance) { + const chromium = playwrightInstance + ? playwrightInstance + : require('@playwright/test').chromium; + const debuggerDetails = await fetch('http://127.0.0.1:9222/json/version'); //DevSkim: ignore DS137138 + const debuggerDetailsConfig = await debuggerDetails.json(); + const webSocketDebuggerUrl = debuggerDetailsConfig.webSocketDebuggerUrl; + if (process.env.SLOW_MODE) { + if (!isNaN(process.env.SLOW_MODE)) { + browser = await chromium.connectOverCDP(webSocketDebuggerUrl, { + slowMo: Number(process.env.SLOW_MODE), + }); + } else { + browser = await chromium.connectOverCDP(webSocketDebuggerUrl, { + slowMo: 50, + }); + } + } else { + browser = await chromium.connectOverCDP(webSocketDebuggerUrl); + } + return browser.isConnected(); + }, + + async assignWindows() { + const keplrExtensionData = (await module.exports.getExtensionsData()).keplr; + + let pages = await browser.contexts()[0].pages(); + + for (const page of pages) { + if (page.url().includes('specs/runner')) { + mainWindow = page; + } else if ( + page + .url() + .includes(`chrome-extension://${keplrExtensionData.id}/register.html`) + ) { + keplrWindow = page; + } + } + return true; + }, + async assignActiveTabName(tabName) { + activeTabName = tabName; + return true; + }, + + async isKeplrWindowActive() { + return activeTabName === 'keplr'; + }, + + keplrNotificationWindow() { + return keplrNotificationWindow; + }, + + async waitAndClickByText(text, page = keplrWindow) { + await module.exports.waitForByText(text, page); + const element = `:is(:text-is("${text}"), :text("${text}"))`; + await page.click(element); + await module.exports.waitUntilStable(); + }, + + async waitAndSetValue(text, selector, page = keplrWindow) { + const element = await module.exports.waitFor(selector, page); + await element.fill(''); + await module.exports.waitUntilStable(page); + await element.fill(text); + await module.exports.waitUntilStable(page); + }, + + async waitAndGetValue(selector, page = keplrWindow) { + const expect = expectInstance + ? expectInstance + : require('@playwright/test').expect; + const element = await module.exports.waitFor(selector, page); + await expect(element).toHaveText(/[a-zA-Z0-9]/, { + ignoreCase: true, + useInnerText: true, + }); + const value = await element.innerText(); + return value; + }, + + async waitAndClick(selector, page = keplrWindow, args = {}) { + const element = await module.exports.waitFor( + selector, + page, + args.number || 0, + ); + if (args.numberOfClicks && !args.waitForEvent) { + await element.click({ + clickCount: args.numberOfClicks, + force: args.force, + }); + } else if (args.numberOfClicks && args.waitForEvent) { + await Promise.all([ + page.waitForEvent(args.waitForEvent), + element.click({ clickCount: args.numberOfClicks, force: args.force }), + ]); + } else if (args.waitForEvent) { + if (args.waitForEvent.includes('navi')) { + await Promise.all([ + page.waitForNavigation(), + element.click({ force: args.force }), + ]); + } else { + await Promise.all([ + page.waitForEvent(args.waitForEvent), + element.click({ force: args.force }), + ]); + } + } else { + await element.click({ force: args.force }); + } + await module.exports.waitUntilStable(); + return element; + }, + + async switchToCypressWindow() { + if (mainWindow) { + await mainWindow.bringToFront(); + await module.exports.assignActiveTabName('cypress'); + } + return true; + }, + + async clear() { + browser = null; + return true; + }, + + async clearWindows() { + mainWindow = null; + keplrWindow = null; + return true; + }, + + async isCypressWindowActive() { + return activeTabName === 'cypress'; + }, + + async switchToKeplrWindow() { + await keplrWindow.bringToFront(); + await module.exports.assignActiveTabName('keplr'); + return true; + }, + + async waitUntilStable(page) { + const keplrExtensionData = (await module.exports.getExtensionsData()) + .keplr; + + if ( + page && + page + .url() + .includes( + `chrome-extension://${keplrExtensionData.id}/register.html`, + ) + ) { + await page.waitForLoadState('load'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + } + await keplrWindow.waitForLoadState('load'); + await keplrWindow.waitForLoadState('domcontentloaded'); + await keplrWindow.waitForLoadState('networkidle'); + + if (mainWindow) { + await mainWindow.waitForLoadState('load'); + await mainWindow.waitForLoadState('domcontentloaded'); + // todo: this may slow down tests and not be necessary but could improve stability + // await mainWindow.waitForLoadState('networkidle'); + } + }, + + keplrWindow() { + return keplrWindow; + }, + + async waitFor(selector, page = keplrWindow, number = 0) { + await module.exports.waitUntilStable(page); + await page.waitForSelector(selector, { strict: false }); + const element = page.locator(selector).nth(number); + await element.waitFor(); + await element.focus(); + if (process.env.STABLE_MODE) { + if (!isNaN(process.env.STABLE_MODE)) { + await page.waitForTimeout(Number(process.env.STABLE_MODE)); + } else { + await page.waitForTimeout(300); + } + } + return element; + }, + async waitForByText(text, page = keplrWindow) { + await module.exports.waitUntilStable(page); + // await page.waitForSelector(selector, { strict: false }); + const element = page.getByText(text).first(); + await element.waitFor(); + await element.focus(); + if (process.env.STABLE_MODE) { + if (!isNaN(process.env.STABLE_MODE)) { + await page.waitForTimeout(Number(process.env.STABLE_MODE)); + } else { + await page.waitForTimeout(300); + } + } + return element; + }, + + async waitAndClick(selector, page = keplrWindow, args = {}) { + const element = await module.exports.waitFor( + selector, + page, + args.number || 0, + ); + if (args.numberOfClicks && !args.waitForEvent) { + await element.click({ + clickCount: args.numberOfClicks, + force: args.force, + }); + } else if (args.numberOfClicks && args.waitForEvent) { + await Promise.all([ + page.waitForEvent(args.waitForEvent), + element.click({ clickCount: args.numberOfClicks, force: args.force }), + ]); + } else if (args.waitForEvent) { + if (args.waitForEvent.includes('navi')) { + await Promise.all([ + page.waitForNavigation(), + element.click({ force: args.force }), + ]); + } else { + await Promise.all([ + page.waitForEvent(args.waitForEvent), + element.click({ force: args.force }), + ]); + } + } else { + await element.click({ force: args.force }); + } + await module.exports.waitUntilStable(); + return element; + }, + + async waitForByRole(role, number = 0, page = keplrWindow) { + await module.exports.waitUntilStable(page); + const element = page.getByRole(role).nth(number); + await element.waitFor(); + await element.focus(); + if (process.env.STABLE_MODE) { + if (!isNaN(process.env.STABLE_MODE)) { + await page.waitForTimeout(Number(process.env.STABLE_MODE)); + } else { + await page.waitForTimeout(300); + } + } + return element; + }, + + async waitAndType(selector, value, page = keplrWindow) { + if (typeof value === 'number') { + value = value.toString(); + } + const element = await module.exports.waitFor(selector, page); + await element.type(value); + await module.exports.waitUntilStable(page); + }, + + async waitAndTypeByLocator(selector, value, number = 0, page = keplrWindow) { + if (typeof value === 'number') { + value = value.toString(); + } + const element = await module.exports.waitForByRole(selector, number, page); + await element.type(value); + await module.exports.waitUntilStable(page); + }, + + async getExtensionsData() { + if (!_.isEmpty(extensionsData)) { + return extensionsData; + } + + const context = await browser.contexts()[0]; + const page = await context.newPage(); + + await page.goto('chrome://extensions'); + await page.waitForLoadState('load'); + await page.waitForLoadState('domcontentloaded'); + + const devModeButton = page.locator('#devMode'); + await devModeButton.waitFor(); + await devModeButton.focus(); + await devModeButton.click(); + + const extensionDataItems = await page.locator('extensions-item').all(); + for (const extensionData of extensionDataItems) { + const extensionName = ( + await extensionData + .locator('#name-and-version') + .locator('#name') + .textContent() + ).toLowerCase(); + + const extensionVersion = ( + await extensionData + .locator('#name-and-version') + .locator('#version') + .textContent() + ).replace(/(\n| )/g, ''); + + const extensionId = ( + await extensionData.locator('#extension-id').textContent() + ).replace('ID: ', ''); + + extensionsData[extensionName] = { + version: extensionVersion, + id: extensionId, + }; + } + await page.close(); + + return extensionsData; + }, + + async switchToKeplrNotification() { + const keplrExtensionData = (await module.exports.getExtensionsData()).keplr; + + let pages = await browser.contexts()[0].pages(); + for (const page of pages) { + if ( + page + .url() + .includes(`chrome-extension://${keplrExtensionData.id}/popup.html`) + ) { + keplrNotificationWindow = page; + retries = 0; + await page.bringToFront(); + await module.exports.waitUntilStable(page); + return page; + } + } + await sleep(200); + if (retries < 50) { + retries++; + return await module.exports.switchToKeplrNotification(); + } else if (retries >= 50) { + retries = 0; + throw new Error( + '[switchToKeplrNotification] Max amount of retries to switch to keplr notification window has been reached. It was never found.', + ); + } + }, +}; \ No newline at end of file diff --git a/commands/playwright.js b/commands/playwright-metamask.js similarity index 100% rename from commands/playwright.js rename to commands/playwright-metamask.js diff --git a/helpers.js b/helpers.js index 80a7d96c8..0205a6aa9 100644 --- a/helpers.js +++ b/helpers.js @@ -193,6 +193,44 @@ module.exports = { ); } }, + async getKeplrReleases(version) { + log(`Trying to find keplr version ${version} in GitHub releases..`); + let filename; + let downloadUrl; + let tagName; + let response; + + try { + if (version === 'latest' || !version) { + response = await axios.get( + 'https://api.github.com/repos/chainapsis/keplr-wallet/releases', + ); + + filename = response.data[0].assets[0].name; + downloadUrl = response.data[0].assets[0].browser_download_url; + tagName = response.data[0].tag_name; + log( + `Keplr version found! Filename: ${filename}; Download url: ${downloadUrl}; Tag name: ${tagName}`, + ); + } else if (version) { + filename = `keplr-extension-manifest-v2-v${version}.zip`; + downloadUrl = `https://github.com/chainapsis/keplr-wallet/releases/download/v${version}/keplr-extension-manifest-v2-v${version}.zip`; + tagName = `keplr-extension-manifest-${version}`; + log( + `Keplr version found! Filename: ${filename}; Download url: ${downloadUrl}; Tag name: ${tagName}`, + ); + } + return { + filename, + downloadUrl, + tagName, + }; + } catch (e) { + throw new Error( + `[getKeplrReleases] Unable to fetch metamask releases from GitHub with following error:\n${e}`, + ); + } + }, async download(url, destination) { try { log( @@ -210,12 +248,19 @@ module.exports = { } } catch (e) { throw new Error( - `[download] Unable to download metamask release from: ${url} to: ${destination} with following error:\n${e}`, + `[download] Unable to download release from: ${url} to: ${destination} with following error:\n${e}`, ); } }, - async prepareMetamask(version) { - const release = await module.exports.getMetamaskReleases(version); + async prepareExtension(version, extension) { + let release; + if (extension === 'metamask') { + release = await module.exports.getMetamaskReleases(version); + } else if (extension === 'keplr') { + release = await module.exports.getKeplrReleases(version); + } else { + throw new Error('Please provide a valid extension name'); + } let downloadsDirectory; if (os.platform() === 'win32') { @@ -225,22 +270,22 @@ module.exports = { } await module.exports.createDirIfNotExist(downloadsDirectory); - const metamaskDirectory = path.join(downloadsDirectory, release.tagName); - const metamaskDirectoryExists = - await module.exports.checkDirOrFileExist(metamaskDirectory); - const metamaskManifestFilePath = path.join( + const extensionDirectory = path.join(downloadsDirectory, release.tagName); + const extensionDirectoryExists = + await module.exports.checkDirOrFileExist(extensionDirectory); + const extensionManifestFilePath = path.join( downloadsDirectory, release.tagName, 'manifest.json', ); - const metamaskManifestFileExists = await module.exports.checkDirOrFileExist( - metamaskManifestFilePath, + const extensionManifestFileExists = await module.exports.checkDirOrFileExist( + extensionManifestFilePath, ); - if (!metamaskDirectoryExists && !metamaskManifestFileExists) { - await module.exports.download(release.downloadUrl, metamaskDirectory); + if (!extensionDirectoryExists && !extensionManifestFileExists) { + await module.exports.download(release.downloadUrl, extensionDirectory); } else { - log('Metamask is already downloaded'); + log('Extension is already downloaded'); } - return metamaskDirectory; + return extensionDirectory; }, }; diff --git a/package.json b/package.json index 69ec585ac..8a1fcb023 100644 --- a/package.json +++ b/package.json @@ -41,14 +41,16 @@ "release:patch": "release-it patch --disable-metrics", "update:deps": "ncu -u -x 'node-fetch' && pnpm install", "start:server": "serve node_modules/@metamask/test-dapp/dist -p 3000", - "synpress:run": "SKIP_METAMASK_SETUP=true SYNPRESS_LOCAL_TEST=true node synpress.js run --configFile=synpress.config.js", - "test:e2e": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'pnpm synpress:run'", + "synpress:run:metamask": "EXTENSION=metamask SKIP_EXTENSION_SETUP=true SYNPRESS_LOCAL_TEST=true node synpress.js run --configFile=synpress.config.js", + "test:e2e:metamask": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'pnpm synpress:run:metamask'", "test:e2e:anvil": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'CYPRESS_USE_ANVIL=true pnpm synpress:run'", "test:e2e:headless": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'pnpm synpress:run --headless'", "test:e2e:headless:anvil": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'CYPRESS_USE_ANVIL=true pnpm synpress:run --headless'", "test:e2e:ci": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'pnpm synpress:run --record --group'", "test:e2e:ci:anvil": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'CYPRESS_USE_ANVIL=true pnpm synpress:run --record --group'", - "test:e2e:ci:cypress-action": "CYPRESS_USE_ANVIL=true pnpm synpress:run" + "test:e2e:ci:cypress-action": "CYPRESS_USE_ANVIL=true pnpm synpress:run", + "synpress:run:keplr": "EXTENSION=keplr SKIP_EXTENSION_SETUP=true SYNPRESS_LOCAL_TEST=true node synpress.js run --configFile=synpress.config.js", + "test:e2e:keplr": "start-server-and-test 'turbo start:server' http-get://localhost:3000 'pnpm synpress:run:keplr'" }, "dependencies": { "@cypress/code-coverage": "^3.11.0", diff --git a/pages/keplr/first-time-flow-page.js b/pages/keplr/first-time-flow-page.js new file mode 100644 index 000000000..d79d6e7f9 --- /dev/null +++ b/pages/keplr/first-time-flow-page.js @@ -0,0 +1,34 @@ +const createWalletButton = 'Create a new wallet'; +const importRecoveryPhraseButton = 'Import existing recovery phrase'; +const useRecoveryPhraseButton = 'Use recovery phrase or private key'; +const phraseCount24 = '24 words'; +const walletInput = 'input[name="name"]:focus'; +const passwordInput = 'input[name="password"]'; +const confirmPasswordInput = 'input[name="confirmPassword"]'; +const walletName = 'My wallet'; +const submitWalletDataButton = 'button[type="submit"]'; +const phraseSelectChain = 'Select Chains'; +const submitChainButton = 'button[type="button"]'; +const phraseAccountCreated = 'Account Created!'; +const finishButton = 'button[type="button"]'; +const textAreaSelector = 'textbox'; +const submitPhraseButton = 'button[type="submit"]'; + +module.exports.onboardingElements = { + createWalletButton, + importRecoveryPhraseButton, + useRecoveryPhraseButton, + phraseCount24, + walletInput, + walletName, + passwordInput, + confirmPasswordInput, + submitWalletDataButton, + phraseSelectChain, + submitChainButton, + phraseAccountCreated, + finishButton, + textAreaSelector, + submitChainButton, + submitPhraseButton, +}; \ No newline at end of file diff --git a/pages/keplr/notification-page.js b/pages/keplr/notification-page.js new file mode 100644 index 000000000..639ba0ae2 --- /dev/null +++ b/pages/keplr/notification-page.js @@ -0,0 +1,5 @@ +const approveButton = `button[type="button"]`; + +module.exports.notificationPageElements = { + approveButton, +}; \ No newline at end of file diff --git a/plugins/index.js b/plugins/index.js index c41090ed6..de16662fa 100644 --- a/plugins/index.js +++ b/plugins/index.js @@ -1,197 +1,15 @@ -const helpers = require('../helpers'); -const playwright = require('../commands/playwright'); -const metamask = require('../commands/metamask'); -const etherscan = require('../commands/etherscan'); - -/** - * @type {Cypress.PluginConfig} - */ module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - - on('before:browser:launch', async (browser = {}, arguments_) => { - if (browser.name === 'chrome') { - // metamask welcome screen blocks cypress from loading - arguments_.args.push( - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-renderer-backgrounding', - ); - if (process.env.CI) { - // Avoid: "dri3 extension not supported" error - arguments_.args.push('--disable-gpu'); - } - if (process.env.HEADLESS_MODE) { - arguments_.args.push('--headless=new'); - } - if (browser.isHeadless) { - arguments_.args.push('--window-size=1920,1080'); - } - } - - if (!process.env.SKIP_METAMASK_INSTALL) { - // NOTE: extensions cannot be loaded in headless Chrome - const metamaskPath = await helpers.prepareMetamask( - process.env.METAMASK_VERSION || '10.25.0', - ); - arguments_.extensions.push(metamaskPath); - } - - return arguments_; - }); - - on('task', { - error(message) { - console.error('\u001B[31m', 'ERROR:', message, '\u001B[0m'); - return true; - }, - warn(message) { - console.warn('\u001B[33m', 'WARNING:', message, '\u001B[0m'); - return true; - }, - // playwright commands - initPlaywright: playwright.init, - clearPlaywright: playwright.clear, - assignWindows: playwright.assignWindows, - clearWindows: playwright.clearWindows, - assignActiveTabName: playwright.assignActiveTabName, - isMetamaskWindowActive: playwright.isMetamaskWindowActive, - isCypressWindowActive: playwright.isCypressWindowActive, - switchToCypressWindow: playwright.switchToCypressWindow, - switchToMetamaskWindow: playwright.switchToMetamaskWindow, - switchToMetamaskNotification: playwright.switchToMetamaskNotification, - unlockMetamask: metamask.unlock, - importMetamaskAccount: metamask.importAccount, - createMetamaskAccount: metamask.createAccount, - renameMetamaskAccount: metamask.renameAccount, - switchMetamaskAccount: metamask.switchAccount, - addMetamaskNetwork: metamask.addNetwork, - changeMetamaskNetwork: async network => { - if (process.env.NETWORK_NAME && !network) { - network = process.env.NETWORK_NAME; - } else if (!network) { - network = 'goerli'; - } - return await metamask.changeNetwork(network); - }, - activateAdvancedGasControlInMetamask: metamask.activateAdvancedGasControl, - activateShowHexDataInMetamask: metamask.activateShowHexData, - activateTestnetConversionInMetamask: metamask.activateTestnetConversion, - activateShowTestnetNetworksInMetamask: metamask.activateShowTestnetNetworks, - activateCustomNonceInMetamask: metamask.activateCustomNonce, - activateDismissBackupReminderInMetamask: - metamask.activateDismissBackupReminder, - activateEthSignRequestsInMetamask: metamask.activateEthSignRequests, - activateImprovedTokenAllowanceInMetamask: - metamask.activateImprovedTokenAllowance, - resetMetamaskAccount: metamask.resetAccount, - disconnectMetamaskWalletFromDapp: metamask.disconnectWalletFromDapp, - disconnectMetamaskWalletFromAllDapps: metamask.disconnectWalletFromAllDapps, - confirmMetamaskSignatureRequest: metamask.confirmSignatureRequest, - confirmMetamaskDataSignatureRequest: metamask.confirmDataSignatureRequest, - rejectMetamaskSignatureRequest: metamask.rejectSignatureRequest, - rejectMetamaskDataSignatureRequest: metamask.rejectDataSignatureRequest, - confirmMetamaskEncryptionPublicKeyRequest: - metamask.confirmEncryptionPublicKeyRequest, - rejectMetamaskEncryptionPublicKeyRequest: - metamask.rejectEncryptionPublicKeyRequest, - confirmMetamaskDecryptionRequest: metamask.confirmDecryptionRequest, - rejectMetamaskDecryptionRequest: metamask.rejectDecryptionRequest, - importMetamaskToken: metamask.importToken, - confirmMetamaskAddToken: metamask.confirmAddToken, - rejectMetamaskAddToken: metamask.rejectAddToken, - confirmMetamaskPermissionToSpend: metamask.confirmPermissionToSpend, - rejectMetamaskPermissionToSpend: metamask.rejectPermissionToSpend, - confirmMetamaskPermissionToApproveAll: - metamask.confirmPermissionToApproveAll, - rejectMetamaskPermissionToApproveAll: metamask.rejectPermissionToApproveAll, - confirmMetamaskRevokePermissionToAll: metamask.confirmRevokePermissionToAll, - rejectMetamaskRevokePermissionToAll: metamask.rejectRevokePermissionToAll, - acceptMetamaskAccess: metamask.acceptAccess, - rejectMetamaskAccess: metamask.rejectAccess, - confirmMetamaskTransaction: metamask.confirmTransaction, - confirmMetamaskTransactionAndWaitForMining: - metamask.confirmTransactionAndWaitForMining, - rejectMetamaskTransaction: metamask.rejectTransaction, - openMetamaskTransactionDetails: metamask.openTransactionDetails, - closeMetamaskTransactionDetailsPopup: metamask.closeTransactionDetailsPopup, - allowMetamaskToAddNetwork: async ({ waitForEvent }) => - await metamask.allowToAddNetwork({ waitForEvent }), - rejectMetamaskToAddNetwork: metamask.rejectToAddNetwork, - allowMetamaskToSwitchNetwork: metamask.allowToSwitchNetwork, - rejectMetamaskToSwitchNetwork: metamask.rejectToSwitchNetwork, - allowMetamaskToAddAndSwitchNetwork: metamask.allowToAddAndSwitchNetwork, - getMetamaskWalletAddress: metamask.getWalletAddress, - fetchMetamaskWalletAddress: metamask.walletAddress, - setupMetamask: async ({ - secretWordsOrPrivateKey, - network, - password, - enableAdvancedSettings, - enableExperimentalSettings, - }) => { - if (process.env.NETWORK_NAME) { - network = process.env.NETWORK_NAME; - } - if ( - process.env.NETWORK_NAME && - process.env.RPC_URL && - process.env.CHAIN_ID && - process.env.SYMBOL - ) { - network = { - id: process.env.CHAIN_ID, - name: process.env.NETWORK_NAME, - nativeCurrency: { - symbol: process.env.SYMBOL, - }, - rpcUrls: { - public: { http: [process.env.RPC_URL] }, - default: { http: [process.env.RPC_URL] }, - }, - blockExplorers: { - etherscan: { url: process.env.BLOCK_EXPLORER }, - default: { url: process.env.BLOCK_EXPLORER }, - }, - testnet: process.env.IS_TESTNET, - }; - } - if (process.env.PRIVATE_KEY) { - secretWordsOrPrivateKey = process.env.PRIVATE_KEY; - } - if (process.env.SECRET_WORDS) { - secretWordsOrPrivateKey = process.env.SECRET_WORDS; - } - await metamask.initialSetup(null, { - secretWordsOrPrivateKey, - network, - password, - enableAdvancedSettings, - enableExperimentalSettings, - }); - return true; - }, - getCurrentNetwork: helpers.getCurrentNetwork, - etherscanGetTransactionStatus: async ({ txid }) => - await etherscan.getTransactionStatus(txid), - etherscanWaitForTxSuccess: async ({ txid }) => - await etherscan.waitForTxSuccess(txid), - }); - - if (process.env.BASE_URL) { - config.e2e.baseUrl = process.env.BASE_URL; - config.component.baseUrl = process.env.BASE_URL; - } - - if (process.env.CI) { - config.retries.runMode = 1; - config.retries.openMode = 1; - } - - if (process.env.SKIP_METAMASK_SETUP) { - config.env.SKIP_METAMASK_SETUP = true; + const extension = process.env.EXTENSION; + let selectedConfig; + if (extension === 'metamask') { + selectedConfig = require('./metamask-plugin'); + } else if (extension === 'keplr') { + selectedConfig = require('./keplr-plugin'); + } else { + throw new InvalidInputException( + `${extension} is not a valid extension name`, + ); } - return config; + return selectedConfig(on, config); }; diff --git a/plugins/keplr-plugin.js b/plugins/keplr-plugin.js new file mode 100644 index 000000000..99ef9be15 --- /dev/null +++ b/plugins/keplr-plugin.js @@ -0,0 +1,86 @@ +const helpers = require('../helpers'); +const playwright = require('../commands/playwright-keplr'); +const keplr = require('../commands/keplr'); + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + + on('before:browser:launch', async (browser = {}, arguments_) => { + if (browser.name === 'chrome') { + // metamask welcome screen blocks cypress from loading + arguments_.args.push( + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + ); + if (process.env.CI) { + // Avoid: "dri3 extension not supported" error + arguments_.args.push('--disable-gpu'); + } + if (process.env.HEADLESS_MODE) { + arguments_.args.push('--headless=new'); + } + if (browser.isHeadless) { + arguments_.args.push('--window-size=1920,1080'); + } + } + + if (!process.env.SKIP_KEPLR_INSTALL) { + // NOTE: extensions cannot be loaded in headless Chrome + const keplrPath = await helpers.prepareExtension( + process.env.KEPLR_VERSION || '0.12.68', process.env.EXTENSION + ); + arguments_.extensions.push(keplrPath); + } + + return arguments_; + }); + + on('task', { + error(message) { + console.error('\u001B[31m', 'ERROR:', message, '\u001B[0m'); + return true; + }, + warn(message) { + console.warn('\u001B[33m', 'WARNING:', message, '\u001B[0m'); + return true; + }, + // playwright commands for Keplr + initPlaywright: playwright.init, + assignWindows: playwright.assignWindows, + assignActiveTabName: playwright.assignActiveTabName, + isExtensionWindowActive: playwright.isKeplrWindowActive, + switchToCypressWindow: playwright.switchToCypressWindow, + clearPlaywright: playwright.clear, + clearWindows: playwright.clearWindows, + isCypressWindowActive: playwright.isCypressWindowActive, + switchToExtensionWindow: playwright.switchToKeplrWindow, + + // keplr commands + importWallet: keplr.importWallet, + acceptAccess: keplr.acceptAccess, + confirmTransaction: keplr.confirmTransaction, + setupWallet: async ({ + secretWordsOrPrivateKey, + network, + password, + enableAdvancedSettings, + enableExperimentalSettings, + }) => { + await keplr.initialSetup(null, { + secretWordsOrPrivateKey, + network, + password, + enableAdvancedSettings, + enableExperimentalSettings, + }); + return true; + }, + }); + + return config; +}; \ No newline at end of file diff --git a/plugins/metamask-plugin.js b/plugins/metamask-plugin.js new file mode 100644 index 000000000..490f058f7 --- /dev/null +++ b/plugins/metamask-plugin.js @@ -0,0 +1,197 @@ +const helpers = require('../helpers'); +const playwright = require('../commands/playwright-metamask'); +const metamask = require('../commands/metamask'); +const etherscan = require('../commands/etherscan'); + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + + on('before:browser:launch', async (browser = {}, arguments_) => { + if (browser.name === 'chrome') { + // metamask welcome screen blocks cypress from loading + arguments_.args.push( + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + ); + if (process.env.CI) { + // Avoid: "dri3 extension not supported" error + arguments_.args.push('--disable-gpu'); + } + if (process.env.HEADLESS_MODE) { + arguments_.args.push('--headless=new'); + } + if (browser.isHeadless) { + arguments_.args.push('--window-size=1920,1080'); + } + } + + if (!process.env.SKIP_METAMASK_INSTALL) { + // NOTE: extensions cannot be loaded in headless Chrome + const metamaskPath = await helpers.prepareExtension( + process.env.METAMASK_VERSION || '10.25.0', process.env.EXTENSION + ); + arguments_.extensions.push(metamaskPath); + } + + return arguments_; + }); + + on('task', { + error(message) { + console.error('\u001B[31m', 'ERROR:', message, '\u001B[0m'); + return true; + }, + warn(message) { + console.warn('\u001B[33m', 'WARNING:', message, '\u001B[0m'); + return true; + }, + // playwright commands + initPlaywright: playwright.init, + clearPlaywright: playwright.clear, + assignWindows: playwright.assignWindows, + clearWindows: playwright.clearWindows, + assignActiveTabName: playwright.assignActiveTabName, + isMetamaskWindowActive: playwright.isMetamaskWindowActive, + isCypressWindowActive: playwright.isCypressWindowActive, + switchToCypressWindow: playwright.switchToCypressWindow, + switchToMetamaskWindow: playwright.switchToMetamaskWindow, + switchToMetamaskNotification: playwright.switchToMetamaskNotification, + unlockMetamask: metamask.unlock, + importMetamaskAccount: metamask.importAccount, + createMetamaskAccount: metamask.createAccount, + renameMetamaskAccount: metamask.renameAccount, + switchMetamaskAccount: metamask.switchAccount, + addMetamaskNetwork: metamask.addNetwork, + changeMetamaskNetwork: async network => { + if (process.env.NETWORK_NAME && !network) { + network = process.env.NETWORK_NAME; + } else if (!network) { + network = 'goerli'; + } + return await metamask.changeNetwork(network); + }, + activateAdvancedGasControlInMetamask: metamask.activateAdvancedGasControl, + activateShowHexDataInMetamask: metamask.activateShowHexData, + activateTestnetConversionInMetamask: metamask.activateTestnetConversion, + activateShowTestnetNetworksInMetamask: metamask.activateShowTestnetNetworks, + activateCustomNonceInMetamask: metamask.activateCustomNonce, + activateDismissBackupReminderInMetamask: + metamask.activateDismissBackupReminder, + activateEthSignRequestsInMetamask: metamask.activateEthSignRequests, + activateImprovedTokenAllowanceInMetamask: + metamask.activateImprovedTokenAllowance, + resetMetamaskAccount: metamask.resetAccount, + disconnectMetamaskWalletFromDapp: metamask.disconnectWalletFromDapp, + disconnectMetamaskWalletFromAllDapps: metamask.disconnectWalletFromAllDapps, + confirmMetamaskSignatureRequest: metamask.confirmSignatureRequest, + confirmMetamaskDataSignatureRequest: metamask.confirmDataSignatureRequest, + rejectMetamaskSignatureRequest: metamask.rejectSignatureRequest, + rejectMetamaskDataSignatureRequest: metamask.rejectDataSignatureRequest, + confirmMetamaskEncryptionPublicKeyRequest: + metamask.confirmEncryptionPublicKeyRequest, + rejectMetamaskEncryptionPublicKeyRequest: + metamask.rejectEncryptionPublicKeyRequest, + confirmMetamaskDecryptionRequest: metamask.confirmDecryptionRequest, + rejectMetamaskDecryptionRequest: metamask.rejectDecryptionRequest, + importMetamaskToken: metamask.importToken, + confirmMetamaskAddToken: metamask.confirmAddToken, + rejectMetamaskAddToken: metamask.rejectAddToken, + confirmMetamaskPermissionToSpend: metamask.confirmPermissionToSpend, + rejectMetamaskPermissionToSpend: metamask.rejectPermissionToSpend, + confirmMetamaskPermissionToApproveAll: + metamask.confirmPermissionToApproveAll, + rejectMetamaskPermissionToApproveAll: metamask.rejectPermissionToApproveAll, + confirmMetamaskRevokePermissionToAll: metamask.confirmRevokePermissionToAll, + rejectMetamaskRevokePermissionToAll: metamask.rejectRevokePermissionToAll, + acceptMetamaskAccess: metamask.acceptAccess, + rejectMetamaskAccess: metamask.rejectAccess, + confirmMetamaskTransaction: metamask.confirmTransaction, + confirmMetamaskTransactionAndWaitForMining: + metamask.confirmTransactionAndWaitForMining, + rejectMetamaskTransaction: metamask.rejectTransaction, + openMetamaskTransactionDetails: metamask.openTransactionDetails, + closeMetamaskTransactionDetailsPopup: metamask.closeTransactionDetailsPopup, + allowMetamaskToAddNetwork: async ({ waitForEvent }) => + await metamask.allowToAddNetwork({ waitForEvent }), + rejectMetamaskToAddNetwork: metamask.rejectToAddNetwork, + allowMetamaskToSwitchNetwork: metamask.allowToSwitchNetwork, + rejectMetamaskToSwitchNetwork: metamask.rejectToSwitchNetwork, + allowMetamaskToAddAndSwitchNetwork: metamask.allowToAddAndSwitchNetwork, + getMetamaskWalletAddress: metamask.getWalletAddress, + fetchMetamaskWalletAddress: metamask.walletAddress, + setupMetamask: async ({ + secretWordsOrPrivateKey, + network, + password, + enableAdvancedSettings, + enableExperimentalSettings, + }) => { + if (process.env.NETWORK_NAME) { + network = process.env.NETWORK_NAME; + } + if ( + process.env.NETWORK_NAME && + process.env.RPC_URL && + process.env.CHAIN_ID && + process.env.SYMBOL + ) { + network = { + id: process.env.CHAIN_ID, + name: process.env.NETWORK_NAME, + nativeCurrency: { + symbol: process.env.SYMBOL, + }, + rpcUrls: { + public: { http: [process.env.RPC_URL] }, + default: { http: [process.env.RPC_URL] }, + }, + blockExplorers: { + etherscan: { url: process.env.BLOCK_EXPLORER }, + default: { url: process.env.BLOCK_EXPLORER }, + }, + testnet: process.env.IS_TESTNET, + }; + } + if (process.env.PRIVATE_KEY) { + secretWordsOrPrivateKey = process.env.PRIVATE_KEY; + } + if (process.env.SECRET_WORDS) { + secretWordsOrPrivateKey = process.env.SECRET_WORDS; + } + await metamask.initialSetup(null, { + secretWordsOrPrivateKey, + network, + password, + enableAdvancedSettings, + enableExperimentalSettings, + }); + return true; + }, + getCurrentNetwork: helpers.getCurrentNetwork, + etherscanGetTransactionStatus: async ({ txid }) => + await etherscan.getTransactionStatus(txid), + etherscanWaitForTxSuccess: async ({ txid }) => + await etherscan.waitForTxSuccess(txid), + }); + + if (process.env.BASE_URL) { + config.e2e.baseUrl = process.env.BASE_URL; + config.component.baseUrl = process.env.BASE_URL; + } + + if (process.env.CI) { + config.retries.runMode = 1; + config.retries.openMode = 1; + } + + if (process.env.SKIP_EXTENSION_SETUP) { + config.env.SKIP_EXTENSION_SETUP = true; + } + + return config; +}; diff --git a/support/commands.js b/support/commands.js index 664571457..1c5363a62 100644 --- a/support/commands.js +++ b/support/commands.js @@ -408,3 +408,33 @@ Cypress.Commands.add( return subject; }, ); + +// Keplr Commands +Cypress.Commands.add( + 'setupWallet', + ( + secretWordsOrPrivateKey = 'orbit bench unit task food shock brand bracket domain regular warfare company announce wheel grape trust sphere boy doctor half guard ritual three ecology', + password = 'Test1234', + ) => { + return cy.task('setupWallet', { + secretWordsOrPrivateKey, + password + }); + }, +); + +Cypress.Commands.add('acceptAccess', () => { + return cy.task('acceptAccess'); +}); + +Cypress.Commands.add('confirmTransaction', () => { + return cy.task('confirmTransaction'); +}); + +Cypress.Commands.add('isExtensionWindowActive', () => { + return cy.task('isExtensionWindowActive'); +}); + +Cypress.Commands.add('switchToExtensionWindow', () => { + return cy.task('switchToExtensionWindow'); +}); \ No newline at end of file diff --git a/support/index.js b/support/index.js index 41b6fae42..8bb05ed2c 100644 --- a/support/index.js +++ b/support/index.js @@ -25,7 +25,7 @@ Cypress.on('window:before:load', win => { }); before(() => { - if (!Cypress.env('SKIP_METAMASK_SETUP')) { + if (Cypress.env('EXTENSION') === 'metamask' && !Cypress.env('SKIP_EXTENSION_SETUP')) { cy.setupMetamask(); } }); diff --git a/synpress.config.js b/synpress.config.js index 3ef0efbc3..a28755ea3 100644 --- a/synpress.config.js +++ b/synpress.config.js @@ -11,6 +11,12 @@ const fixturesFolder = `${synpressPath}/fixtures`; log(`Detected synpress fixtures path is: ${fixturesFolder}`); const supportFile = 'tests/e2e/support.js'; + +const specPattern = { + metamask: 'tests/e2e/specs/metamask/**/*.{js,jsx,ts,tsx}', + keplr : 'tests/e2e/specs/keplr/**/*.{js,jsx,ts,tsx}' +} + module.exports = defineConfig({ userAgent: 'synpress', retries: { @@ -33,7 +39,7 @@ module.exports = defineConfig({ testIsolation: false, setupNodeEvents, baseUrl: 'http://localhost:3000', - specPattern: 'tests/e2e/specs/**/*.{js,jsx,ts,tsx}', + specPattern: specPattern[process.env.EXTENSION], supportFile, }, component: { diff --git a/synpress.js b/synpress.js index d7daab5bf..4dac7d251 100644 --- a/synpress.js +++ b/synpress.js @@ -3,6 +3,7 @@ const log = require('debug')('synpress:cli'); const program = require('commander'); const { run, open } = require('./launcher'); const { version } = require('./package.json'); +const SUPPORTED_EXTENSIONS = ['metamask', 'keplr'] if (process.env.DEBUG && process.env.DEBUG.includes('synpress')) { log('DEBUG mode is enabled'); @@ -13,6 +14,15 @@ if (process.env.DEBUG && process.env.DEBUG.includes('synpress')) { } } +if (!process.env.EXTENSION) { + throw new Error('Please provide EXTENSION environment variable'); +} +if (!SUPPORTED_EXTENSIONS.includes(process.env.EXTENSION)) { + throw new Error( + `Invalid EXTENSION value. EXTENSION can have the following values: ${SUPPORTED_EXTENSIONS.toString()}`, + ); +} + if (process.env.SYNPRESS_LOCAL_TEST) { log('Loading .env config file from root folder'); require('dotenv').config(); @@ -27,7 +37,7 @@ if (process.env.SYNPRESS_LOCAL_TEST) { } // if user skips metamask install or setup -if (!process.env.SKIP_METAMASK_INSTALL && !process.env.SKIP_METAMASK_SETUP) { +if (!process.env.SKIP_METAMASK_INSTALL && !process.env.SKIP_EXTENSION_SETUP) { // we don't want to check for presence of SECRET_WORDS or PRIVATE_KEY if (!process.env.SECRET_WORDS && !process.env.PRIVATE_KEY) { throw new Error( @@ -36,7 +46,7 @@ if (!process.env.SKIP_METAMASK_INSTALL && !process.env.SKIP_METAMASK_SETUP) { } } else { log( - 'Skipping check for SECRET_WORDS and PRIVATE_KEY as SKIP_METAMASK_INSTALL or SKIP_METAMASK_SETUP is set', + 'Skipping check for SECRET_WORDS and PRIVATE_KEY as SKIP_EXTENSION_SETUP or SKIP_METAMASK_SETUP is set', ); } diff --git a/tests/e2e/specs/keplr/keplr-spec.js b/tests/e2e/specs/keplr/keplr-spec.js new file mode 100644 index 000000000..28e5b3049 --- /dev/null +++ b/tests/e2e/specs/keplr/keplr-spec.js @@ -0,0 +1,30 @@ +/* eslint-disable ui-testing/no-disabled-tests */ +describe('Keplr', () => { + context('Test commands', () => { + it(`setupWallet should finish Keplr setup using secret words`, () => { + cy.setupWallet().then(setupFinished => { + expect(setupFinished).to.be.true; + }); + }); + + it(`acceptAccess should accept connection request to Keplr`, () => { + cy.visit('/'); + cy.contains('Connect Wallet').click(); + cy.acceptAccess().then(taskCompleted => { + expect(taskCompleted).to.be.true; + }); + cy.get('.card') + .contains('My Wallet') + .then(p => console.log(p)); + + cy.contains('agoric1p2aqakv3ulz4qfy2nut86j9gx0dx0yw09h96md'); + }); + + it(`confirmTransaction should confirm transaction for token creation (contract deployment) and check tx data`, () => { + cy.contains('Make an Offer').click(); + cy.confirmTransaction().then(taskCompleted => { + expect(taskCompleted).to.be.true; + }); + }); + }); + }); \ No newline at end of file diff --git a/tests/e2e/specs/keplr/playwright-spec.js b/tests/e2e/specs/keplr/playwright-spec.js new file mode 100644 index 000000000..46bad8fdf --- /dev/null +++ b/tests/e2e/specs/keplr/playwright-spec.js @@ -0,0 +1,32 @@ +describe('Playwright', () => { + context('Test commands', () => { + it(`initPlaywright should connect with cypress browser`, () => { + cy.initPlaywright().then(isConnected => { + expect(isConnected).to.be.true; + }); + }); + it(`assignActiveTabName should properly assign keplr tab as currently active and verify result using isKeplrWindowActive & isCypressWindowActive`, () => { + cy.assignActiveTabName('keplr'); + cy.isExtensionWindowActive().then(isActive => { + expect(isActive).to.be.true; + }); + cy.isCypressWindowActive().then(isActive => { + expect(isActive).to.be.false; + }); + }); + it(`assignWindows should properly assign cypress and keplr windows`, () => { + cy.assignWindows().then(assigned => { + expect(assigned).to.be.true; + }); + }); + it(`switchToCypressWindow should properly switch active tab to cypress window`, () => { + cy.switchToCypressWindow(); + cy.isCypressWindowActive().then(isActive => { + expect(isActive).to.be.true; + }); + cy.isExtensionWindowActive().then(isActive => { + expect(isActive).to.be.false; + }); + }); + }); + }); \ No newline at end of file diff --git a/tests/e2e/specs/metamask-spec.js b/tests/e2e/specs/metamask/metamask-spec.js similarity index 100% rename from tests/e2e/specs/metamask-spec.js rename to tests/e2e/specs/metamask/metamask-spec.js diff --git a/tests/e2e/specs/playwright-spec.js b/tests/e2e/specs/metamask/playwright-spec.js similarity index 100% rename from tests/e2e/specs/playwright-spec.js rename to tests/e2e/specs/metamask/playwright-spec.js