diff --git a/.circleci/config.yml b/.circleci/config.yml index ba9283f218ca..ae81189b5626 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -227,9 +227,6 @@ workflows: - test-e2e-mmi-playwright - OPTIONAL: requires: - prep-build-test-mmi-playwright - - test-e2e-swap-playwright - OPTIONAL: - requires: - - prep-build - test-e2e-chrome-rpc-mmi: requires: - prep-build-test-mmi diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 8b5680b17d3f..bca1bdeec143 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -86,7 +86,7 @@ async function storeGitDiffOutput() { // Store the output of git diff const outputPath = path.resolve(outputDir, 'changed-files.txt'); - fs.writeFileSync(outputPath, diffOutput); + fs.writeFileSync(outputPath, diffOutput.trim()); console.log(`Git diff results saved to ${outputPath}`); process.exit(0); diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index d518a4f76419..c79604574582 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Senden" }, - "sendAToken": { - "message": "Token senden" - }, "sendBugReport": { "message": "Übermitteln Sie uns einen Fehlerbericht." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index b9ee6aebef9f..0d19c084a788 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Αποστολή" }, - "sendAToken": { - "message": "Στείλτε ένα token" - }, "sendBugReport": { "message": "Στείλτε μας μια αναφορά σφάλματος." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7ba8016a3244..637329f50f04 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4669,9 +4669,6 @@ "send": { "message": "Send" }, - "sendAToken": { - "message": "Send a token" - }, "sendBugReport": { "message": "Send us a bug report." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index c98710b9cb20..e002ef51d1a5 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Enviar" }, - "sendAToken": { - "message": "Enviar un token" - }, "sendBugReport": { "message": "Envíenos un informe de error." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index db9a22822545..ede2e5dd0c26 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Envoyer" }, - "sendAToken": { - "message": "Envoyer un jeton" - }, "sendBugReport": { "message": "Envoyez-nous un rapport de bogue." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index b2f2f2cc2ff5..ebe4d332817a 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "भेजें" }, - "sendAToken": { - "message": "एक टोकन भेजें" - }, "sendBugReport": { "message": "हमें एक बग रिपोर्ट भेजें।" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 69373f6c1a29..45159fc588d4 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Kirim" }, - "sendAToken": { - "message": "Kirimkan token" - }, "sendBugReport": { "message": "Kirimi kami laporan bug." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 57fc7f0c84cf..c031577d7888 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "送金" }, - "sendAToken": { - "message": "トークンを送信" - }, "sendBugReport": { "message": "バグの報告をお送りください。" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 4c94e71f3758..7c6845b639d5 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "보내기" }, - "sendAToken": { - "message": "토큰 보내기" - }, "sendBugReport": { "message": "버그 리포트 전송" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index e2096428b1ff..246c8bf8f9e2 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Enviar" }, - "sendAToken": { - "message": "Enviar um token" - }, "sendBugReport": { "message": "Envie-nos um relatório de bugs." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index a2c78a570554..fcff79b945db 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Отправить" }, - "sendAToken": { - "message": "Отправить токен" - }, "sendBugReport": { "message": "Отправьте нам отчет об ошибке." }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 60ab484989c3..5cce80dd34e6 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Magpadala" }, - "sendAToken": { - "message": "Magpadala ng token" - }, "sendBugReport": { "message": "Padalhan kami ng ulat ng bug." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 57cc95ca98c5..5aa540dec442 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Gönder" }, - "sendAToken": { - "message": "Bir token gönder" - }, "sendBugReport": { "message": "Bize bir hata raporu gönder." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index dae1661327bc..a443e3f364a5 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "Gửi" }, - "sendAToken": { - "message": "Gửi token" - }, "sendBugReport": { "message": "Gửi báo cáo lỗi." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index d35e7ee4829a..77d3699edd68 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -4584,9 +4584,6 @@ "send": { "message": "发送" }, - "sendAToken": { - "message": "发送代币" - }, "sendBugReport": { "message": "向我们发送错误报告。" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index 5ed0f46fc71c..529714d9a4e7 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -41,6 +41,7 @@ import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.ut import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { maskObject } from '../../shared/modules/object.utils'; import { FIXTURE_STATE_METADATA_VERSION } from '../../test/e2e/default-fixture'; +import { getSocketBackgroundToMocha } from '../../test/e2e/background-socket/socket-background-to-mocha'; import { OffscreenCommunicationTarget, OffscreenCommunicationEvents, @@ -401,6 +402,14 @@ async function initialize() { let isFirstMetaMaskControllerSetup; + // We only want to start this if we are running a test build, not for the release build. + // `navigator.webdriver` is true if Selenium, Puppeteer, or Playwright are running. + // In MV3, the Service Worker sees `navigator.webdriver` as `undefined`, so this will trigger from + // an Offscreen Document message instead. Because it's a singleton class, it's safe to start multiple times. + if (process.env.IN_TEST && window.navigator?.webdriver) { + getSocketBackgroundToMocha(); + } + if (isManifestV3) { // Save the timestamp immediately and then every `SAVE_TIMESTAMP_INTERVAL` // miliseconds. This keeps the service worker alive. diff --git a/app/scripts/offscreen.js b/app/scripts/offscreen.js index cb18f393e11d..159a2c8a5773 100644 --- a/app/scripts/offscreen.js +++ b/app/scripts/offscreen.js @@ -1,5 +1,6 @@ import { captureException } from '@sentry/browser'; import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-communication'; +import { getSocketBackgroundToMocha } from '../../test/e2e/background-socket/socket-background-to-mocha'; /** * Creates an offscreen document that can be used to load additional scripts @@ -25,6 +26,12 @@ export async function createOffscreen() { offscreenDocumentLoadedListener, ); resolve(); + + // If the Offscreen Document sees `navigator.webdriver === true` and we are in a test environment, + // start the SocketBackgroundToMocha. + if (process.env.IN_TEST && msg.webdriverPresent) { + getSocketBackgroundToMocha(); + } } }; chrome.runtime.onMessage.addListener(offscreenDocumentLoadedListener); diff --git a/development/build/manifest.js b/development/build/manifest.js index d0042af75c67..5eff1920a28f 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -71,6 +71,7 @@ function createManifestTasks({ ...manifest.permissions, 'webRequestBlocking', 'http://localhost/*', + 'tabs', // test builds need tabs permission for switchToWindowWithTitle ]; }); @@ -80,6 +81,7 @@ function createManifestTasks({ ...manifest.permissions, 'webRequestBlocking', 'http://localhost/*', + 'tabs', // test builds need tabs permission for switchToWindowWithTitle ]; }); diff --git a/development/generate-rc-commits.js b/development/generate-rc-commits.js index a09103c35db9..8b91ec0ff4f1 100644 --- a/development/generate-rc-commits.js +++ b/development/generate-rc-commits.js @@ -10,7 +10,7 @@ const octokit = new Octokit({ /** * This script is used to filter and group commits by teams based on unique commit messages. * It takes two branches as input and generates a CSV file with the commit message, author,PR link, team,release tag and commit hash - * The teams and their members are defined in the 'authorTeams' object. + * The teams and their members are defined in the 'teams.json' file. * * Command to run the script: node development/generate-rc-commits.js origin/branchA origin/branchB * @@ -19,96 +19,24 @@ const octokit = new Octokit({ * Output: the generated commits will be in a file named 'commits.csv'. */ -// JSON mapping authors to teams -const authorTeams = { - Accounts: [ - 'Owen Craston', - 'Gustavo Antunes', - 'Monte Lai', - 'Daniel Rocha', - 'Howard Braham', - 'Kate Johnson', - 'Xiaoming Wang', - 'Charly Chevalier', - 'Mike B', - ], - 'Wallet UX': ['David Walsh', 'Nidhi Kumari', 'Jony Bursztyn'], - 'Extension Platform': [ - 'chloeYue', - 'Chloe Gao', - 'danjm', - 'Danica Shen', - 'Brad Decker', - 'hjetpoluru', - 'Harika Jetpoluru', - 'Marina Boboc', - 'Gauthier Petetin', - 'Dan Miller', - 'Dan J Miller', - 'David Murdoch', - 'Niranjana Binoy', - 'Victor Thomas', - 'vthomas13', - 'seaona', - 'Norbert Elter', - ], - 'Wallet API': ['tmashuang', 'jiexi', 'BelfordZ', 'Shane'], - Confirmations: [ - 'Pedro Figueiredo', - 'Sylva Elendu', - 'Olusegun Akintayo', - 'Jyoti Puri', - 'Ariella Vu', - 'OGPoyraz', - 'vinistevam', - 'Matthew Walsh', - 'cryptotavares', - 'Vinicius Stevam', - 'Derek Brans', - 'sleepytanya', - 'Priya', - ], - 'Design Systems': [ - 'georgewrmarshall', - 'Garrett Bear', - 'George Marshall', - 'Devin', - ], - Snaps: [ - 'David Drazic', - 'hmalik88', - 'Montoya', - 'Mrtenz', - 'Frederik Bolding', - 'Bowen Sanders', - 'Guillaume Roux', - 'Hassan Malik', - 'Maarten Zuidhoorn', - 'Jonathan Ferreira', - ], - Assets: ['salimtb', 'sahar-fehri', 'Brian Bergeron'], - Linea: ['VGau', 'Victorien Gauch'], - lavamoat: ['weizman', 'legobeat', 'kumavis', 'LeoTM'], - 'Wallet Framework': [ - 'Michele Esposito', - 'Elliot Winkler', - 'Gudahtt', - 'Jongsun Suh', - 'Mark Stacey', - ], - MMI: [ - 'António Regadas', - 'Albert Olivé', - 'Ramon AC', - 'Shane T', - 'Bernardo Garces Chapero', - ], - Swaps: ['Daniel', 'Davide Brocchetto', 'Nicolas Ferro', 'infiniteflower'], - Devex: ['Thomas Huang', 'Alex Donesky', 'jiexi', 'Zachary Belford'], - Notifications: ['Prithpal-Sooriya', 'Matteo Scurati', 'Prithpal Sooriya'], - Bridging: ['Bilal', 'micaelae', 'Ethan Wessel'], - Ramps: ['George Weiler'], -}; +// Function to fetch author teams mapping file from teams.json +async function fetchAuthorTeamsFile() { + try { + const { data } = await octokit.request( + 'GET /repos/{owner}/{repo}/contents/{path}', + { + owner: 'MetaMask', + repo: 'MetaMask-planning', + path: 'teams.json', + }, + ); + const content = Buffer.from(data.content, 'base64').toString('utf-8'); + return JSON.parse(content); // Assuming the file is in JSON format + } catch (error) { + console.error('Error fetching author teams mapping file:', error); + return {}; + } +} // Function to get PR labels async function getPRLabels(owner, repo, prNumber) { @@ -129,18 +57,26 @@ async function getPRLabels(owner, repo, prNumber) { } } -// Function to get the team for a given author -function getTeamForAuthor(authorName) { - for (const [team, authors] of Object.entries(authorTeams)) { - if (authors.includes(authorName)) { - return team; - } +// Function to get the GitHub username for a given commit hash +async function getGitHubUsername(commitHash) { + try { + const { data } = await octokit.request( + 'GET /repos/{owner}/{repo}/commits/{ref}', + { + owner: 'MetaMask', + repo: 'metamask-extension', + ref: commitHash, + }, + ); + return data.author ? data.author.login : null; + } catch (error) { + console.error('Error fetching GitHub username:', error); + return null; } - return 'Other/Unknown'; // Default team for unknown authors } // Function to filter commits based on unique commit messages and group by teams -async function filterCommitsByTeam(branchA, branchB) { +async function filterCommitsByTeam(branchA, branchB, authorTeams) { try { const git = simpleGit(); @@ -157,17 +93,27 @@ async function filterCommitsByTeam(branchA, branchB) { const log = await git.log(logOptions); const seenMessages = new Set(); const commitsByTeam = {}; + let processedCommits = 0; const MAX_COMMITS = 500; // Limit the number of commits to process + console.log('Generation of the CSV file "commits.csv" is in progress...'); for (const commit of log.all) { - const { author, message, hash } = commit; - if (commitsByTeam.length >= MAX_COMMITS) { + if (processedCommits >= MAX_COMMITS) { break; } - const team = getTeamForAuthor(author); + const { author, message, hash } = commit; + const githubUsername = await getGitHubUsername(hash); + let team = authorTeams[githubUsername] || 'Other/Unknown'; + + // Format the team label + team = team + .replace(/^team-/u, '') // Remove the "team-" prefix + .split('-') // Split the string into an array of words + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize the first letter of each word + .join(' '); // Join the words back into a string with spaces // Extract PR number from the commit message using regex const prMatch = message.match(/\(#(\d+)\)/u); @@ -206,9 +152,9 @@ async function filterCommitsByTeam(branchA, branchB) { releaseLabel, hash: hash.substring(0, 10), }); + processedCommits += 1; } } - return commitsByTeam; } catch (error) { console.error(error); @@ -233,7 +179,7 @@ function formatAsCSV(commitsByTeam) { }); } csvContent.unshift( - 'Commit Message,Author,PR Link,Team,Release Label, Commit Hash', + 'Commit Message,Author,PR Link,Team,Release Label,Commit Hash', ); return csvContent; @@ -250,7 +196,14 @@ async function main() { const branchA = args[0]; const branchB = args[1]; - const commitsByTeam = await filterCommitsByTeam(branchA, branchB); + // Fetch author teams mapping from the teams.json file + const authorTeams = await fetchAuthorTeamsFile(); + + const commitsByTeam = await filterCommitsByTeam( + branchA, + branchB, + authorTeams, + ); if (Object.keys(commitsByTeam).length === 0) { console.log('No unique commits found.'); diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 8130ebf79f2f..8abed58007f5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -803,7 +803,7 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@metamask-institutional/custody-keyring>@metamask-institutional/configuration-client": true, "@metamask-institutional/sdk": true, - "@metamask-institutional/sdk>@metamask-institutional/types": true, + "@metamask-institutional/types": true, "@metamask/obs-store": true, "browserify>crypto-browserify": true, "gulp-sass>lodash.clonedeep": true, @@ -823,7 +823,7 @@ "packages": { "@metamask-institutional/custody-controller": true, "@metamask-institutional/sdk": true, - "@metamask-institutional/sdk>@metamask-institutional/types": true, + "@metamask-institutional/types": true, "gulp-sass>lodash.clonedeep": true } }, diff --git a/offscreen/scripts/offscreen.ts b/offscreen/scripts/offscreen.ts index 09b6227a997c..9f611333b581 100644 --- a/offscreen/scripts/offscreen.ts +++ b/offscreen/scripts/offscreen.ts @@ -42,4 +42,8 @@ if (process.env.IN_TEST) { chrome.runtime.sendMessage({ target: OffscreenCommunicationTarget.extensionMain, isBooted: true, + + // This message is being sent from the Offscreen Document to the Service Worker. + // The Service Worker has no way to query `navigator.webdriver`, so we send it here. + webdriverPresent: navigator.webdriver === true, }); diff --git a/package.json b/package.json index ee9da3596c82..83dbfe1d2dd0 100644 --- a/package.json +++ b/package.json @@ -285,6 +285,7 @@ "@metamask-institutional/rpc-allowlist": "^1.0.3", "@metamask-institutional/sdk": "^0.1.30", "@metamask-institutional/transaction-update": "^0.2.5", + "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", "@metamask/accounts-controller": "^17.2.0", "@metamask/address-book-controller": "^4.0.1", @@ -292,7 +293,7 @@ "@metamask/approval-controller": "^7.0.0", "@metamask/assets-controllers": "^36.0.0", "@metamask/base-controller": "^5.0.1", - "@metamask/bitcoin-wallet-snap": "^0.2.4", + "@metamask/bitcoin-wallet-snap": "^0.2.5", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^10.0.0", @@ -491,6 +492,7 @@ "@tsconfig/node20": "^20.1.2", "@types/babelify": "^7.3.7", "@types/browserify": "^12.0.37", + "@types/chrome": "^0.0.268", "@types/currency-formatter": "^1.5.1", "@types/fs-extra": "^9.0.13", "@types/gulp": "^4.0.9", @@ -519,6 +521,7 @@ "@types/w3c-web-hid": "^1.0.3", "@types/watchify": "^3.11.1", "@types/webextension-polyfill": "^0.10.4", + "@types/ws": "^8.5.10", "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", @@ -637,6 +640,7 @@ "watchify": "^4.0.0", "webextension-polyfill": "^0.8.0", "webpack": "^5.91.0", + "ws": "^8.17.1", "yaml": "^2.4.1", "yargs": "^17.7.2" }, @@ -704,6 +708,8 @@ "@trezor/connect-web>@trezor/connect>@trezor/utxo-lib>tiny-secp256k1": false, "@storybook/test-runner>@swc/core": false, "@lavamoat/lavadome-react>@lavamoat/preinstall-always-fail": false, + "ws>bufferutil": false, + "ws>utf-8-validate": false, "tsx>esbuild": false, "@metamask/eth-trezor-keyring>@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": false, "firebase>@firebase/firestore>@grpc/proto-loader>protobufjs": false, diff --git a/test/data/mock-data.js b/test/data/mock-data.js index b39b2305cfdd..f42327497b67 100644 --- a/test/data/mock-data.js +++ b/test/data/mock-data.js @@ -429,6 +429,291 @@ const TRADES_API_MOCK_RESULT = [ }, ]; +const SWAP_TEST_ETH_USDC_TRADES_MOCK = [ + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001c616972737761704c696768743446656544796e616d696346697865640000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000190c020b8bd00000000000000000000000000000000000000000000000000000000669e592c00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000034ed3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000000001b7e0912a9d133a8b79f7213ecb9c3754f4e7b5ed6afc5d3897451660ecfc64dd8645690a525c41c903efa55e3ff3a35e94e3e9d214481c26072c7bf22a09e38ee000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000001fe", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3468593", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 300000, + averageGas: 150000, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 845, + aggregator: "airswapV4", + aggType: "RFQ", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 1.008542852896115, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.468593, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.000995894893460576, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.000995894893460576 + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f6e65496e6368563546656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034a5600000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c8e449022e0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034a55f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000002c000000000000000000000006ce6d6d40a4c4088309293b0582372a2e6bb632e8000000000000000000000008592064903ef23d34e4d5aaaed40abf6d96af1867dcbea7c000000000000000000000000000000000000000000000000014e", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3520620", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 584, + aggregator: "oneInchV5", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.06, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9936715373533649, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.52062, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001010832772774198, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001010832772774198 + } + }, + { + trade: null, + hasRoute: false, + maxGas: 2750000, + averageGas: 637198, + estimatedRefund: 0, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + sourceAmount: "1000000000000000", + destinationAmount: null, + error: "Request failed with status code 403", + approvalNeeded: null, + fetchTime: 121, + aggregator: "paraswap", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.05, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: null, + hasRoute: false, + maxGas: 405000, + averageGas: 209421, + estimatedRefund: 0, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + sourceAmount: "1000000000000000", + destinationAmount: null, + error: "Cannot read properties of undefined (reading '0xv4order')", + approvalNeeded: null, + fetchTime: 108, + aggregator: "pmm", + aggType: "RFQ", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000c307846656544796e616d6963000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000033b4150000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f191500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000128d9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000033b41500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48869584cd00000000000000000000000011ededebf63bef0ea2d2d071bdf88f71543ec6fb00000000000000000000000000000000000000004b3fbadd405c612525d4c81900000000000000000000000000000000000000000000000001f5", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3457589", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1650000, + averageGas: 411000, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 215, + aggregator: "zeroEx", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.2, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 1.0116882188836294, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.457589, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.000992735448865134, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.000992735448865134 + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f70656e4f6365616e46656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000003472230000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f19150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000088490411a32000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a00000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034722400000000000000000000000000000000000000000000000000000000003584240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ef53a4bd0e16ccc9116770a41c4bd3ad1147bd4f00000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004e451a74316000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064eb5625d9000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c452bbbe2900000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffff6f0ed6f346007563d3266de350d174a831bde0ca0001000000000000000005db0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000016400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000182", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3507236", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 1832, + aggregator: "openOcean", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9974971987834718, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.507236, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001006989987744627, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001006989987744627 + } + }, + { + trade: null, + hasRoute: false, + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + sourceToken: 0x0000000000000000000000000000000000000000, + destinationToken: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, + sourceAmount: 1000000000000000, + destinationAmount: null, + error: "Request failed with status code 403", + approvalNeeded: null, + fetchTime: 33, + aggregator: "hashFlow", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: + { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136b796265725377617046656544796e616d6963000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000003860210000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f191500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b44e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000008c00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000074de5d4fcbf63e00296fd95d33236b979401663100000000000000000000000000000000000000000000000000000000669e5d4100000000000000000000000000000000000000000000000000000000000005a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000000400e00deaa0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000002aa3041fe813cfe572969216c6843c33f14f9194000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000040ca6182da00000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000007a819fa46734a49d0112796f9377e024c350fb260000000000000000000000000000000000000000669e58be8ddca21b4e2a8e860000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000807cf9a772d5a3f9cefbc1192e939d62f0d9bd380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000316439e530f4a3c0000000000000000000000000000000000000000000000000000000000000149500000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000001495000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000000000000000000000000000000000000000000041f8b567a2d7c3e67633821bc6d207b655cb8c65bb94a0ceda4e3cca76c040ae9015be7ee2c63c80506271283fa5be190f00ba64c10aa2abefc9b4fd012ad4654c1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400e00deaa000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000ec577a919fca1b682f584a50b1048331ef0f30dd0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000316439e530f4a3c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000003000000000000000000000000003986aa000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000074de5d4fcbf63e00296fd95d33236b979401663100000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000386021000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002267b22536f75726365223a226d6574616d61736b222c22416d6f756e74496e555344223a22332e34363835383836343432363131323336222c22416d6f756e744f7574555344223a22332e37353438323837323633373238333935222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2233373730303236222c2254696d657374616d70223a313732313635333339332c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224779574558562f6454475354315036694a7347522f366d502f755953554d6e5669664e536e52657a505a546471734976366c5939345a564c45587952393442534359427376452f396a4f4c427a474d77564d32473248766f4e707a39644c2f2f646a534b576f4d354a4558694664524c73354277547137416b7656625653677155726e3930756d6950684854386857303448612f68626647584e59784a38474f445557726d4e366a2b714d5a4656434d59305a2b475666596646464830797575724a554e794d507a38434f2b524c5a374f586e594f4b3344555275784f366e76424248463451304c4577696450597a41365331416f4c69504a577a4e77366177594e644d486b687a74464644303431384c664430533630517031306947744470726e6c59725761633275466e356d696c546a735157442b4f4f6b797a4a78576b7450333545464266436f555449452f5a58524d3345513d3d227d7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3770026", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 824, + aggregator: "kyberSwap", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9223817200612836, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.770026, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001082441682149968, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001082441682149968 + } + } +] + const NETWORKS_2_API_MOCK_RESULT = { active: true, networkId: 1, @@ -482,5 +767,6 @@ module.exports = { GAS_PRICE_API_MOCK_RESULT, FEATURE_FLAGS_API_MOCK_RESULT, TRADES_API_MOCK_RESULT, + SWAP_TEST_ETH_USDC_TRADES_MOCK, NETWORKS_2_API_MOCK_RESULT, }; diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index db9440cb8c31..656ecb1a125d 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -11,7 +11,6 @@ import { validateContractDetails, multipleGanacheOptions, regularDelayMs, - openDapp, } from '../helpers'; import { Driver } from '../webdriver/driver'; import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; @@ -86,6 +85,9 @@ export async function installSnapSimpleKeyring( tag: 'button', }); + // Wait until popup is closed before proceeding + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); await driver.waitForSelector({ @@ -185,20 +187,8 @@ async function switchToAccount2(driver: Driver) { } export async function connectAccountToTestDapp(driver: Driver) { - try { - // Do an unusually fast switchToWindowWithTitle, just 1 second - await driver.switchToWindowWithTitle( - WINDOW_TITLES.TestDApp, - null, - 1000, - 1000, - ); - } catch { - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await openDapp(driver); - } + await switchToOrOpenDapp(driver); + await driver.clickElement('#connectButton'); await driver.delay(regularDelayMs); diff --git a/test/e2e/accounts/create-snap-account.spec.ts b/test/e2e/accounts/create-snap-account.spec.ts index 8dcff978bad9..7ead73e45c73 100644 --- a/test/e2e/accounts/create-snap-account.spec.ts +++ b/test/e2e/accounts/create-snap-account.spec.ts @@ -49,6 +49,9 @@ describe('Create Snap Account', function (this: Suite) { tag: 'button', }); + // Wait until popup is closed before proceeding + await driver.waitUntilXWindowHandles(2); + // move back to the Snap window to test the create account flow await driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, @@ -128,6 +131,9 @@ describe('Create Snap Account', function (this: Suite) { tag: 'button', }); + // Wait until popup is closed before proceeding + await driver.waitUntilXWindowHandles(2); + // move back to the Snap window to test the create account flow await driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, @@ -220,6 +226,9 @@ describe('Create Snap Account', function (this: Suite) { tag: 'button', }); + // Wait until popup is closed before proceeding + await driver.waitUntilXWindowHandles(2); + // move back to the Snap window to test the create account flow await driver.switchToWindowWithTitle( WINDOW_TITLES.SnapSimpleKeyringDapp, diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts index 8a5af361c029..b4ef979bf123 100644 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ b/test/e2e/accounts/snap-account-signatures.spec.ts @@ -1,6 +1,5 @@ import { Suite } from 'mocha'; import { - openDapp, tempToggleSettingRedesignedConfirmations, withFixtures, } from '../helpers'; @@ -33,8 +32,6 @@ describe('Snap Account Signatures', function (this: Suite) { await tempToggleSettingRedesignedConfirmations(driver); - await openDapp(driver); - // Run all 6 signature types const locatorIDs = [ '#ethSign', diff --git a/test/e2e/background-socket/server-mocha-to-background.ts b/test/e2e/background-socket/server-mocha-to-background.ts new file mode 100644 index 000000000000..3b4cc1c88b2f --- /dev/null +++ b/test/e2e/background-socket/server-mocha-to-background.ts @@ -0,0 +1,130 @@ +import events from 'events'; +import { WebSocketServer } from 'ws'; +import { + MessageType, + ServerMochaEventEmitterType, + WindowProperties, +} from './types'; + +/** + * This singleton class runs on the Mocha/Selenium test. + * It's used to communicate from the Mocha/Selenium test to the Extension background script (service worker in MV3). + */ +class ServerMochaToBackground { + private server: WebSocketServer; + + private ws: WebSocket | null = null; + + private eventEmitter; + + constructor() { + this.server = new WebSocketServer({ port: 8111 }); + + console.debug('ServerMochaToBackground created'); + + this.server.on('connection', (ws: WebSocket) => { + // Check for existing connection and close it + if (this.ws) { + console.error( + 'ServerMochaToBackground got a second client connection, closing the first one', + ); + this.ws.close(); + } + + this.ws = ws; + + console.debug('ServerMochaToBackground got a client connection'); + + ws.onmessage = (ev: MessageEvent) => { + let message: MessageType; + + try { + message = JSON.parse(ev.data); + } catch (e) { + throw new Error( + `Error in JSON sent to ServerMochaToBackground: ${ + (e as Error).message + }`, + ); + } + + this.receivedMessage(message); + }; + + ws.onclose = () => { + this.ws = null; + console.debug('ServerMochaToBackground disconnected from client'); + }; + }); + + this.eventEmitter = new events.EventEmitter(); + } + + // This function is never explicitly called, but in the future it could be + stop() { + this.ws?.close(); + + this.server.close(); + + console.debug('ServerMochaToBackground stopped'); + } + + // Send a message to the Extension background script (service worker in MV3) + send(message: MessageType) { + if (!this.ws) { + throw new Error('No client connected to ServerMochaToBackground'); + } + + this.ws.send(JSON.stringify(message)); + } + + // Handle messages received from the Extension background script (service worker in MV3) + private receivedMessage(message: MessageType) { + if (message.command === 'openTabs' && message.tabs) { + this.eventEmitter.emit('openTabs', message.tabs); + } else if (message.command === 'notFound') { + throw new Error( + `No window found by background script with ${message.property}: ${message.value}`, + ); + } + } + + // This is not used in the current code, but could be used in the future + queryTabs(tabTitle: string) { + this.send({ command: 'queryTabs', title: tabTitle }); + } + + // Sends the message to the Extension, and waits for a response + async waitUntilWindowWithProperty(property: WindowProperties, value: string) { + this.send({ command: 'waitUntilWindowWithProperty', property, value }); + + const tabs = await this.waitForResponse(); + // console.debug('ServerMochaToBackground got the response', tabs); + + // The return value here is less useful than we had hoped, because the tabs + // are not in the same order as driver.getAllWindowHandles() + return tabs; + } + + // This is a way to wait for an event async, without timeouts or polling + async waitForResponse() { + return new Promise((resolve) => { + this.eventEmitter.once('openTabs', resolve); + }); + } +} + +// Singleton setup below +let _serverMochaToBackground: ServerMochaToBackground; + +export function getServerMochaToBackground() { + if (!_serverMochaToBackground) { + startServerMochaToBackground(); + } + + return _serverMochaToBackground; +} + +function startServerMochaToBackground() { + _serverMochaToBackground = new ServerMochaToBackground(); +} diff --git a/test/e2e/background-socket/socket-background-to-mocha.ts b/test/e2e/background-socket/socket-background-to-mocha.ts new file mode 100644 index 000000000000..03f877b6aa0a --- /dev/null +++ b/test/e2e/background-socket/socket-background-to-mocha.ts @@ -0,0 +1,155 @@ +import log from 'loglevel'; +import { MessageType, WindowProperties } from './types'; + +/** + * This singleton class runs on the Extension background script (service worker in MV3). + * It's used to communicate from the Extension background script to the Mocha/Selenium test. + * The main advantage is that it can call chrome.tabs.query(). + * We had hoped it would be able to call chrome.tabs.highlight(), but Selenium doesn't see the tab change. + */ +class SocketBackgroundToMocha { + private client: WebSocket; + + constructor() { + this.client = new WebSocket('ws://localhost:8111'); + + this.client.onopen = () => + log.debug('SocketBackgroundToMocha WebSocket connection opened'); + + this.client.onmessage = (ev: MessageEvent) => { + let message: MessageType; + + try { + message = JSON.parse(ev.data); + } catch (e) { + throw new Error( + `Error in JSON sent to SocketBackgroundToMocha: ${ + (e as Error).message + }`, + ); + } + + this.receivedMessage(message); + }; + + this.client.onclose = () => + log.debug('SocketBackgroundToMocha WebSocket connection closed'); + + this.client.onerror = (error) => + log.error('SocketBackgroundToMocha WebSocket error:', error); + } + + /** + * Waits until a window with the given property is open. + * delayStep = 200ms, timeout = 10s + * + * You can think of this kind of like a template function: + * If `property` is `title`, then this becomes `waitUntilWindowWithTitle` + * If `property` is `url`, then this becomes `waitUntilWindowWithUrl` + * Remember that `a[property]` becomes `a.title` or `a.url` + * + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to wait for + * @returns The handle of the window tab with the given property value + */ + async waitUntilWindowWithProperty(property: WindowProperties, value: string) { + let tabs: chrome.tabs.Tab[] = []; + const delayStep = 200; + const timeout = 10000; + + for ( + let timeElapsed = 0; + timeElapsed <= timeout; + timeElapsed += delayStep + ) { + tabs = await this.queryTabs({}); + + const index = tabs.findIndex((a) => a[property] === value); + + if (index !== -1) { + this.send({ command: 'openTabs', tabs: this.cleanTabs(tabs) }); + return; + } + + // wait for delayStep milliseconds + await new Promise((resolve) => setTimeout(resolve, delayStep)); + } + + // The window was not found at the end of the timeout + this.send({ + command: 'notFound', + property, + value, + tabs: this.cleanTabs(tabs), + }); + } + + // This function exists to support both MV2 and MV3 + private async queryTabs(queryInfo: object): Promise { + if ( + process.env.ENABLE_MV3 === 'true' || + process.env.ENABLE_MV3 === undefined + ) { + // With MV3, chrome.tabs.query has an await form + return await chrome.tabs.query(queryInfo); + } + + // With MV2, we have to wrap chrome.tabs.query in a Promise + return new Promise((resolve) => { + chrome.tabs.query(queryInfo, (tabs: chrome.tabs.Tab[]) => { + resolve(tabs); + }); + }); + } + + // Clean up the tab data before sending them to the client + private cleanTabs(tabs: chrome.tabs.Tab[]): chrome.tabs.Tab[] { + return tabs.map((tab) => { + // This field can be very long, and is not needed + if (tab.favIconUrl && tab.favIconUrl.length > 40) { + tab.favIconUrl = undefined; + } + + return tab; + }); + } + + // Send a message to the Mocha/Selenium test + send(message: MessageType) { + this.client.send(JSON.stringify(message)); + } + + // Handle messages received from the Mocha/Selenium test + private async receivedMessage(message: MessageType) { + log.debug('SocketBackgroundToMocha received message:', message); + + if (message.command === 'queryTabs') { + const tabs = await this.queryTabs({ title: message.title }); + log.debug('SocketBackgroundToMocha sending tabs:', tabs); + this.send({ command: 'openTabs', tabs: this.cleanTabs(tabs) }); + } else if ( + message.command === 'waitUntilWindowWithProperty' && + message.property && + message.value + ) { + this.waitUntilWindowWithProperty(message.property, message.value); + } + } +} + +// Singleton setup below +let _socketBackgroundToMocha: SocketBackgroundToMocha; + +export function getSocketBackgroundToMocha() { + if (!_socketBackgroundToMocha) { + startSocketBackgroundToMocha(); + } + + return _socketBackgroundToMocha; +} + +function startSocketBackgroundToMocha() { + if (process.env.IN_TEST) { + _socketBackgroundToMocha = new SocketBackgroundToMocha(); + } +} diff --git a/test/e2e/background-socket/types.ts b/test/e2e/background-socket/types.ts new file mode 100644 index 000000000000..00a734374c90 --- /dev/null +++ b/test/e2e/background-socket/types.ts @@ -0,0 +1,24 @@ +export type MessageType = { + command: + | 'openTabs' + | 'notFound' + | 'queryTabs' + | 'waitUntilWindowWithProperty'; + tabs?: chrome.tabs.Tab[]; + title?: string; + property?: WindowProperties; + value?: string; +}; + +export type Handle = { + id: string; + title: string; + url: string; +}; + +export type WindowProperties = 'title' | 'url'; + +export type ServerMochaEventEmitterType = { + openTabs: [openTabs: chrome.tabs.Tab[]]; + notFound: [openTabs: chrome.tabs.Tab[]]; +}; diff --git a/test/e2e/background-socket/window-handles.ts b/test/e2e/background-socket/window-handles.ts new file mode 100644 index 000000000000..8e86a48a68cb --- /dev/null +++ b/test/e2e/background-socket/window-handles.ts @@ -0,0 +1,231 @@ +import log from 'loglevel'; +import { ThenableWebDriver } from 'selenium-webdriver'; +import { getServerMochaToBackground } from './server-mocha-to-background'; +import { Handle, WindowProperties } from './types'; + +/** + * Keeps a list of window handles and their properties (title, url). + * Takes control of switchToWindowWithTitle and switchToWindowWithUrl away from driver.js + */ +export class WindowHandles { + driver: ThenableWebDriver; + + rawHandles: string[] = []; + + annotatedHandles: Handle[] = []; + + constructor(driver: ThenableWebDriver) { + this.driver = driver; + } + + // Gets all window handles and annotates them with title and url + async getAllWindowHandles() { + this.rawHandles = await this.driver.getAllWindowHandles(); + + this.updateAnnotatedHandles(); + + log.debug('rawHandles', this.rawHandles); + log.debug('annotatedHandles', this.annotatedHandles); + + return this.rawHandles; + } + + // Remove outdated annotatedHandles and add new annotatedHandles + updateAnnotatedHandles() { + // Remove any annotatedHandles that are no longer present + for (let i = 0; i < this.annotatedHandles.length; i++) { + const handleId = this.annotatedHandles[i].id; + if (this.rawHandles.indexOf(handleId) === -1) { + log.debug('Removing handle:', this.annotatedHandles[i]); + this.annotatedHandles.splice(i, 1); + i -= 1; + } + } + + // Add any new rawHandles to the annotatedHandles + for (const handleId of this.rawHandles) { + if (!this.annotatedHandles.find((a) => a.id === handleId)) { + this.annotatedHandles.push({ id: handleId, title: '', url: '' }); + } + } + } + + /** + * Get the given property (title or url) of the current window. + * + * @param property - 'title' or 'url' + * @param optionalCurrentHandle - If we already know the current handle, we can pass it in here + * @returns Depending on `property`, returns the title or url of the current window, or null if no return value is requested + */ + async getCurrentWindowProperties( + property?: WindowProperties, + optionalCurrentHandle?: string, + ) { + const currentHandle = + optionalCurrentHandle || (await this.driver.getWindowHandle()); + + let currentTitle, currentUrl; + + // Wait 25 x 200ms = 5 seconds for the title and url to be set + for (let i = 0; i < 25 && !currentTitle && !currentUrl; i++) { + currentTitle = await this.driver.getTitle(); + currentUrl = await this.driver.getCurrentUrl(); + + if (!currentTitle || !currentUrl) { + await this.driver.sleep(200); + } + } + + if (!currentTitle || !currentUrl) { + log.debug( + 'Cannot get properties of current window', + currentHandle, + currentTitle, + currentUrl, + ); + return null; + } + + let annotatedHandle = this.annotatedHandles.find( + (a) => a.id === currentHandle, + ); + + if (annotatedHandle) { + // Handle already there, update it + annotatedHandle.title = currentTitle; + annotatedHandle.url = currentUrl; + } else { + // Handle not there, add it + annotatedHandle = { + id: currentHandle, + title: currentTitle, + url: currentUrl, + }; + this.annotatedHandles.push(annotatedHandle); + } + + if (property) { + // Return the current title or url + return annotatedHandle[property]; + } + + // Didn't ask for any return value + return null; + } + + // Count the number of handles with an unknown title + countUntitledHandles() { + return this.annotatedHandles.filter((a) => !a.title).length; + } + + // Count the number of handles with an unknown template property + countHandlesWithoutProperty(property: WindowProperties) { + return this.annotatedHandles.filter((a) => !a[property]).length; + } + + /** + * Switches the context of the browser session to the window/tab with the given property. + * + * You can think of this kind of like a template function: + * If `property` is `title`, then this becomes `switchToWindowWithTitle` + * If `property` is `url`, then this becomes `switchToWindowWithUrl` + * Remember that `a[property]` becomes `a.title` or `a.url` + * + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to switch to + * @returns The handle of the window tab with the given property value + */ + async switchToWindowWithProperty(property: WindowProperties, value: string) { + // Ask the extension to wait until the window with the given property is open + // (just ignore the return value here, because the tabs are not in the same order as driver.getAllWindowHandles()) + await getServerMochaToBackground().waitUntilWindowWithProperty( + property, + value, + ); + + await this.getAllWindowHandles(); + + // See if we already know the handle by annotation + const handle = this.annotatedHandles.find((a) => a[property] === value); + let handleId = handle?.id; + + // If there's exactly one un-annotated handle, we should try it + if (!handleId) { + if (this.countHandlesWithoutProperty(property) === 1) { + handleId = this.annotatedHandles.find((a) => !a[property])?.id; + } + } + + // Do the actual switching for the one we found + if (handleId) { + const matchesProperty = await this.switchToHandleAndCheckForProperty( + handleId, + property, + value, + ); + + if (matchesProperty) { + return handleId; + } + } + + // We have not found it in the annotatedHandles, or the one we found was wrong, so we have to cycle through all + // handles and bring them into focus to get their titles/urls. There is no need for repeats or delays, because + // ServerMochaToBackground has already waited for the tab to be found + for (handleId of this.rawHandles) { + const matchesProperty = await this.switchToHandleAndCheckForProperty( + handleId, + property, + value, + ); + + if (matchesProperty) { + return handleId; + } + } + + // If we still haven't found it, throw an error + throw new Error(`No window with ${property}: ${value}`); + } + + /** + * Switches the window and makes sure it matches the expected title or url. + * + * @param handleId - The handle we want to switch to + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to switch to + * @returns Whether the window we switched to has the expected property value + */ + async switchToHandleAndCheckForProperty( + handleId: string, + property: WindowProperties, + value: string, + ): Promise { + await this.driver.switchTo().window(handleId); + const handleProperty = await this.getCurrentWindowProperties( + property, + handleId, + ); + + return handleProperty === value; + } + + /** + * If we already know this window, switch to it + * Otherwise, return null + * This is used in helpers.switchToOrOpenDapp() and when there's an alert open + * + * @param title - The title of the window we want to switch to + */ + async switchToWindowIfKnown(title: string) { + const handle = this.annotatedHandles.find((a) => a.title === title); + const handleId = handle?.id; + + if (handleId) { + await this.driver.switchTo().window(handleId); + return handleId; + } + + return null; + } +} diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index cb68df89e9c0..738d766f8555 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -20,7 +20,10 @@ const ALL_PAGES = Object.values(PAGES); async function measurePage(pageName) { let metrics; await withFixtures( - { fixtures: new FixtureBuilder().build() }, + { + fixtures: new FixtureBuilder().build(), + disableServerMochaToBackground: true, + }, async ({ driver }) => { await driver.delay(tinyDelayMs); await unlockWallet(driver, { diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts index d84779ba79b6..92e24f47bf0b 100644 --- a/test/e2e/flask/user-operations.spec.ts +++ b/test/e2e/flask/user-operations.spec.ts @@ -22,6 +22,8 @@ import { import { buildQuote, reviewQuote } from '../tests/swaps/shared'; import { Driver } from '../webdriver/driver'; import { Bundler } from '../bundler'; +import { SWAP_TEST_ETH_USDC_TRADES_MOCK } from '../../data/mock-data'; +import { Mockttp } from '../mock-e2e'; enum TransactionDetailRowIndex { Nonce = 0, @@ -31,17 +33,19 @@ enum TransactionDetailRowIndex { async function installExampleSnap(driver: Driver) { await driver.openNewPage(ERC_4337_ACCOUNT_SNAP_URL); await driver.clickElement('#connectButton'); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); await driver.findElement({ text: 'Add to MetaMask', tag: 'h3' }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); + await driver.waitForSelector({ text: 'Confirm' }); await driver.clickElement({ text: 'Confirm', tag: 'button', }); + await driver.waitForSelector({ text: 'OK' }); await driver.clickElement({ text: 'OK', tag: 'button', @@ -102,6 +106,7 @@ async function createSwap(driver: Driver) { swapFrom: 'TESTETH', swapTo: 'USDC', }); + await driver.clickElement({ text: 'Swap', tag: 'button' }); await driver.clickElement({ text: 'Close', tag: 'button' }); } @@ -167,6 +172,17 @@ async function expectTransactionDetailsMatchReceipt( ); } +async function mockSwapsTransactionQuote(mockServer: Mockttp) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => ({ + statusCode: 200, + json: SWAP_TEST_ETH_USDC_TRADES_MOCK, + })), + ]; +} + async function withAccountSnap( { title, paymaster }: { title?: string; paymaster?: string }, test: (driver: Driver, bundlerServer: Bundler) => Promise, @@ -183,6 +199,7 @@ async function withAccountSnap( ganacheOptions: { hardfork: 'london', }, + testSpecificMock: mockSwapsTransactionQuote, }, async ({ driver, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index b00498832560..26dbba91f0b5 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -20,6 +20,9 @@ const { ERC_4337_ACCOUNT, DEFAULT_GANACHE_ETH_BALANCE_DEC, } = require('./constants'); +const { + getServerMochaToBackground, +} = require('./background-socket/server-mocha-to-background'); const tinyDelayMs = 200; const regularDelayMs = tinyDelayMs * 2; @@ -48,6 +51,7 @@ async function withFixtures(options, testSuite) { ignoredConsoleErrors = [], dappPath = undefined, disableGanache, + disableServerMochaToBackground = false, dappPaths, testSpecificMock = function () { // do nothing. @@ -70,6 +74,10 @@ async function withFixtures(options, testSuite) { const dappServer = []; const phishingPageServer = new PhishingWarningPageServer(); + if (!disableServerMochaToBackground) { + getServerMochaToBackground(); + } + let webDriver; let driver; let failed = false; @@ -616,6 +624,12 @@ const testSRPDropdownIterations = async (options, driver, iterations) => { const openSRPRevealQuiz = async (driver) => { // navigate settings to reveal SRP await driver.clickElement('[data-testid="account-options-menu-button"]'); + + // fix race condition with mmi build + if (process.env.MMI) { + await driver.waitForSelector('[data-testid="global-menu-mmi-portfolio"]'); + } + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); await driver.clickElement('[data-testid="reveal-seed-words"]'); @@ -698,15 +712,11 @@ const switchToOrOpenDapp = async ( contract = null, dappURL = DAPP_URL, ) => { - try { - // Do an unusually fast switchToWindowWithTitle, just 1 second - await driver.switchToWindowWithTitle( - WINDOW_TITLES.TestDApp, - null, - 1000, - 1000, - ); - } catch { + const handle = await driver.windowHandles.switchToWindowIfKnown( + WINDOW_TITLES.TestDApp, + ); + + if (!handle) { await openDapp(driver, contract, dappURL); } }; @@ -797,7 +807,6 @@ const editGasFeeForm = async (driver, gasLimit, gasPrice) => { }; const openActionMenuAndStartSendFlow = async (driver) => { - await driver.delay(500); await driver.clickElement('[data-testid="eth-overview-send"]'); }; @@ -821,22 +830,10 @@ const sendScreenToConfirmScreen = async ( await driver.fill('.unit-input__input', quantity); // check if element exists and click it - await driver - .findElement({ - text: 'I understand', - tag: 'button', - }) - .then( - (_found) => { - driver.clickElement({ - text: 'I understand', - tag: 'button', - }); - }, - (error) => { - console.error('Element not found.', error); - }, - ); + await driver.clickElementSafe({ + text: 'I understand', + tag: 'button', + }); await driver.clickElement({ text: 'Continue', tag: 'button' }); }; @@ -1010,29 +1007,18 @@ async function validateContractDetails(driver) { await driver.clickElement(verifyDetailsBtnSelector); await driver.clickElement({ text: 'Got it', tag: 'button' }); - // Approve signing typed data - try { - await driver.clickElement( - '[data-testid="signature-request-scroll-button"]', - ); - } catch (error) { - // Ignore error if scroll button is not present - } - await driver.delay(regularDelayMs); + await driver.clickElementSafe( + '[data-testid="signature-request-scroll-button"]', + ); } /** - * This method assumes the extension is open, the dapp is open and waits for a - * third window handle to open (the notification window). Once it does it - * switches to the new window. - * + * @deprecated since the background socket was added, and special handling is no longer necessary + * Just call `await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog)` instead. * @param {WebDriver} driver - * @param numHandles */ -async function switchToNotificationWindow(driver, numHandles = 3) { - const windowHandles = await driver.waitUntilXWindowHandles(numHandles); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog, windowHandles); +async function switchToNotificationWindow(driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); } /** @@ -1176,6 +1162,11 @@ async function tempToggleSettingRedesignedConfirmations(driver) { await driver.waitForSelector(accountOptionsMenuSelector); await driver.clickElement(accountOptionsMenuSelector); + // fix race condition with mmi build + if (process.env.MMI) { + await driver.waitForSelector('[data-testid="global-menu-mmi-portfolio"]'); + } + // Click settings from dropdown menu await driver.clickElement('[data-testid="global-menu-settings"]'); diff --git a/test/e2e/mv3-perf-stats/init-load-stats.js b/test/e2e/mv3-perf-stats/init-load-stats.js index f9975557d3af..40584343d990 100755 --- a/test/e2e/mv3-perf-stats/init-load-stats.js +++ b/test/e2e/mv3-perf-stats/init-load-stats.js @@ -22,7 +22,10 @@ async function profilePageLoad() { const parsedLogs = {}; try { await withFixtures( - { fixtures: new FixtureBuilder().build() }, + { + fixtures: new FixtureBuilder().build(), + disableServerMochaToBackground: true, + }, async ({ driver }) => { await driver.delay(tinyDelayMs); await driver.navigate(); diff --git a/test/e2e/snaps/test-snap-metrics.spec.js b/test/e2e/snaps/test-snap-metrics.spec.js index bd414c209402..6856aab2fd08 100644 --- a/test/e2e/snaps/test-snap-metrics.spec.js +++ b/test/e2e/snaps/test-snap-metrics.spec.js @@ -1,7 +1,6 @@ const { strict: assert } = require('assert'); const { withFixtures, - switchToNotificationWindow, unlockWallet, getEventPayloads, WINDOW_TITLES, @@ -199,16 +198,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -292,16 +283,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -378,16 +361,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -455,16 +430,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -485,8 +452,9 @@ describe('Test Snap Metrics', function () { }); // switch to the original MM tab - const extensionPage = windowHandles[0]; - await driver.switchToWindow(extensionPage); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // click on the global action menu await driver.waitForSelector( @@ -580,8 +548,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -620,12 +588,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -639,8 +602,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdateNew'); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Confirm' }); @@ -659,7 +622,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // look for the correct version text await driver.waitForSelector({ @@ -711,6 +674,9 @@ describe('Test Snap Metrics', function () { .build(), title: this.test.fullTitle(), testSpecificMock: mockSegment, + ignoredConsoleErrors: [ + 'MetaMask - RPC Error: User rejected the request.', + ], }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); @@ -730,8 +696,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -770,12 +736,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -789,8 +750,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdateNew'); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Confirm' }); @@ -801,7 +762,10 @@ describe('Test Snap Metrics', function () { tag: 'button', }); - await driver.switchToWindow(windowHandles[0]); + // It is necessary to use switchToWindowIfKnown because there is an alert open. + // Trying to use switchToWindowWithTitle (the new Socket version) or even + // closeAlertPopup will cause an error. + await driver.switchToWindowIfKnown(WINDOW_TITLES.TestSnaps); // check that snap updated event metrics have been sent const events = await getEventPayloads(driver, mockedEndpoints); @@ -846,7 +810,9 @@ describe('Test Snap Metrics', function () { .build(), title: this.test.fullTitle(), testSpecificMock: mockSegment, - ignoredConsoleErrors: ['Object'], + ignoredConsoleErrors: [ + 'MetaMask - RPC Error: Failed to fetch snap "npm:@metamask/bip32-example-snap": Failed to fetch tarball for package "@metamask/bip32-example-snap"..', + ], }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); @@ -866,8 +832,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -906,12 +872,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -928,8 +889,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.closeAlertPopup(); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Update failed' }); diff --git a/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts index 766c4561fd56..9521b91cbb21 100644 --- a/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts +++ b/test/e2e/tests/bridge/bridge-click-from-asset-overview.spec.ts @@ -5,6 +5,9 @@ import GanacheContractAddressRegistry from '../../seeder/ganache-contract-addres import { Driver } from '../../webdriver/driver'; import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; +const EXPECTED_PORTFOLIO_URL = + 'https://portfolio.metamask.io/bridge?metametricsId=null&metricsEnabled=false&marketingEnabled=false'; + describe('Click bridge button from asset page @no-mmi', function (this: Suite) { it('loads portfolio tab when flag is turned off', async function () { await withFixtures( @@ -25,18 +28,13 @@ describe('Click bridge button from asset page @no-mmi', function (this: Suite) { // ETH await bridgePage.loadAssetPage(contractRegistry); await bridgePage.load('coin-overview'); - await bridgePage.verifyPortfolioTab( - 'https://portfolio.metamask.io/bridge?metametricsId=null', - ); - + await bridgePage.verifyPortfolioTab(EXPECTED_PORTFOLIO_URL); await bridgePage.reloadHome(); // TST await bridgePage.loadAssetPage(contractRegistry, 'TST'); await bridgePage.load('token-overview'); - await bridgePage.verifyPortfolioTab( - 'https://portfolio.metamask.io/bridge?metametricsId=null', - ); + await bridgePage.verifyPortfolioTab(EXPECTED_PORTFOLIO_URL); }, ); }); diff --git a/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts index 0a6098e01592..df4794ec8f22 100644 --- a/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts +++ b/test/e2e/tests/bridge/bridge-click-from-eth-overview.spec.ts @@ -19,7 +19,7 @@ describe('Click bridge button from wallet overview @no-mmi', function (this: Sui await logInWithBalanceValidation(driver, ganacheServer); await bridgePage.load(); await bridgePage.verifyPortfolioTab( - 'https://portfolio.metamask.io/bridge?metametricsId=null', + 'https://portfolio.metamask.io/bridge?metametricsId=null&metricsEnabled=false&marketingEnabled=false', ); }, ); diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index 2d34b011f7ac..b044ebb571ff 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -143,8 +143,7 @@ async function assertInfoValues(driver: Driver) { async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitUntilXWindowHandles(2); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + await driver.switchToWindowWithTitle('E2E Test Dapp'); await driver.clickElement('#signTypedDataV3Verify'); await driver.delay(500); diff --git a/test/e2e/tests/network/chain-interactions.spec.js b/test/e2e/tests/network/chain-interactions.spec.js index c03387c93155..ba774ffecdb1 100644 --- a/test/e2e/tests/network/chain-interactions.spec.js +++ b/test/e2e/tests/network/chain-interactions.spec.js @@ -3,8 +3,8 @@ const { generateGanacheOptions, withFixtures, openDapp, - unlockWallet, WINDOW_TITLES, + logInWithBalanceValidation, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -23,18 +23,13 @@ describe('Chain Interactions', function () { title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // trigger add chain confirmation await openDapp(driver); await driver.clickElement('#addEthereumChain'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // verify chain details const [networkName, networkUrl, chainIdElement] = @@ -47,9 +42,9 @@ describe('Chain Interactions', function () { await driver.clickElement({ text: 'Approve', tag: 'button' }); await driver.clickElement({ text: 'Cancel', tag: 'button' }); - // switch to extension - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // verify networks await driver.findElement({ @@ -76,26 +71,22 @@ describe('Chain Interactions', function () { title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // trigger add chain confirmation await openDapp(driver); await driver.clickElement('#addEthereumChain'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // approve and switch chain await driver.clickElement({ text: 'Approve', tag: 'button' }); await driver.clickElement({ text: 'Switch network', tag: 'button' }); // switch to extension - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // verify current network await driver.findElement({ diff --git a/test/e2e/tests/portfolio/portfolio-site.spec.js b/test/e2e/tests/portfolio/portfolio-site.spec.js index 8e8c0ab917c0..99c515ac36be 100644 --- a/test/e2e/tests/portfolio/portfolio-site.spec.js +++ b/test/e2e/tests/portfolio/portfolio-site.spec.js @@ -42,10 +42,10 @@ describe('Portfolio site', function () { await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); // Verify site - assert.equal( - await driver.getCurrentUrl(), - 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null', - ); + const currentUrl = await driver.getCurrentUrl(); + const expectedUrl = + 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false'; + assert.equal(currentUrl, expectedUrl); }, ); }); diff --git a/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js b/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js index fb609461d42b..8f3e7a657716 100644 --- a/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js +++ b/test/e2e/tests/ppom/ppom-blockaid-alert-simple-send.spec.js @@ -5,7 +5,7 @@ const { defaultGanacheOptions, withFixtures, sendScreenToConfirmScreen, - unlockWallet, + logInWithBalanceValidation, } = require('../../helpers'); const { mockServerJsonRpc } = require('./mocks/mock-server-json-rpc'); @@ -142,8 +142,7 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { }, async ({ driver }) => { - // await driver.delay(10000) - await unlockWallet(driver); + await logInWithBalanceValidation(driver); await sendScreenToConfirmScreen(driver, mockBenignAddress, '1'); // await driver.delay(100000) @@ -174,7 +173,7 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); await sendScreenToConfirmScreen(driver, mockMaliciousAddress, '1'); @@ -214,8 +213,7 @@ describe('Simple Send Security Alert - Blockaid @no-mmi', function () { }, async ({ driver }) => { - // await driver.delay(10000) - await unlockWallet(driver); + await logInWithBalanceValidation(driver); await sendScreenToConfirmScreen( driver, diff --git a/test/e2e/tests/settings/full-size-view-settings.spec.js b/test/e2e/tests/settings/full-size-view-settings.spec.js index bef81caf6183..852fe3748039 100644 --- a/test/e2e/tests/settings/full-size-view-settings.spec.js +++ b/test/e2e/tests/settings/full-size-view-settings.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, unlockWallet, @@ -47,11 +46,11 @@ describe('Full-size View Setting @no-mmi', function () { const [newWindowHandle] = windowHandlesPostClick.filter( (handleId) => !windowHandlesPreClick.includes(handleId), ); - const newWindowTitle = await driver.getWindowTitleByHandlerId( + + await driver.switchToHandleAndWaitForTitleToBe( newWindowHandle, + WINDOW_TITLES.Dialog, ); - - assert.equal(newWindowTitle, WINDOW_TITLES.Dialog); }, ); }); diff --git a/test/e2e/tests/settings/show-hex-data.spec.js b/test/e2e/tests/settings/show-hex-data.spec.js index d5714c64c6df..5e0ae9a133a0 100644 --- a/test/e2e/tests/settings/show-hex-data.spec.js +++ b/test/e2e/tests/settings/show-hex-data.spec.js @@ -8,7 +8,8 @@ const FixtureBuilder = require('../../fixture-builder'); const selectors = { accountOptionsMenu: '[data-testid="account-options-menu-button"]', - settingsDiv: { text: 'Settings', tag: 'div' }, + settingsDiv: '[data-testid="global-menu-settings"]', + portfolioMenuOption: '[data-testid="global-menu-mmi-portfolio"]', advancedDiv: { text: 'Advanced', tag: 'div' }, hexDataToggle: '[data-testid="advanced-setting-hex-data"] .toggle-button', appHeaderLogo: '[data-testid="app-header-logo"]', @@ -31,7 +32,11 @@ const inputData = { // Function to click elements in sequence async function clickElementsInSequence(driver, clickSelectors) { for (const selector of clickSelectors) { - await driver.waitForSelector(selector); + if (process.env.MMI && selector === selectors.settingsDiv) { + await driver.waitForSelector(selectors.portfolioMenuOption); + } else { + await driver.waitForSelector(selector); + } await driver.clickElement(selector); } } diff --git a/test/e2e/user-actions-benchmark.js b/test/e2e/user-actions-benchmark.js index 3ed21821d265..c9ea7c88b728 100644 --- a/test/e2e/user-actions-benchmark.js +++ b/test/e2e/user-actions-benchmark.js @@ -33,6 +33,7 @@ async function loadNewAccount() { { fixtures: new FixtureBuilder().build(), ganacheOptions, + disableServerMochaToBackground: true, }, async ({ driver }) => { await unlockWallet(driver); @@ -64,6 +65,7 @@ async function confirmTx() { { fixtures: new FixtureBuilder().build(), ganacheOptions, + disableServerMochaToBackground: true, }, async ({ driver, ganacheServer }) => { await logInWithBalanceValidation(driver, ganacheServer); diff --git a/test/e2e/vault-decryption-chrome.spec.js b/test/e2e/vault-decryption-chrome.spec.js index 2241735521ce..a04e25f176de 100644 --- a/test/e2e/vault-decryption-chrome.spec.js +++ b/test/e2e/vault-decryption-chrome.spec.js @@ -90,35 +90,47 @@ async function getSRP(driver) { describe('Vault Decryptor Page', function () { it('is able to decrypt the vault using the vault-decryptor webapp', async function () { - await withFixtures({}, async ({ driver }) => { - // we don't need to use navigate - // since MM will automatically open a new window in prod build - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle('MetaMask'); - // create a new vault through onboarding flow - await completeCreateNewWalletOnboardingFlowWithOptOut( - driver, - WALLET_PASSWORD, - ); - // close popover if any (Announcements etc..) - await closePopoverIfPresent(driver); - // obtain SRP - const seedPhrase = await getSRP(driver); + await withFixtures( + { + disableServerMochaToBackground: true, + }, + async ({ driver }) => { + // we don't need to use navigate + // since MM will automatically open a new window in prod build + await driver.waitUntilXWindowHandles(2); - // navigate to the Vault decryptor webapp - await driver.openNewPage(VAULT_DECRYPTOR_PAGE); - // fill the input field with storage recovered from filesystem - await driver.clickElement('[name="vault-source"]'); - const inputField = await driver.findElement('#fileinput'); - inputField.press(await getExtensionStorageFilePath(driver)); - // fill in the password - await driver.fill('#passwordinput', WALLET_PASSWORD); - // decrypt - await driver.clickElement('.decrypt'); - const decrypted = await driver.findElement('.content div div div'); - const recoveredVault = JSON.parse(await decrypted.getText()); + // we cannot use the customized driver functions + // as there is no socket for window communications in prod builds + const windowHandles = await driver.driver.getAllWindowHandles(); - assert.equal(recoveredVault[0].data.mnemonic, seedPhrase); - }); + // switch to MetaMask window + await driver.driver.switchTo().window(windowHandles[2]); + + // create a new vault through onboarding flow + await completeCreateNewWalletOnboardingFlowWithOptOut( + driver, + WALLET_PASSWORD, + ); + // close popover if any (Announcements etc..) + await closePopoverIfPresent(driver); + // obtain SRP + const seedPhrase = await getSRP(driver); + + // navigate to the Vault decryptor webapp + await driver.openNewPage(VAULT_DECRYPTOR_PAGE); + // fill the input field with storage recovered from filesystem + await driver.clickElement('[name="vault-source"]'); + const inputField = await driver.findElement('#fileinput'); + inputField.press(await getExtensionStorageFilePath(driver)); + // fill in the password + await driver.fill('#passwordinput', WALLET_PASSWORD); + // decrypt + await driver.clickElement('.decrypt'); + const decrypted = await driver.findElement('.content div div div'); + const recoveredVault = JSON.parse(await decrypted.getText()); + + assert.equal(recoveredVault[0].data.mnemonic, seedPhrase); + }, + ); }); }); diff --git a/test/e2e/webdriver/README.md b/test/e2e/webdriver/README.md index 02e0bf25859a..da9232b04e3e 100644 --- a/test/e2e/webdriver/README.md +++ b/test/e2e/webdriver/README.md @@ -24,9 +24,11 @@ The **`buildLocator`** function enhances element-matching capabilities by introd [source](https://github.com/MetaMask/metamask-extension/blob/1f2bfb388695034db8859877ed21b4b045514f9f/test/e2e/webdriver/driver.js#L190) #### Arguments + @param {string | object} locator - this could be 'css' or 'xpath' and value to use with the locator strategy. #### Returns + @returns {object} By object that can be used to locate elements.
@throws {Error} Will throw an error if an invalid locator strategy is provided. @@ -96,7 +98,7 @@ CSS Selectors in Selenium are string patterns used to identify an element based > ```tsx > await driver.findElement({ text: 'Delete contact', tag: 'a' }); > ``` -> +
Locate element by XPath @@ -195,14 +197,16 @@ Finding web elements is a fundamental task in web automation and testing, allowi
findElement -> **`findElement`** returns a reference to the first element in the DOM that the provided locator matches. +> **`findElement`** returns a reference to the first element in the DOM that the provided locator matches. > > [source](https://github.com/MetaMask/metamask-extension/blob/1f2bfb388695034db8859877ed21b4b045514f9f/test/e2e/webdriver/driver.js#L458) > > #### Arguments +> > @param {string} rawLocator - element locator > > #### Returns +> > @return `{Promise}` A promise that resolves to the WebElement. > > **Example - Evaluating entire DOM** @@ -219,7 +223,7 @@ Finding web elements is a fundamental task in web automation and testing, allowi > text: 'Localhost 8545', > }); > ``` -> +
findElements @@ -229,9 +233,11 @@ Finding web elements is a fundamental task in web automation and testing, allowi > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L370) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise>} `A promise that resolves to an array of found elements. > > **Example for all matching FindElements** @@ -247,7 +253,7 @@ Finding web elements is a fundamental task in web automation and testing, allowi > const warning = warnings[1]; > warningText = await warning.getText(); > ``` -> +
findVisibleElement @@ -257,9 +263,11 @@ Finding web elements is a fundamental task in web automation and testing, allowi > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L355) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @return `{Promise}` A promise that resolves to the WebElement. > > **Example for all matching** findVisibleElement @@ -269,7 +277,6 @@ Finding web elements is a fundamental task in web automation and testing, allowi > '[data-testid="confirm-delete-network-modal"]', > ); > ``` ->
@@ -280,9 +287,11 @@ Finding web elements is a fundamental task in web automation and testing, allowi > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L361) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @return `{Promise}` A promise that resolves to the WebElement. > > **Example for f**indClickableElement @@ -290,8 +299,7 @@ Finding web elements is a fundamental task in web automation and testing, allowi > ```jsx > await driver.findClickableElement('#depositButton'); > ``` -> -> +
findClickableElements @@ -300,9 +308,11 @@ Finding web elements is a fundamental task in web automation and testing, allowi > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L379) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @return `{Promise}` A promise that resolves to the WebElement. > > **Example** @@ -313,7 +323,7 @@ Finding web elements is a fundamental task in web automation and testing, allowi > ); > assert.equal(domains.length, 1); > ``` -> +
## Interactions @@ -336,10 +346,12 @@ Each of these actions requires first [locating](#locators) the web element you w > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L208) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param {string} input - The value to fill the element > > #### Returns +> > @returns `{Promise}` Promise resolving to the filled element > > **Example** @@ -350,8 +362,7 @@ Each of these actions requires first [locating](#locators) the web element you w > '0xc427D562164062a23a5cFf596A4a3208e72Acd28', > ); > ``` -> -> +
clickElementSafe @@ -360,6 +371,7 @@ Each of these actions requires first [locating](#locators) the web element you w > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L420) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param timeout - The maximum time in ms to wait for the element > @@ -372,7 +384,7 @@ Each of these actions requires first [locating](#locators) the web element you w > ```jsx > await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); > ``` -> +
clickElement @@ -381,9 +393,11 @@ Each of these actions requires first [locating](#locators) the web element you w > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L393) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise}` - promise that resolves to the WebElement > > **Example** @@ -392,7 +406,6 @@ Each of these actions requires first [locating](#locators) the web element you w > const nextPageButton = '[data-testid="page-container-footer-next"]', > await driver.clickElement(nextPageButton); > ``` ->
press @@ -402,10 +415,12 @@ Each of these actions requires first [locating](#locators) the web element you w > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L214) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param {string} keys - The key to press > > #### Returns +> > @returns `{Promise}` promise resolving to the filled element > **Example** > @@ -426,7 +441,6 @@ Each of these actions requires first [locating](#locators) the web element you w > MODIFIER: process.platform === 'darwin' ? Key.COMMAND : Key.CONTROL, > }; > ``` ->
getText @@ -434,9 +448,11 @@ Each of these actions requires first [locating](#locators) the web element you w > **`getText`** function in Selenium is used to retrieve the visible text of a web element. > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns {text} String > > **Example** @@ -478,11 +494,13 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L224) > > #### Arguments +> > @param {Function} condition - condition or function the method awaits to become true.
> @param {number} timeout - Optional parameter specifies the maximum milliseconds to wait.
> @param catchError - Optional parameter that determines whether errors during the wait should be caught and handled within the function > > #### Returns +> > @returns {Promise} - promise resolving with a delay.
> @throws {Error} Will throw an error if the condition is not met within the timeout period. > @@ -514,7 +532,7 @@ This organization helps provide a clear structure for understanding the various > return isPending === false; > }, 3000); > ``` -> +
waitForSelector @@ -533,6 +551,7 @@ This organization helps provide a clear structure for understanding the various > The other supported state is 'detached', which means waiting until the element is removed from the DOM. > > #### Returns +> > @returns `{Promise}` promise resolving when the element meets the state or timeout occurs.
> @throws {Error} Will throw an error if the element does not reach the specified state within the timeout period. > @@ -541,7 +560,7 @@ This organization helps provide a clear structure for understanding the various > ```jsx > await driver.waitForSelector('.import-srp__actions'); > ``` -> +
waitForMultipleSelectors @@ -580,9 +599,11 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L262) > > #### Arguments +> > @param {string | object} element - Element locator > > #### Returns +> > @returns `{Promise}` promise resolving once the element fills or timeout hits.
> @throws {Error} Will throw an error if the element does not become non-empty within the timeout period. > @@ -594,8 +615,7 @@ This organization helps provide a clear structure for understanding the various > ); > await driver.waitForNonEmptyElement(revealedSeedPhrase); > ``` -> -> +
waitForElementState @@ -604,11 +624,13 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L55) > > #### Arguments +> > @param {WebElement} element - Element locator
> @param {string} state - state to wait for could be 'visible', 'hidden', 'enabled', 'disabled' > @param {number} [timeout=5000] - amount of time in milliseconds to wait before timing out > > #### Returns +> > @returns `'{Promise}'` A promise that resolves when the element is in the specified state.
> @throws {Error} Will throw an error if the element does not reach the specified state within the timeout period. > @@ -619,7 +641,7 @@ This organization helps provide a clear structure for understanding the various > // Wait for network to change and token list to load from state > await networkSelectionModal.waitForElementState('hidden'); > ``` -> +
clickElementAndWaitToDisappear
@@ -631,6 +653,7 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L406) > > #### Arguments +> > @param rawLocator - Element locator
> @param timeout - The maximum time in ms to wait for the element to disappear after clicking. > @@ -651,10 +674,12 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L270) > > #### Arguments +> > @param {string | object} rawLocator - element locator > @param {number} count - The expected number of elements.
> > #### Returns +> > @returns {Promise} promise resolving when the count of elements is matched. > > **Example** @@ -675,10 +700,12 @@ This organization helps provide a clear structure for understanding the various > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L574) > > #### Arguments +> > @param {number} x - The number of window handles to wait for
> @param {number} [timeout=5000] - The amount of time in milliseconds to wait before timing out
> > #### Returns +> > @returns `{Promise}` promise resolving when the target window handle count is met
> @throws {Error} - throws an error if the target number of window handles isn't met by the timeout > @@ -731,10 +758,12 @@ await approveInput.clear(); > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L502) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > @param {string} contentToPaste - content to paste. > > #### Returns +> > @return `{Promise}` A promise that resolves to the WebElement. > > **Example** @@ -742,7 +771,7 @@ await approveInput.clear(); > ```jsx > await driver.pasteIntoField('#bip44Message', '1234'); > ``` -> +
### Mouse @@ -758,9 +787,11 @@ A representation of any pointer device for interacting with a web page. > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L440) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise}` promise that will be fulfilled when the click command has completed. > > **Example** @@ -771,7 +802,7 @@ A representation of any pointer device for interacting with a web page. > tag: 'div', > }); > ``` -> +
scrollToElement @@ -781,9 +812,11 @@ A representation of any pointer device for interacting with a web page. > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L471) > > #### Arguments +> > @param {string | object} element - Element locator > > #### Returns +> > @returns `{Promise}` - promise resolving after scrolling > > **Example** @@ -795,7 +828,7 @@ A representation of any pointer device for interacting with a web page. > await driver.scrollToElement(removeButton); > await driver.clickElement('[data-testid="remove-snap-button"]'); > ``` -> +
holdMouseDownOnElement @@ -805,10 +838,12 @@ A representation of any pointer device for interacting with a web page. > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L459) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param {int} ms - The number of milliseconds to hold the mouse button down > > #### Returns +> > @returns `{Promise}` - promise resolving after mouse down completed > > **Example** @@ -822,6 +857,7 @@ A representation of any pointer device for interacting with a web page. > 2000, > ); > ``` +
clickPoint @@ -831,13 +867,15 @@ A representation of any pointer device for interacting with a web page. > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L450) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param {number} x - x coordinate to click at
> @param {number} y - y coordinate to click at
> > #### Returns -> @returns `{Promise}` - promise resolving after a click > +> @returns `{Promise}` - promise resolving after a click +
## Navigation @@ -855,9 +893,11 @@ Navigation refers to the process of moving through web pages within a browser se > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L522) > > #### Arguments +> > @param {string} [page] - its optional parameter to specify the page you want to navigate. Defaults to home if no other page is specified. > > #### Returns +> > @returns {Promise} promise resolves when the page has finished loading
> @throws {Error} Will throw an error if the navigation fails or the page does not load within the timeout period. > @@ -878,7 +918,7 @@ Navigation refers to the process of moving through web pages within a browser se > ```jsx > await driver.navigate(PAGES.BACKGROUND); > ``` -> +
getCurrentUrl @@ -888,6 +928,7 @@ Navigation refers to the process of moving through web pages within a browser se > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L532) > > #### Returns +> > @returns `{Promise}` promise resolves upon retrieving the text. > > **Example** @@ -895,7 +936,6 @@ Navigation refers to the process of moving through web pages within a browser se > ```jsx > const currentUrl = await driver.getCurrentUrl(); > ``` ->
@@ -914,7 +954,6 @@ Navigation refers to the process of moving through web pages within a browser se > ```jsx > await driver.refresh(); > ``` -> @@ -932,9 +971,11 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L547) > > #### Arguments +> > @param {string} url - The URL to navigate to in the new window tab. > > #### Returns +> > @returns {newHandle} The handle of the new window tab. This handle can be used later to switch between different tab windows during the test. > > **Example** @@ -952,9 +993,11 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L543) > > #### Arguments +> > @param {string} url - Any URL > > #### Returns +> > @returns `{Promise}` promise resolves when the URL page has finished loading > > **Example** @@ -972,6 +1015,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L562) > > #### Returns +> > @returns `{Promise}` A promise resolves after switching to the new window. > > **Example** @@ -991,6 +1035,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L570) > > #### Returns +> > @returns {int} - number of windows
> @return `{Promise>}` A promise that will be resolved with an array of window handles. > @@ -1002,23 +1047,27 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut -
getWindowTitleByHandlerId +
switchToHandleAndWaitForTitleToBe -> **`getWindowTitleByHandlerId`** function changes to a specific window tab using its ID and gets its title. +> **`switchToHandleAndWaitForTitleToBe`** switches to a specific window tab using its ID and waits for the title to match the expectedTitle. > > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L589) > > #### Arguments -> @param {int} handlerId - unique ID for the tab whose title is needed +> +> @param {int} handleId - unique ID for the tab whose title is needed. +> @param {string} expectedTitle - the title we are expecting. > > #### Returns -> @returns `{Promise} `promise resolving to the window tab title after command completion +> +> @returns nothing on success. > > **Example** > > ```jsx -> const fullScreenWindowTitle = await driver.getWindowTitleByHandlerId( +> const fullScreenWindowTitle = await driver.switchToHandleAndWaitForTitleToBe( > windowHandles[0], +> WINDOW_TITLES.Dialog, > ); > ``` @@ -1033,9 +1082,11 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L558) > > #### Arguments +> > @param {int} handle - unique ID for the tab to which you want to switch. > > #### Returns +> > @returns `{Promise}` promise that resolves once the switch is complete > > **Example** @@ -1053,6 +1104,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L631) > > #### Arguments +> > @param {string} url - The URL of the window tab to switch
> @param {string} [initialWindowHandles] - optional array of window handles to search through
> If not provided, the function fetches all current window handles.
@@ -1062,6 +1114,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > @param {int} retryDelay - optional for retrying the URL fetch operation, with defaults max 2500 ms
> > #### Returns +> > @returns `{Promise} `- promise that resolves once the switch is complete
> @throws {Error} - throws an error if no window with the specified url is found > @@ -1081,6 +1134,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L594) > > #### Arguments +> > @param {string} title - The title of the window tab to switch
> @param {string} [initialWindowHandles] - optional array of window handles to search through
> If not provided, the function fetches all current window handles
@@ -1090,6 +1144,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > @param {int} retryDelay - optional for retrying the title fetch operation, with defaults max 2500 ms
> > #### Returns +> > @returns `{Promise}` promise that resolves once the switch is complete
> @throws {Error} throws an error if no window with the specified title is found. > @@ -1113,6 +1168,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L668) > > #### Returns +> > @returns `{Promise}` promise resolving after closing the current window > > **Example** @@ -1130,9 +1186,11 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L672) > > #### Arguments +> > @param {string} windowHandle - representing the unique identifier of the browser window to be closed. > > #### Returns +> > @returns `{Promise} `- promise resolving after closing the specified window > > **Example** @@ -1154,10 +1212,12 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L689) > > #### Arguments +> > @param {Array} exceptions - list of window handle exceptions
> @param {Array} [windowHandles] - full list of window handles > > #### Returns +> > @returns `{Promise}`- promise resolving after closing the specified window
@@ -1169,6 +1229,7 @@ Web browsers can have multiple windows or tabs open at the same time. In web aut > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L336) > > #### Returns +> > @returns `{Promise}` - promise resolving after quitting > > **Example** @@ -1192,6 +1253,7 @@ Alerts are pop-up messages that appear > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L678) > > #### Returns +> > @returns `{Promise}` promise resolving when the alert is closed > > **Example** @@ -1216,9 +1278,11 @@ Web pages can be segmented into frames or IFrames, which are essentially documen > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L566) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise}` promise that resolves once the switch is complete > > **Example** @@ -1256,9 +1320,11 @@ They are used to verify that the application under test behaves as expected unde > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L478) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise}` - promise that resolves to a boolean indicating whether the element is present. > > **Example** @@ -1283,9 +1349,11 @@ They are used to verify that the application under test behaves as expected unde > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L487) > > #### Arguments +> > @param {string | object} rawLocator - Element locator > > #### Returns +> > @returns `{Promise}`- promise that resolves to a boolean indicating whether the element is present and visible. > > **Example** @@ -1310,6 +1378,7 @@ They are used to verify that the application under test behaves as expected unde > [source](https://github.com/MetaMask/metamask-extension/blob/671c9975424a83904a4752dfb8a7cf728ae67355/test/e2e/webdriver/driver.js#L303) > > #### Arguments +> > @param {string | object} rawLocator - Element locator
> @param {object} guards
> @param {string | object} [guards.findElementGuard] - A rawLocator to perform a findElement and act as a guard
@@ -1317,6 +1386,7 @@ They are used to verify that the application under test behaves as expected unde > @param {number} [guards.timeout] - The maximum milliseconds to wait before failing
> > #### Return +> > @returns `{Promise}` - promise resolving after the element is not present
> @throws {Error} - throws an error if the element is present > **Example** diff --git a/test/e2e/webdriver/chrome.js b/test/e2e/webdriver/chrome.js index 2e90eec7dd67..4a8d41afe9ec 100644 --- a/test/e2e/webdriver/chrome.js +++ b/test/e2e/webdriver/chrome.js @@ -72,6 +72,9 @@ class ChromeDriver { 'download.default_directory': `${process.cwd()}/test-artifacts/downloads`, }); + // Temporarily lock to version 126 + options.setBrowserVersion('126'); + // Allow disabling DoT local testing if (process.env.SELENIUM_USE_SYSTEM_DN) { options.setLocalState({ diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 9f5a048e3d69..876f7fc35e91 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -10,8 +10,8 @@ const { } = require('selenium-webdriver'); const cssToXPath = require('css-to-xpath'); const { sprintf } = require('sprintf-js'); -const { retry } = require('../../../development/lib/retry'); const { quoteXPathText } = require('../../helpers/quoteXPathText'); +const { WindowHandles } = require('../background-socket/window-handles'); const PAGES = { BACKGROUND: 'background', @@ -132,6 +132,8 @@ class Driver { this.timeout = timeout; this.exceptions = []; this.errors = []; + this.windowHandles = new WindowHandles(this.driver); + // The following values are found in // https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110 // These should be replaced with string constants 'Enter' etc for playwright. @@ -825,6 +827,7 @@ class Driver { */ async switchToWindow(handle) { await this.driver.switchTo().window(handle); + await this.windowHandles.getCurrentWindowProperties(null, handle); } /** @@ -853,7 +856,7 @@ class Driver { * be resolved with an array of window handles. */ async getAllWindowHandles() { - return await this.driver.getAllWindowHandles(); + return await this.windowHandles.getAllWindowHandles(); } /** @@ -866,10 +869,13 @@ class Driver { * @throws {Error} throws an error if the target number of window handles isn't met by the timeout. */ async waitUntilXWindowHandles(_x, delayStep = 1000, timeout = this.timeout) { + // In the MV3 build, there is an extra windowHandle with a title of "MetaMask Offscreen Page" + // So we add 1 to the expected number of window handles const x = process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined ? _x + 1 : _x; + let timeElapsed = 0; let windowHandles = []; while (timeElapsed <= timeout) { @@ -888,24 +894,29 @@ class Driver { } /** - * Retrieves the title of the window tab with the given handle ID. + * Switches to a specific window tab using its ID and waits for the title to match the expectedTitle. * - * @param {int} handlerId - unique ID for the tab whose title is needed. - * @param {number} retries - Number of times to retry fetching the title if not immediately available. - * @param {number} interval - Time in milliseconds to wait between retries. - * @returns {Promise} Promise resolving to the tab title after command completion. - * @throws {Error} Throws an error if the window title does not load within the specified retries. + * @param {int} handleId - unique ID for the tab whose title is needed. + * @param {string} expectedTitle - the title we are expecting. + * @returns nothing on success. + * @throws {Error} Throws an error if the window title is incorrect. */ - async getWindowTitleByHandlerId(handlerId, retries = 5, interval = 1000) { - await this.driver.switchTo().window(handlerId); - for (let attempt = 1; attempt <= retries; attempt++) { - const title = await this.driver.getTitle(); - if (title) { - return title; - } - await new Promise((resolve) => setTimeout(resolve, interval)); + async switchToHandleAndWaitForTitleToBe(handleId, expectedTitle) { + await this.driver.switchTo().window(handleId); + + let currentTitle = await this.driver.getTitle(); + + // Wait 25 x 200ms = 5 seconds for the title to be set properly + for (let i = 0; i < 25 && currentTitle !== expectedTitle; i++) { + await this.driver.sleep(200); + currentTitle = await this.driver.getTitle(); + } + + if (currentTitle !== expectedTitle) { + throw new Error( + `switchToHandleAndWaitForTitleToBe got title ${currentTitle} instead of ${expectedTitle}`, + ); } - throw new Error('Window title did not load within the specified retries'); } /** @@ -914,98 +925,38 @@ class Driver { * allowing for interaction with a particular window or tab based on its title * * @param {string} title - The title of the window or tab to switch to. - * @param {string[] | null} initialWindowHandles - optional array of window handles to search through. - * If not provided, the function fetches all current window handles. - * @param {int} delayStep - optional defaults to 1000 milliseconds - * @param {int} timeout - optional set to the defaults to 1000 milliseconds in the file - * @param {int} retries,retryDelay - optional for retrying the title fetch operation, ranging 8 ms to 2500 ms * @returns {Promise} promise that resolves once the switch is complete * @throws {Error} throws an error if no window with the specified title is found */ - async switchToWindowWithTitle( - title, - initialWindowHandles = null, - delayStep = 1000, - timeout = this.timeout, - { retries = 8, retryDelay = 2500 } = {}, - ) { - let windowHandles = - initialWindowHandles || (await this.driver.getAllWindowHandles()); - let timeElapsed = 0; - - while (timeElapsed <= timeout) { - for (const handle of windowHandles) { - const handleTitle = await retry( - { - retries, - delay: retryDelay, - }, - async () => { - await this.driver.switchTo().window(handle); - return await this.driver.getTitle(); - }, - ); - - if (handleTitle === title) { - return handle; - } - } - await this.delay(delayStep); - timeElapsed += delayStep; - // refresh the window handles - windowHandles = await this.driver.getAllWindowHandles(); - } - - throw new Error(`No window with title: ${title}`); + async switchToWindowWithTitle(title) { + return await this.windowHandles.switchToWindowWithProperty('title', title); } /** * Switches the context of the browser session to the window tab with the given URL. * - * @param {string} url - Window URL to switch - * @param {string} [initialWindowHandles] - optional array of window handles to search through. - * If not provided, the function fetches all current window handles. - * @param {int} delayStep - optional defaults to 1000 milliseconds - * @param {int} timeout - optional set to the defaults to 1000 milliseconds in the file - * @param {int} retries,retryDelay - optional for retrying the URL fetch operation, defaults to starting at 8 ms to 2500 ms + * @param {string} url - Window URL to find * @returns {Promise} promise that resolves once the switch is complete - * @throws {Error} throws an error if no window with the specified url is found + * @throws {Error} throws an error if no window with the specified URL is found */ - async switchToWindowWithUrl( - url, - initialWindowHandles, - delayStep = 1000, - timeout = this.timeout, - { retries = 8, retryDelay = 2500 } = {}, - ) { - let windowHandles = - initialWindowHandles || (await this.driver.getAllWindowHandles()); - let timeElapsed = 0; - - while (timeElapsed <= timeout) { - for (const handle of windowHandles) { - const handleUrl = await retry( - { - retries, - delay: retryDelay, - }, - async () => { - await this.driver.switchTo().window(handle); - return await this.driver.getCurrentUrl(); - }, - ); - - if (handleUrl === `${url}/`) { - return handle; - } - } - await this.delay(delayStep); - timeElapsed += delayStep; - // refresh the window handles - windowHandles = await this.driver.getAllWindowHandles(); - } + async switchToWindowWithUrl(url) { + return await this.windowHandles.switchToWindowWithProperty( + 'url', + new URL(url).toString(), // Make sure the URL has a trailing slash + ); + } - throw new Error(`No window with url: ${url}`); + /** + * If we already know this window, switch to it + * Otherwise, return null + * This is used in helpers.switchToOrOpenDapp() and when there's an alert open + * + * @param {string} title - The title of the window we want to switch to + * @returns {Promise} promise that resolves once the switch is complete + * @throws {Error} throws an error if no window with the specified URL is found + */ + async switchToWindowIfKnown(title) { + return await this.windowHandles.switchToWindowIfKnown(title); } /** @@ -1013,42 +964,12 @@ class Driver { * * @param {object} options - Parameters for the function. * @param {string} options.url - URL to wait for. - * @param {int} options.delayStep - optional delay between retries, defaults to 1000 milliseconds. * @param {int} options.timeout - optional timeout period, defaults to this.timeout. - * @param {int} options.retries - optional number of retries for the URL fetch operation, defaults to 8. - * @param {int} options.retryDelay - optional delay between retries for the URL fetch operation, defaults to 2500 milliseconds. * @returns {Promise} Promise that resolves once the URL matches. * @throws {Error} Throws an error if the URL does not match within the timeout period. */ - async waitForUrl({ - url, - delayStep = 1000, - timeout = this.timeout, - retries = 8, - retryDelay = 2500, - }) { - let timeElapsed = 0; - - while (timeElapsed <= timeout) { - const currentUrl = await retry( - { - retries, - delay: retryDelay, - }, - async () => { - return await this.driver.getCurrentUrl(); - }, - ); - - if (currentUrl === url) { - return; - } - - await this.delay(delayStep); - timeElapsed += delayStep; - } - - throw new Error(`URL did not match: ${url} within ${timeout} ms`); + async waitForUrl({ url, timeout = this.timeout }) { + return await this.driver.wait(until.urlIs(url), timeout); } /** @@ -1208,23 +1129,7 @@ class Driver { this.driver.onLogEvent(cdpConnection, (event) => { if (event.type === 'error') { - const eventDescriptions = event.args.filter( - (err) => err.description !== undefined, - ); - - if (eventDescriptions.length !== 0) { - // If we received an SES_UNHANDLED_REJECTION from Chrome, eventDescriptions.length will be nonzero - // Update: as of January 2024, this code path may never happen - const [eventDescription] = eventDescriptions; - const ignored = logBrowserError( - ignoredConsoleErrors, - eventDescription?.description, - ); - - if (!ignored && !ignoreAllErrors) { - this.errors.push(eventDescription?.description); - } - } else if (event.args.length !== 0) { + if (event.args.length !== 0) { const newError = this.#getErrorFromEvent(event); const ignored = logBrowserError(ignoredConsoleErrors, newError); @@ -1241,7 +1146,7 @@ class Driver { // Extract the values from the array const values = event.args.map((a) => a.value); - if (values[0].includes('%s')) { + if (values[0]?.includes('%s')) { // The values are in the "printf" form of [message, ...substitutions] // so use sprintf to parse return sprintf(...values); diff --git a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.js.snap b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.js.snap index 139f516bdc95..a3ddde203929 100644 --- a/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.js.snap +++ b/ui/components/app/assets/token-cell/__snapshots__/token-cell.test.js.snap @@ -12,19 +12,14 @@ exports[`Token Cell should match snapshot 1`] = ` href="#" >
- TEST logo
diff --git a/ui/components/app/import-token/token-list/token-list.component.js b/ui/components/app/import-token/token-list/token-list.component.js index 5c8c0a07d070..bb472a703ba0 100644 --- a/ui/components/app/import-token/token-list/token-list.component.js +++ b/ui/components/app/import-token/token-list/token-list.component.js @@ -108,7 +108,6 @@ export default class TokenList extends Component { diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index 15893fa4d204..8f7441f72530 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import thunk from 'redux-thunk'; import { Cryptocurrency } from '@metamask/assets-controllers'; import { BtcAccountType, BtcMethod } from '@metamask/keyring-api'; @@ -208,6 +208,7 @@ describe('BtcOverview', () => { metamaskEntry: RampsMetaMaskEntry.BuySellButton, chainId: MultichainNetworks.BITCOIN, metametricsId: mockMetaMetricsId, + metricsEnabled: String(false), }), }); }); @@ -227,11 +228,12 @@ describe('BtcOverview', () => { fireEvent.click(portfolioButton as HTMLElement); expect(openTabSpy).toHaveBeenCalledTimes(1); - expect(openTabSpy).toHaveBeenCalledWith({ - url: makePortfolioUrl('', { - metamaskEntry: 'ext_portfolio_button', - metametricsId: mockMetaMetricsId, + await waitFor(() => + expect(openTabSpy).toHaveBeenCalledWith({ + url: expect.stringContaining( + `?metamaskEntry=ext_portfolio_button&metametricsId=${mockMetaMetricsId}`, + ), }), - }); + ); }); }); diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index 5d710b83adfb..8ecb9ec60e4b 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -36,7 +36,9 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) SwapsEthToken, getCurrentKeyring, + getDataCollectionForMarketing, getMetaMetricsId, + getParticipateInMetaMetrics, ///: END:ONLY_INCLUDE_IF getUseExternalServices, } from '../../../selectors'; @@ -97,6 +99,8 @@ const CoinButtons = ({ ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const location = useLocation(); const metaMetricsId = useSelector(getMetaMetricsId); + const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); + const isMarketingEnabled = useSelector(getDataCollectionForMarketing); const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = isHardwareKeyring(keyring?.type); ///: END:ONLY_INCLUDE_IF @@ -287,6 +291,8 @@ const CoinButtons = ({ 'bridge', 'ext_bridge_button', metaMetricsId, + isMetaMetricsEnabled, + isMarketingEnabled, ); global.platform.openTab({ url: `${portfolioUrl}${ @@ -307,7 +313,13 @@ const CoinButtons = ({ }, [isBridgeChain, chainId, metaMetricsId]); const handlePortfolioOnClick = useCallback(() => { - const url = getPortfolioUrl('', 'ext_portfolio_button', metaMetricsId); + const url = getPortfolioUrl( + '', + 'ext_portfolio_button', + metaMetricsId, + isMetaMetricsEnabled, + isMarketingEnabled, + ); global.platform.openTab({ url }); trackEvent({ category: MetaMetricsEventCategory.Navigation, diff --git a/ui/components/institutional/confirm-remove-jwt-modal/__snapshots__/confirm-remove-jwt-modal.test.js.snap b/ui/components/institutional/confirm-remove-jwt-modal/__snapshots__/confirm-remove-jwt-modal.test.tsx.snap similarity index 100% rename from ui/components/institutional/confirm-remove-jwt-modal/__snapshots__/confirm-remove-jwt-modal.test.js.snap rename to ui/components/institutional/confirm-remove-jwt-modal/__snapshots__/confirm-remove-jwt-modal.test.tsx.snap diff --git a/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.js b/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.tsx similarity index 75% rename from ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.js rename to ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.tsx index 440541fcb150..e2331752f273 100644 --- a/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.js +++ b/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.stories.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; import ConfirmRemoveJWT from '.'; export default { @@ -24,8 +25,11 @@ export default { { address: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', balance: '0x0' }, ], }, -}; +} as Meta; -export const DefaultStory = (args) => ; +const Template: StoryFn = (args) => ( + +); +export const DefaultStory = Template.bind({}); DefaultStory.storyName = 'ConfirmRemoveJWT'; diff --git a/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.test.js b/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.test.tsx similarity index 100% rename from ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.test.js rename to ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.test.tsx diff --git a/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.js b/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.tsx similarity index 79% rename from ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.js rename to ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.tsx index 9ec21650c310..f8c97c98cff2 100644 --- a/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.js +++ b/ui/components/institutional/confirm-remove-jwt-modal/confirm-remove-jwt-modal.tsx @@ -1,5 +1,4 @@ import React, { memo, useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import CustodyAccountList from '../../../pages/institutional/connect-custody/account-list'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -14,8 +13,8 @@ import { ModalBody, ModalOverlay, Button, - BUTTON_VARIANT, - BUTTON_SIZES, + ButtonVariant, + ButtonSize, } from '../../component-library'; import { BorderRadius, @@ -25,7 +24,40 @@ import { TextVariant, } from '../../../helpers/constants/design-system'; -const ConfirmRemoveJWT = ({ +type AuthDetails = { + token?: string; + jwt?: string; + refreshToken?: string; +}; + +type CustodyAccountDetail = { + address: string; + name: string; + labels: string[]; + authDetails: AuthDetails; +}; + +type Account = { + address: string; + balance: string; +}; + +type TokenAccount = { + address: string; + name: string; + labels: string[]; + balance?: string; + token?: string; +}; + +type ConfirmRemoveJWTProps = { + hideModal: () => void; + token: string | { address: string }; + custodyAccountDetails: CustodyAccountDetail[]; + accounts: Account[]; +}; + +const ConfirmRemoveJWT: React.FC = ({ custodyAccountDetails, accounts, token: propsToken, @@ -34,8 +66,8 @@ const ConfirmRemoveJWT = ({ const t = useI18nContext(); const dispatch = useDispatch(); const [showMore, setShowMore] = useState(false); - const [tokenAccounts, setTokenAccounts] = useState([]); - let token = null; + const [tokenAccounts, setTokenAccounts] = useState([]); + let token: string | null = null; if (propsToken) { if (typeof propsToken === 'object') { @@ -46,7 +78,7 @@ const ConfirmRemoveJWT = ({ } useEffect(() => { - const lowercasedTokenAddress = token.toLowerCase(); + const lowercasedTokenAddress = token?.toLowerCase() || ''; const filteredAccounts = custodyAccountDetails.filter((item) => { const addressLower = item.address.toLowerCase(); @@ -102,12 +134,12 @@ const ConfirmRemoveJWT = ({ className="confirm-action-jwt__jwt" > - {showMore && token ? token : `...${token.slice(-9)}`} + {showMore && token ? token : `...${token?.slice(-9)}`} {!showMore && ( @@ -131,8 +163,8 @@ const ConfirmRemoveJWT = ({ - - - - ); -}; - -export default InteractiveReplacementTokenModal; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx similarity index 100% rename from ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js rename to ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.tsx diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx similarity index 95% rename from ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js rename to ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx index 276cc3d987b1..6d71c4681de6 100644 --- a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.tsx @@ -60,6 +60,11 @@ describe('Interactive Replacement Token Modal', function () { const store = configureMockStore()(mockStore); it('should render the interactive-replacement-token-modal', () => { + global.platform = { + closeCurrentWindow: jest.fn(), + openTab: jest.fn(), + }; + const { getByText, getByTestId } = renderWithProvider( , store, diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx new file mode 100644 index 000000000000..4526595fa455 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.tsx @@ -0,0 +1,155 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { ICustodianType } from '@metamask-institutional/types'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { hideModal } from '../../../store/actions'; +import { getSelectedInternalAccount } from '../../../selectors/selectors'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { + Box, + Button, + Modal, + ModalOverlay, + Text, + ModalHeader, + ModalContent, +} from '../../component-library'; + +import { + BlockSize, + BackgroundColor, + Display, + FlexWrap, + FlexDirection, + BorderRadius, + FontWeight, + TextAlign, + AlignItems, +} from '../../../helpers/constants/design-system'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +type State = { + metamask: { + interactiveReplacementToken?: { url: string }; + mmiConfiguration: { custodians: ICustodianType[] }; + custodyAccountDetails: { [address: string]: { custodianName?: string } }; + }; +}; + +const InteractiveReplacementTokenModal: React.FC = () => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + + const url = useSelector( + (state: State) => state.metamask.interactiveReplacementToken?.url, + ); + const custodians = useSelector( + (state: State) => state.metamask.mmiConfiguration.custodians, + ); + const address = useSelector(getSelectedInternalAccount)?.address || ''; + const custodyAccountDetails = useSelector( + (state: State) => + state.metamask.custodyAccountDetails[toChecksumHexAddress(address)], + ); + + const custodianName = custodyAccountDetails?.custodianName; + const custodian = custodians.find( + (item) => item.envName === custodianName, + ) || { displayName: '', iconUrl: null }; + + const handleSubmit = () => { + if (url) { + global.platform.openTab({ url }); + + trackEvent({ + category: MetaMetricsEventCategory.MMI, + event: MetaMetricsEventName.InteractiveReplacementTokenButtonClicked, + }); + } + }; + + const handleClose = () => { + dispatch(hideModal()); + }; + + return ( + + + + + + {t('custodyRefreshTokenModalTitle')} + + { + // @ts-expect-error: todo: Merge MetaMask Institutional PR 778 to fix this + custodian.iconUrl ? ( + + + {custodian.displayName} + + + ) : ( + + {custodian.displayName} + + ) + } + + + {t('custodyRefreshTokenModalDescription', [ + custodian.displayName, + ])} + + + {t('custodyRefreshTokenModalSubtitle')} + + + {t('custodyRefreshTokenModalDescription1')} + + + {t('custodyRefreshTokenModalDescription2')} + + + + + + + ); +}; + +export default InteractiveReplacementTokenModal; diff --git a/ui/components/institutional/interactive-replacement-token-notification/index.js b/ui/components/institutional/interactive-replacement-token-notification/index.ts similarity index 100% rename from ui/components/institutional/interactive-replacement-token-notification/index.js rename to ui/components/institutional/interactive-replacement-token-notification/index.ts diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.js b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx similarity index 54% rename from ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.js rename to ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx index 363a9294528a..a92ebadc04e5 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.js +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.stories.tsx @@ -26,6 +26,33 @@ const customData = { accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281'], }, ], + internalAccounts: { + accounts: { + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Test Account', + keyring: { + type: 'Custody - Saturn', + }, + }, + options: {}, + methods: [], + type: 'EOA', + }, + }, + selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + }, + custodyAccountDetails: { + '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281': { + address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', + authDetails: { + refreshToken: 'def', + }, + custodianName: 'saturn-dev', + }, + }, }, }; @@ -45,8 +72,12 @@ export default { }, }; -export const DefaultStory = (args) => ( - -); +type InteractiveReplacementTokenNotificationArgs = { + isVisible?: boolean; +}; + +export const DefaultStory = ( + args: InteractiveReplacementTokenNotificationArgs, +) => ; DefaultStory.storyName = 'InteractiveReplacementTokenNotification'; diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.js b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx similarity index 93% rename from ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.js rename to ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx index a4f45acbc831..a6d2a417ea03 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.js +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.test.tsx @@ -4,13 +4,15 @@ import thunk from 'redux-thunk'; import { screen, fireEvent } from '@testing-library/react'; import { EthAccountType } from '@metamask/keyring-api'; import { act } from 'react-dom/test-utils'; -import { sha256 } from '../../../../shared/modules/hash.utils'; +import * as hashUtils from '../../../../shared/modules/hash.utils'; import { KeyringType } from '../../../../shared/constants/keyring'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import InteractiveReplacementTokenNotification from './interactive-replacement-token-notification'; -jest.mock('../../../../shared/modules/hash.utils'); +jest.mock('../../../../shared/modules/hash.utils', () => ({ + sha256: jest.fn(), +})); const mockedGetCustodianToken = jest .fn() @@ -40,6 +42,12 @@ jest.mock('../../../store/institutional/institution-actions', () => ({ })); describe('Interactive Replacement Token Notification', () => { + beforeEach(() => { + ( + hashUtils.sha256 as jest.MockedFunction + ).mockImplementation(() => Promise.resolve('def')); + }); + const selectedAccount = 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3'; const mockStore = { @@ -110,7 +118,6 @@ describe('Interactive Replacement Token Notification', () => { }; const store = configureMockStore([thunk])(customMockStore); - sha256.mockReturnValue('def'); await act(async () => { renderWithProvider(, store); }); @@ -147,7 +154,6 @@ describe('Interactive Replacement Token Notification', () => { const store = configureMockStore([thunk])(customMockStore); - sha256.mockReturnValue('def'); await act(async () => { renderWithProvider(, store); }); diff --git a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.js b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx similarity index 87% rename from ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.js rename to ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx index d8edd4fa9392..30b72fc2b882 100644 --- a/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.js +++ b/ui/components/institutional/interactive-replacement-token-notification/interactive-replacement-token-notification.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; import { getCurrentKeyring, getSelectedInternalAccount, @@ -12,7 +11,6 @@ import { mmiActionsFactory } from '../../../store/institutional/institution-back import { showInteractiveReplacementTokenModal } from '../../../store/institutional/institution-actions'; import { sha256 } from '../../../../shared/modules/hash.utils'; import { - Size, IconColor, AlignItems, Display, @@ -29,9 +27,16 @@ import { ButtonLink, Box, Text, + ButtonLinkSize, } from '../../component-library'; -const InteractiveReplacementTokenNotification = ({ isVisible }) => { +type InteractiveReplacementTokenNotificationProps = { + isVisible?: boolean; +}; + +const InteractiveReplacementTokenNotification: React.FC< + InteractiveReplacementTokenNotificationProps +> = ({ isVisible }) => { const t = useI18nContext(); const dispatch = useDispatch(); const mmiActions = mmiActionsFactory(); @@ -56,7 +61,9 @@ const InteractiveReplacementTokenNotification = ({ isVisible }) => { return; } - const token = await dispatch(mmiActions.getCustodianToken(address)); + const token = (await dispatch( + mmiActions.getCustodianToken(address), + )) as unknown as string; const custodyAccountDetails = await dispatch( mmiActions.getAllCustodianAccountsWithToken( keyring.type.split(' - ')[1], @@ -93,8 +100,15 @@ const InteractiveReplacementTokenNotification = ({ isVisible }) => { }; handleShowNotification(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [address, interactiveReplacementToken.oldRefreshToken, isUnlocked]); + }, [ + address, + interactiveReplacementToken.oldRefreshToken, + isUnlocked, + dispatch, + keyring.type, + interactiveReplacementToken, + mmiActions, + ]); return showNotification ? ( { { dispatch(showInteractiveReplacementTokenModal()); @@ -132,7 +146,3 @@ const InteractiveReplacementTokenNotification = ({ isVisible }) => { }; export default InteractiveReplacementTokenNotification; - -InteractiveReplacementTokenNotification.propTypes = { - isVisible: PropTypes.bool, -}; diff --git a/ui/components/institutional/jwt-dropdown/__snapshots__/jwt-dropdown.test.js.snap b/ui/components/institutional/jwt-dropdown/__snapshots__/jwt-dropdown.test.tsx.snap similarity index 100% rename from ui/components/institutional/jwt-dropdown/__snapshots__/jwt-dropdown.test.js.snap rename to ui/components/institutional/jwt-dropdown/__snapshots__/jwt-dropdown.test.tsx.snap diff --git a/ui/components/institutional/jwt-dropdown/index.js b/ui/components/institutional/jwt-dropdown/index.ts similarity index 100% rename from ui/components/institutional/jwt-dropdown/index.js rename to ui/components/institutional/jwt-dropdown/index.ts diff --git a/ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.js b/ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.tsx similarity index 65% rename from ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.js rename to ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.tsx index e691dc602e9e..f1cb13d2f9b6 100644 --- a/ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.js +++ b/ui/components/institutional/jwt-dropdown/jwt-dropdown.stories.tsx @@ -14,6 +14,14 @@ export default { }, }; -export const DefaultStory = (args) => ; +type JwtDropdownArgs = { + jwtList: string[]; + currentJwt: string; + onChange: (value: string) => void; +}; + +export const DefaultStory = (args: JwtDropdownArgs) => ( + +); DefaultStory.storyName = 'JwtDropdown'; diff --git a/ui/components/institutional/jwt-dropdown/jwt-dropdown.test.js b/ui/components/institutional/jwt-dropdown/jwt-dropdown.test.tsx similarity index 91% rename from ui/components/institutional/jwt-dropdown/jwt-dropdown.test.js rename to ui/components/institutional/jwt-dropdown/jwt-dropdown.test.tsx index 58fdc2aa583c..18613de6fbf6 100644 --- a/ui/components/institutional/jwt-dropdown/jwt-dropdown.test.js +++ b/ui/components/institutional/jwt-dropdown/jwt-dropdown.test.tsx @@ -1,6 +1,5 @@ import { render, fireEvent } from '@testing-library/react'; import React from 'react'; -import sinon from 'sinon'; import JwtDropdown from './jwt-dropdown'; describe('JwtDropdown', () => { @@ -8,7 +7,7 @@ describe('JwtDropdown', () => { const props = { jwtList: ['jwy1', 'jwt2'], currentJwt: 'someToken', - onChange: sinon.spy(), + onChange: jest.fn(), }; const { getByTestId, container } = render(); diff --git a/ui/components/institutional/jwt-dropdown/jwt-dropdown.js b/ui/components/institutional/jwt-dropdown/jwt-dropdown.tsx similarity index 73% rename from ui/components/institutional/jwt-dropdown/jwt-dropdown.js rename to ui/components/institutional/jwt-dropdown/jwt-dropdown.tsx index 118f522e71a9..58c9cd83f466 100644 --- a/ui/components/institutional/jwt-dropdown/jwt-dropdown.js +++ b/ui/components/institutional/jwt-dropdown/jwt-dropdown.tsx @@ -1,19 +1,27 @@ import React from 'react'; -import PropTypes from 'prop-types'; import Dropdown from '../../ui/dropdown'; -import { Color } from '../../../helpers/constants/design-system'; +import { TextColor } from '../../../helpers/constants/design-system'; import { Box, Text } from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; -const JwtDropdown = (props) => { +type JwtDropdownProps = { + jwtList: string[]; + currentJwt: string; + onChange: (value: string) => void; +}; + +const JwtDropdown: React.FC = ({ + currentJwt, + jwtList, + onChange, +}) => { const t = useI18nContext(); - const { currentJwt, jwtList } = props; return ( {t('selectJWT')} @@ -21,7 +29,6 @@ const JwtDropdown = (props) => { item === currentJwt) @@ -42,16 +49,10 @@ const JwtDropdown = (props) => { }; }), ]} - onChange={(opt) => props.onChange(opt.value)} + onChange={(opt) => onChange(opt.value)} /> ); }; -JwtDropdown.propTypes = { - jwtList: PropTypes.array, - currentJwt: PropTypes.string, - onChange: PropTypes.func, -}; - export default JwtDropdown; diff --git a/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap b/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.tsx.snap similarity index 53% rename from ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap rename to ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.tsx.snap index dcc3a387b0c0..b27982b28a4f 100644 --- a/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap +++ b/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.tsx.snap @@ -16,13 +16,16 @@ exports[`JwtUrlForm shows JWT text area when no jwt token exists 1`] = ` > input text

-