diff --git a/commands/metamask.js b/commands/metamask.js index 15f76e749..3b796c48f 100644 --- a/commands/metamask.js +++ b/commands/metamask.js @@ -1,5 +1,6 @@ const log = require('debug')('synpress:metamask'); const playwright = require('./playwright'); +const sleep = require('util').promisify(setTimeout); const { onboardingWelcomePageElements, @@ -380,7 +381,9 @@ const metamask = { mainPageElements.createAccount.createAccountError, ); const formErrorTxt = await formErrorEl.innerText(); - const accountExists = 'This account name already exists' === formErrorTxt; + const accountExists = + 'This account name already exists' === formErrorTxt || + 'This account name is reserved' === formErrorTxt; if (accountExists) { log(`[createAccount] ${formErrorTxt}`); @@ -397,6 +400,40 @@ const metamask = { await switchToCypressIfNotActive(); return accountExists ? formErrorTxt : true; }, + async renameAccount(newAccountName) { + await switchToMetamaskIfNotActive(); + + await playwright.waitAndClick(mainPageElements.optionsMenu.button); + await playwright.waitAndClick( + mainPageElements.optionsMenu.accountDetailsButton, + ); + + await playwright.waitAndClick(mainPageElements.renameAccount.invokeInput); + await playwright.waitClearAndType( + newAccountName, + mainPageElements.renameAccount.input, + ); + + const formErrorEl = await playwright.waitFor( + mainPageElements.renameAccount.error, + ); + const formErrorTxt = await formErrorEl.innerText(); + const accountExists = + 'This account name already exists' === formErrorTxt || + 'This account name is reserved' === formErrorTxt; + + if (accountExists) { + log(`[createAccount] ${formErrorTxt}`); + } else { + await playwright.waitAndClick( + mainPageElements.renameAccount.confirmButton, + ); + } + + await playwright.waitAndClick(mainPageElements.accountModal.closeButton); + await switchToCypressIfNotActive(); + return accountExists ? formErrorTxt : true; + }, async switchAccount(accountNameOrAccountNumber) { if (typeof accountNameOrAccountNumber === 'string') { accountNameOrAccountNumber = accountNameOrAccountNumber.toLowerCase(); @@ -1052,18 +1089,34 @@ const metamask = { .count()) > 0 ) { log('[confirmTransaction] Getting recipient address..'); - await playwright.waitAndClick( - confirmPageElements.recipientButton, - notificationPage, - ); - txData.recipientPublicAddress = await playwright.waitAndGetValue( - recipientPopupElements.recipientPublicAddress, - notificationPage, - ); - await playwright.waitAndClick( - recipientPopupElements.popupCloseButton, + + const tooltip = await playwright.waitAndGetAttributeValue( + confirmPageElements.recipientAddressTooltipContainerButton, + 'aria-describedby', notificationPage, + true, ); + + // Handles the case where the recipient address is saved and has a "nickname". + if (tooltip === 'tippy-tooltip-2') { + txData.recipientPublicAddress = await playwright.waitAndGetValue( + confirmPageElements.recipientButton, + notificationPage, + ); + } else { + await playwright.waitAndClick( + confirmPageElements.recipientButton, + notificationPage, + ); + txData.recipientPublicAddress = await playwright.waitAndGetValue( + recipientPopupElements.recipientPublicAddress, + notificationPage, + ); + await playwright.waitAndClick( + recipientPopupElements.popupCloseButton, + notificationPage, + ); + } } log('[confirmTransaction] Checking if network name is present..'); if ( @@ -1128,6 +1181,70 @@ const metamask = { log('[confirmTransaction] Transaction confirmed!'); return txData; }, + async confirmTransactionAndWaitForMining(gasConfig) { + // Before we switch to MetaMask tab we have to make sure the notification window has opened. + // + // Chaining `confirmTransactionAndWaitForMining` results in quick tabs switching + // which breaks MetaMask and the notification window does not open + // until we switch back to the "Cypress" tab. + await playwright.switchToMetamaskNotification(); + + await switchToMetamaskIfNotActive(); + await playwright + .metamaskWindow() + .locator(mainPageElements.tabs.activityButton) + .click(); + + let retries = 0; + const retiresLimit = 600; + + // 120 seconds + while (retries < retiresLimit) { + const unapprovedTxs = await playwright + .metamaskWindow() + .getByText('Unapproved') + .count(); + if (unapprovedTxs === 1) { + break; + } + await sleep(200); + retries++; + } + + if (retries === retiresLimit) { + throw new Error( + 'New unapproved transaction was not detected in 120 seconds.', + ); + } + + const txData = await module.exports.confirmTransaction(gasConfig); + + // 120 seconds + while (retries < retiresLimit) { + const pendingTxs = await playwright + .metamaskWindow() + .getByText('Pending') + .count(); + const queuedTxs = await playwright + .metamaskWindow() + .getByText('Queued') + .count(); + if (pendingTxs === 0 && queuedTxs === 0) { + break; + } + await sleep(200); + retries++; + } + + if (retries === retiresLimit) { + throw new Error('Transaction was not mined in 120 seconds.'); + } + + await switchToCypressIfNotActive(); + + log('[confirmTransactionAndWaitForMining] Transaction confirmed!'); + return txData; + }, async rejectTransaction() { const notificationPage = await playwright.switchToMetamaskNotification(); await playwright.waitAndClick( @@ -1137,6 +1254,64 @@ const metamask = { ); return true; }, + async openTransactionDetails(txIndex) { + await switchToMetamaskIfNotActive(); + await playwright + .metamaskWindow() + .locator(mainPageElements.tabs.activityButton) + .click(); + + let visibleTxs = await playwright + .metamaskWindow() + .locator( + `${mainPageElements.activityTab.completedTransactionsList} > div`, + ) + .filter({ + has: playwright.metamaskWindow().locator('div.list-item__heading'), + }) + .all(); + + while (txIndex >= visibleTxs.length) { + try { + await playwright + .metamaskWindow() + .locator( + `${mainPageElements.activityTab.completedTransactionsList} > button`, + ) + .click(); + } catch (error) { + log('[openTransactionDetails] Clicking "View more" failed!'); + throw new Error( + `Transaction with index ${txIndex} is not found. There are only ${visibleTxs.length} transactions.`, + ); + } + + visibleTxs = await playwright + .metamaskWindow() + .locator( + `${mainPageElements.activityTab.completedTransactionsList} > div`, + ) + .filter({ + has: playwright.metamaskWindow().locator('div.list-item__heading'), + }) + .all(); + } + + await visibleTxs[txIndex].click(); + + await playwright + .metamaskWindow() + .locator(mainPageElements.popup.container) + .waitFor({ state: 'visible', timeout: 10000 }); + + return true; + }, + async closeTransactionDetailsPopup() { + await switchToMetamaskIfNotActive(); + await module.exports.closePopupAndTooltips(); + await switchToCypressIfNotActive(); + return true; + }, async confirmEncryptionPublicKeyRequest() { const notificationPage = await playwright.switchToMetamaskNotification(); await playwright.waitAndClick( diff --git a/commands/playwright.js b/commands/playwright.js index f895d7a1d..a474e8b5f 100644 --- a/commands/playwright.js +++ b/commands/playwright.js @@ -275,12 +275,19 @@ module.exports = { const value = await element.inputValue(); return value; }, - async waitAndGetAttributeValue(selector, attribute, page = metamaskWindow) { + async waitAndGetAttributeValue( + selector, + attribute, + page = metamaskWindow, + skipValidation = false, + ) { const expect = expectInstance ? expectInstance : require('@playwright/test').expect; const element = await module.exports.waitFor(selector, page); - await expect(element).toHaveAttribute(attribute, /[a-zA-Z0-9]/); + if (!skipValidation) { + await expect(element).toHaveAttribute(attribute, /[a-zA-Z0-9]/); + } const attrValue = await element.getAttribute(attribute); return attrValue; }, diff --git a/docs/synpress-commands.md b/docs/synpress-commands.md index 9fefccda5..408d800aa 100644 --- a/docs/synpress-commands.md +++ b/docs/synpress-commands.md @@ -104,6 +104,14 @@ Create new account in metamask. createMetamaskAccount(accountName?: string): Chainable; ``` +#### `cy.renameMetamaskAccount()` + +Rename current account in metamask. + +```ts +createMetamaskAccount(newAccountName: string): Chainable; +``` + #### `cy.switchMetamaskAccount()` Switch metamask account. @@ -360,6 +368,14 @@ Confirm metamask transaction (auto-detects eip-1559 and legacy transactions). confirmMetamaskTransaction(gasConfig?: object | string): Chainable; ``` +#### `cy.confirmMetamaskTransactionAndWaitForMining()` + +Confirm metamask transaction (auto-detects eip-1559 and legacy transactions) and wait for ALL pending transactions to be mined. + +```ts +confirmMetamaskTransactionAndWaitForMining(gasConfig?: object | string): Chainable; +``` + #### `cy.rejectMetamaskTransaction()` Reject metamask transaction. @@ -368,6 +384,22 @@ Reject metamask transaction. rejectMetamaskTransaction(): Chainable; ``` +#### `cy.openMetamaskTransactionDetails()` + +Open metamask transaction details based on the index of the transaction in the list on the activity tab. + +```ts +openMetamaskTransactionDetails(txIndex: number): Chainable; +``` + +#### `cy.closeMetamaskTransactionDetailsPopup()` + +Close currently open transaction details popup. + +```ts +closeMetamaskTransactionDetailsPopup(): Chainable; +``` + #### `cy.allowMetamaskToAddNetwork()` Allow site to add new network in metamask. diff --git a/package.json b/package.json index 09c3f8b1f..df0ac85d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@synthetixio/synpress", - "version": "3.7.2-beta.3", + "version": "3.7.2-beta.6", "packageManager": "pnpm@8.4.0", "description": "Synpress is e2e testing framework based around Cypress.io & playwright with included MetaMask support. Test your dapps with ease.", "keywords": [ @@ -69,7 +69,7 @@ "babel-plugin-transform-react-styled-components-qa": "^2.1.0", "bytes32": "^0.0.3", "commander": "^11.0.0", - "cypress": "^12.17.4", + "cypress": "12.17.3", "cypress-wait-until": "^2.0.1", "debug": "^4.3.4", "dotenv": "^16.3.1", diff --git a/pages/metamask/main-page.js b/pages/metamask/main-page.js index c97c587c4..06afa6c72 100644 --- a/pages/metamask/main-page.js +++ b/pages/metamask/main-page.js @@ -24,10 +24,13 @@ const tabs = { const transactionList = '.transaction-list__transactions'; const pendingTransactionsList = `${transactionList} .transaction-list__pending-transactions`; const completedTransactionsList = `${transactionList} .transaction-list__completed-transactions`; +const completedTransaction = txIndex => + `${completedTransactionsList} > div:nth-child(${txIndex + 1})`; const activityTab = { transactionList, pendingTransactionsList, completedTransactionsList, + completedTransaction, unconfirmedTransaction: `${pendingTransactionsList} .transaction-list-item--unconfirmed`, confirmedTransaction: `${completedTransactionsList} .transaction-list-item`, }; @@ -84,11 +87,19 @@ const connectedSites = { closeButton: `${connectedSitesSelector} [data-testid="popover-close"]`, }; +const accountModalSelector = '.account-modal'; const accountModal = { - walletAddressInput: '.account-modal .qr-code__address', + walletAddressInput: `${accountModalSelector} .qr-code__address`, closeButton: '.account-modal__close', }; +const renameAccount = { + invokeInput: `${accountModalSelector} [data-testid="editable-label-button"]`, + input: `${accountModalSelector} [data-testid="editable-input"]`, + confirmButton: `${accountModalSelector} .editable-label__icon-button`, + error: `${accountModalSelector} .editable-label__error`, +}; + const importAccountSelector = '.new-account'; const importAccount = { page: importAccountSelector, @@ -135,6 +146,7 @@ module.exports.mainPageElements = { optionsMenu, connectedSites, accountModal, + renameAccount, importAccount, createAccount, importToken, diff --git a/pages/metamask/notification-page.js b/pages/metamask/notification-page.js index f848564fb..ccac8014c 100644 --- a/pages/metamask/notification-page.js +++ b/pages/metamask/notification-page.js @@ -66,6 +66,7 @@ const confirmPageContent = `${notificationPage} .confirm-page-container-content` const networkLabel = `${confirmPageHeader} .network-display`; const senderButton = `${confirmPageHeader} .sender-to-recipient__party--sender`; const recipientButton = `${confirmPageHeader} .sender-to-recipient__party--recipient-with-address`; +const recipientAddressTooltipContainerButton = `${confirmPageHeader} .sender-to-recipient__party--recipient .sender-to-recipient__tooltip-container`; const editGasFeeLegacyButton = `${notificationPage} .transaction-detail-edit button`; const editGasFeeLegacyOverrideAckButton = `${notificationPage} .edit-gas-display .edit-gas-display__dapp-acknowledgement-button`; const editGasLegacyPopup = `${notificationPage} .edit-gas-popover__wrapper`; @@ -102,6 +103,7 @@ module.exports.confirmPageElements = { networkLabel, senderButton, recipientButton, + recipientAddressTooltipContainerButton, editGasFeeLegacyButton, editGasFeeLegacyOverrideAckButton, editGasLegacyPopup, diff --git a/plugins/index.js b/plugins/index.js index 7933258d5..f8e386ea6 100644 --- a/plugins/index.js +++ b/plugins/index.js @@ -72,6 +72,7 @@ module.exports = (on, config) => { unlockMetamask: metamask.unlock, importMetamaskAccount: metamask.importAccount, createMetamaskAccount: metamask.createAccount, + renameMetamaskAccount: metamask.renameAccount, switchMetamaskAccount: metamask.switchAccount, addMetamaskNetwork: metamask.addNetwork, changeMetamaskNetwork: async network => { @@ -115,7 +116,11 @@ module.exports = (on, config) => { acceptMetamaskAccess: metamask.acceptAccess, rejectMetamaskAccess: metamask.rejectAccess, confirmMetamaskTransaction: metamask.confirmTransaction, + confirmMetamaskTransactionAndWaitForMining: + metamask.confirmTransactionAndWaitForMining, rejectMetamaskTransaction: metamask.rejectTransaction, + openMetamaskTransactionDetails: metamask.openTransactionDetails, + closeMetamaskTransactionDetailsPopup: metamask.closeTransactionDetailsPopup, allowMetamaskToAddNetwork: async ({ waitForEvent }) => await metamask.allowToAddNetwork({ waitForEvent }), rejectMetamaskToAddNetwork: metamask.rejectToAddNetwork, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c61e32e8..b9bfa0f14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: dependencies: '@cypress/code-coverage': specifier: ^3.11.0 - version: 3.11.0(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(cypress@12.17.4)(webpack@5.81.0) + version: 3.11.0(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(cypress@12.17.3)(webpack@5.81.0) '@cypress/webpack-dev-server': specifier: ^3.5.2 version: 3.5.2(debug@4.3.4)(webpack@5.81.0) @@ -28,7 +28,7 @@ dependencies: version: 1.37.0 '@testing-library/cypress': specifier: ^9.0.0 - version: 9.0.0(cypress@12.17.4) + version: 9.0.0(cypress@12.17.3) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) @@ -66,8 +66,8 @@ dependencies: specifier: ^11.0.0 version: 11.0.0 cypress: - specifier: ^12.17.4 - version: 12.17.4 + specifier: 12.17.3 + version: 12.17.3 cypress-wait-until: specifier: ^2.0.1 version: 2.0.1 @@ -513,6 +513,7 @@ packages: /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.8): resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -528,6 +529,7 @@ packages: /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -541,6 +543,7 @@ packages: /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. peerDependencies: '@babel/core': ^7.12.0 dependencies: @@ -555,6 +558,7 @@ packages: /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -566,6 +570,7 @@ packages: /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.8): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -577,6 +582,7 @@ packages: /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -588,6 +594,7 @@ packages: /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.8): resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -599,6 +606,7 @@ packages: /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -610,6 +618,7 @@ packages: /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -621,6 +630,7 @@ packages: /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.8): resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -635,6 +645,7 @@ packages: /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -646,6 +657,7 @@ packages: /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -658,6 +670,7 @@ packages: /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -671,6 +684,7 @@ packages: /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -686,6 +700,7 @@ packages: /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 dependencies: @@ -1356,14 +1371,14 @@ packages: requiresBuild: true optional: true - /@cypress/code-coverage@3.11.0(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(cypress@12.17.4)(webpack@5.81.0): + /@cypress/code-coverage@3.11.0(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(cypress@12.17.3)(webpack@5.81.0): resolution: {integrity: sha512-ihSO1s03gmLRE224oIjrbdG1ey63vw/UY+VSqQ5m/TKkAvyz6GIiniq6juk3AV/+0vQC1Eb4UWFu8ndtji4M1g==} peerDependencies: cypress: '*' dependencies: '@cypress/webpack-preprocessor': 5.17.1(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(webpack@5.81.0) chalk: 4.1.2 - cypress: 12.17.4 + cypress: 12.17.3 dayjs: 1.11.9 debug: 4.3.4(supports-color@8.1.1) execa: 4.1.0 @@ -1986,7 +2001,7 @@ packages: defer-to-connect: 2.0.1 dev: true - /@testing-library/cypress@9.0.0(cypress@12.17.4): + /@testing-library/cypress@9.0.0(cypress@12.17.3): resolution: {integrity: sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -1994,7 +2009,7 @@ packages: dependencies: '@babel/runtime': 7.21.5 '@testing-library/dom': 8.20.0 - cypress: 12.17.4 + cypress: 12.17.3 dev: false /@testing-library/dom@8.20.0: @@ -2258,7 +2273,7 @@ packages: resolution: {integrity: sha512-WTiIZhZKWDnV+Tgo42pxff8YfHdmaNFQz/bFoTlmfw2vbXcstCcb39VRaRi5yFHj/lb7t3K47btKYEbR1fci+Q==} dependencies: '@testing-library/dom': 8.20.0 - cypress: 12.17.4 + cypress: 12.17.3 dev: false /@types/uglify-js@3.17.1: @@ -3821,8 +3836,8 @@ packages: resolution: {integrity: sha512-+IyVnYNiaX1+C+V/LazrJWAi/CqiwfNoRSrFviECQEyolW1gDRy765PZosL2alSSGK8V10Y7BGfOQyZUDgmnjQ==} dev: false - /cypress@12.17.4: - resolution: {integrity: sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==} + /cypress@12.17.3: + resolution: {integrity: sha512-/R4+xdIDjUSLYkiQfwJd630S81KIgicmQOLXotFxVXkl+eTeVO+3bHXxdi5KBh/OgC33HWN33kHX+0tQR/ZWpg==} engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} hasBin: true requiresBuild: true @@ -3862,7 +3877,6 @@ packages: minimist: 1.2.8 ospath: 1.2.2 pretty-bytes: 5.6.0 - process: 0.11.10 proxy-from-env: 1.0.0 request-progress: 3.0.0 semver: 7.5.4 @@ -4009,7 +4023,7 @@ packages: array-buffer-byte-length: 1.0.0 call-bind: 1.0.2 es-get-iterator: 1.1.3 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.1 is-arguments: 1.1.1 is-array-buffer: 3.0.2 is-date-object: 1.0.5 @@ -4399,7 +4413,7 @@ packages: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.1 has-symbols: 1.0.3 is-arguments: 1.1.1 is-map: 2.0.2 @@ -8329,11 +8343,6 @@ packages: fromentries: 1.3.2 dev: false - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: false - /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -9168,7 +9177,7 @@ packages: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.2.0 + get-intrinsic: 1.2.1 object-inspect: 1.12.3 /signal-exit@3.0.7: diff --git a/support/commands.js b/support/commands.js index 15694e6d5..b787ad067 100644 --- a/support/commands.js +++ b/support/commands.js @@ -79,6 +79,10 @@ Cypress.Commands.add('createMetamaskAccount', accountName => { return cy.task('createMetamaskAccount', accountName); }); +Cypress.Commands.add('renameMetamaskAccount', newAccountName => { + return cy.task('renameMetamaskAccount', newAccountName); +}); + Cypress.Commands.add('switchMetamaskAccount', accountNameOrAccountNumber => { return cy.task('switchMetamaskAccount', accountNameOrAccountNumber); }); @@ -218,10 +222,25 @@ Cypress.Commands.add('confirmMetamaskTransaction', gasConfig => { return cy.task('confirmMetamaskTransaction', gasConfig); }); +Cypress.Commands.add( + 'confirmMetamaskTransactionAndWaitForMining', + gasConfig => { + return cy.task('confirmMetamaskTransactionAndWaitForMining', gasConfig); + }, +); + Cypress.Commands.add('rejectMetamaskTransaction', () => { return cy.task('rejectMetamaskTransaction'); }); +Cypress.Commands.add('openMetamaskTransactionDetails', txIndex => { + return cy.task('openMetamaskTransactionDetails', txIndex); +}); + +Cypress.Commands.add('closeMetamaskTransactionDetailsPopup', () => { + return cy.task('closeMetamaskTransactionDetailsPopup'); +}); + Cypress.Commands.add('rejectMetamaskPermisionToApproveAll', () => { return cy.task('rejectMetamaskPermisionToApproveAll'); }); diff --git a/support/index.d.ts b/support/index.d.ts index 8e375993f..8a2d1cc9a 100644 --- a/support/index.d.ts +++ b/support/index.d.ts @@ -164,6 +164,12 @@ declare namespace Cypress { * cy.createMetamaskAccount('accountName') */ createMetamaskAccount(accountName?: string): Chainable; + /** + * Rename current account in metamask + * @example + * cy.renameMetamaskAccount('fancyAccountName') + */ + renameMetamaskAccount(newAccountName: string): Chainable; /** * Switch metamask account * @example @@ -395,12 +401,49 @@ declare namespace Cypress { | 'aggressive' | 'site', ): Chainable; + /** + * Confirm metamask transaction (auto-detects eip-1559 and legacy transactions) and wait for ALL pending transactions to be mined + * @example + * cy.confirmMetamaskTransactionAndWaitForMining() + * cy.confirmMetamaskTransactionAndWaitForMining({ gasLimit: 1000000, baseFee: 20, priorityFee: 20 }) // eip-1559 + * cy.confirmMetamaskTransactionAndWaitForMining({ gasLimit: 1000000, gasPrice: 20 }) // legacy + * cy.confirmMetamaskTransactionAndWaitForMining('aggressive') // eip-1559 only! => available options: 'low', 'market', 'aggressive', 'site' (site is usually by default) + */ + confirmMetamaskTransactionAndWaitForMining( + gasConfig?: + | { + gasLimit?: number; + baseFee?: number; + priorityFee?: number; + } + | { + gasLimit?: number; + gasPrice?: number; + } + | 'low' + | 'market' + | 'aggressive' + | 'site', + ): Chainable; /** * Reject metamask transaction * @example * cy.rejectMetamaskTransaction() */ rejectMetamaskTransaction(): Chainable; + /** + * Open metamask transaction details based on the index of the transaction in the list on the activity tab + * @example + * cy.openMetamaskTransactionDetails(0) + * cy.openMetamaskTransactionDetails(1) + */ + openMetamaskTransactionDetails(txIndex: number): Chainable; + /** + * Close metamask transaction details popup + * @example + * cy.closeMetamaskTransactionDetailsPopup() + */ + closeMetamaskTransactionDetailsPopup(): Chainable; /** * Allow site to add new network in metamask * @example diff --git a/tests/e2e/specs/1-metamask-spec.js b/tests/e2e/specs/1-metamask-spec.js index ed8953e72..3b00ae2a2 100644 --- a/tests/e2e/specs/1-metamask-spec.js +++ b/tests/e2e/specs/1-metamask-spec.js @@ -171,11 +171,21 @@ describe('Metamask', () => { expect(created).to.be.equal('This account name already exists'); }); }); + it(`renameMetamaskAccount should rename metamask account`, () => { + cy.renameMetamaskAccount('custom-fancy-wallet').then(created => { + expect(created).to.be.true; + }); + }); it(`switchMetamaskAccount should switch to another account using order number`, () => { cy.switchMetamaskAccount(2).then(switched => { expect(switched).to.be.true; }); }); + it(`renameMetamaskAccount should not fail when account with this name already exists`, () => { + cy.renameMetamaskAccount('custom-fancy-wallet').then(created => { + expect(created).to.be.equal('This account name already exists'); + }); + }); it(`getMetamaskWalletAddress should return wallet address of current metamask account`, () => { cy.getMetamaskWalletAddress().then(address => { expect(address).to.be.equal( @@ -331,6 +341,15 @@ describe('Metamask', () => { expect(txData.confirmed).to.be.true; }); }); + it(`confirmMetamaskTransaction should confirm legacy ETH transfer to yourself`, () => { + cy.get('#fromInput').type('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'); + cy.get('#toInput').type('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'); + cy.get('#amountInput').type('0x38D7EA4C68000'); // 0.001 ETH + cy.get('#submitForm').click(); + cy.confirmMetamaskTransaction().then(txData => { + expect(txData.recipientPublicAddress).to.be.equal('Account 1'); + }); + }); it(`confirmMetamaskTransaction should confirm eip-1559 transaction using default settings`, () => { cy.get('#sendEIP1559Button').click(); cy.confirmMetamaskTransaction().then(txData => { @@ -368,6 +387,60 @@ describe('Metamask', () => { expect(txData.confirmed).to.be.true; }); }); + it(`confirmMetamaskTransactionAndWaitForMining should confirm legacy transaction and wait for it to be mined`, () => { + cy.get('#sendButton').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.recipientPublicAddress).to.be.not.empty; + expect(txData.networkName).to.be.not.empty; + expect(txData.customNonce).to.be.not.empty; + expect(txData.confirmed).to.be.true; + }); + }); + it(`confirmMetamaskTransactionAndWaitForMining should confirm eip-1559 transaction and wait for it to be mined`, () => { + cy.get('#sendEIP1559Button').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.recipientPublicAddress).to.be.not.empty; + expect(txData.networkName).to.be.not.empty; + expect(txData.customNonce).to.be.not.empty; + expect(txData.confirmed).to.be.true; + }); + }); + it(`chaining confirmMetamaskTransactionAndWaitForMining should work as expected`, () => { + cy.get('#sendEIP1559Button').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.confirmed).to.be.true; + }); + cy.get('#sendEIP1559Button').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.confirmed).to.be.true; + }); + cy.get('#sendEIP1559Button').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.confirmed).to.be.true; + }); + cy.get('#sendEIP1559Button').click(); + cy.confirmMetamaskTransactionAndWaitForMining().then(txData => { + expect(txData.confirmed).to.be.true; + }); + }); + it(`openMetamaskTransactionDetails should open transaction details popup`, () => { + // Cannot be tested further with Cypress 😔 + cy.openMetamaskTransactionDetails(0).then( + opened => expect(opened).to.be.true, + ); + }); + it(`closeMetamaskTransactionDetailsPopup should close transaction details popup`, () => { + cy.closeMetamaskTransactionDetailsPopup().then( + closed => expect(closed).to.be.true, + ); + }); + it(`openMetamaskTransactionDetails should click "View more" button enough times to open correct transaction details popup`, () => { + // Cannot be tested further with Cypress 😔 + cy.openMetamaskTransactionDetails(14); + cy.closeMetamaskTransactionDetailsPopup().then( + closed => expect(closed).to.be.true, + ); + }); it(`confirmMetamaskTransaction should confirm transaction for token creation (contract deployment) and check tx data`, () => { cy.get('#createToken').click(); cy.confirmMetamaskTransaction().then(txData => { @@ -383,6 +456,13 @@ describe('Metamask', () => { .invoke('text') .then(text => cy.log('Token hash: ' + text)); }); + it(`openMetamaskTransactionDetails should open correct transaction details popup when there is a pending tx`, () => { + // Cannot be tested further with Cypress 😔 + cy.openMetamaskTransactionDetails(0); + cy.closeMetamaskTransactionDetailsPopup().then( + closed => expect(closed).to.be.true, + ); + }); it(`rejectMetamaskAddToken should cancel importing a token`, () => { cy.get('#watchAsset').click(); cy.rejectMetamaskAddToken().then(rejected => {