diff --git a/commands/metamask.js b/commands/metamask.js index 4c005ed4b..2c217513a 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, @@ -1180,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 // TODO rename + .metamaskWindow() + .getByText('Pending') + .count(); + const queuedTxs = await playwright // TODO rename + .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( @@ -1189,6 +1254,57 @@ 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({ hasNotText: 'History' }) + .filter({ hasNotText: 'View more' }) + .all(); + + while (txIndex >= visibleTxs.length) { + try { + await playwright.metamaskWindow().getByText('View more').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({ hasNotText: 'History' }) + .filter({ hasNotText: 'View more' }) + .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/docs/synpress-commands.md b/docs/synpress-commands.md index a21abf107..408d800aa 100644 --- a/docs/synpress-commands.md +++ b/docs/synpress-commands.md @@ -368,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. @@ -376,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/pages/metamask/main-page.js b/pages/metamask/main-page.js index 4c022a097..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`, }; diff --git a/plugins/index.js b/plugins/index.js index a5ab9e7ec..51fe4965b 100644 --- a/plugins/index.js +++ b/plugins/index.js @@ -108,7 +108,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/support/commands.js b/support/commands.js index 04860445a..73869e3fc 100644 --- a/support/commands.js +++ b/support/commands.js @@ -196,10 +196,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 d694bf0ca..804e32d54 100644 --- a/support/index.d.ts +++ b/support/index.d.ts @@ -334,12 +334,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/metamask-spec.js b/tests/e2e/specs/metamask-spec.js index 4fb810524..3b00ae2a2 100644 --- a/tests/e2e/specs/metamask-spec.js +++ b/tests/e2e/specs/metamask-spec.js @@ -387,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 => { @@ -402,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 => {