diff --git a/wallets/metamask/src/cypress/configureSynpress.ts b/wallets/metamask/src/cypress/configureSynpress.ts index 6c5ec83da..90744be65 100644 --- a/wallets/metamask/src/cypress/configureSynpress.ts +++ b/wallets/metamask/src/cypress/configureSynpress.ts @@ -1,7 +1,11 @@ import type { BrowserContext, Page } from '@playwright/test' import { expect } from '@playwright/test' import { ensureRdpPort } from '@synthetixio/synpress-core' +import { type CreateAnvilOptions, createPool } from '@viem/anvil' +import { waitFor } from '../playwright/utils/waitFor' +import HomePageSelectors from '../selectors/pages/HomePage' import Selectors from '../selectors/pages/HomePage' +import type { Network } from '../type/Network' import getPlaywrightMetamask from './getPlaywrightMetamask' import importMetaMaskWallet from './support/importMetaMaskWallet' import { initMetaMask } from './support/initMetaMask' @@ -118,7 +122,7 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr await metamask.renameAccount(currentAccountName, newAccountName) - await metamaskExtensionPage.locator(Selectors.threeDotsMenu.accountDetailsCloseButton).click() + await metamaskExtensionPage.locator(HomePageSelectors.threeDotsMenu.accountDetailsCloseButton).click() await expect(metamaskExtensionPage.locator(metamask.homePage.selectors.accountMenu.accountButton)).toHaveText( newAccountName @@ -146,6 +150,84 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr }) }, + async createAnvilNode(options?: CreateAnvilOptions) { + const pool = createPool() + + const nodeId = Array.from(pool.instances()).length + const anvil = await pool.start(nodeId, options) + + const rpcUrl = `http://${anvil.host}:${anvil.port}` + + const DEFAULT_ANVIL_CHAIN_ID = 31337 + const chainId = options?.chainId ?? DEFAULT_ANVIL_CHAIN_ID + + return { anvil, rpcUrl, chainId } + }, + + async connectToAnvil({ + rpcUrl, + chainId + }: { + rpcUrl: string + chainId: number + }) { + const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) + + try { + await metamask.addNetwork({ + name: 'Anvil', + rpcUrl, + chainId, + symbol: 'ETH', + blockExplorerUrl: 'https://etherscan.io/' + }) + + await metamask.switchNetwork('Anvil') + return true + } catch (e) { + console.error('Error connecting to Anvil network', e) + return false + } + }, + + async addNetwork(network: Network) { + const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) + + await metamask.addNetwork(network) + + await waitFor( + () => metamaskExtensionPage.locator(HomePageSelectors.networkAddedPopover.switchToNetworkButton).isVisible(), + 3_000, + false + ) + + await metamaskExtensionPage.locator(HomePageSelectors.networkAddedPopover.switchToNetworkButton).click() + + return true + }, + + // Token + + async deployToken() { + const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) + + await metamask.confirmTransaction() + + return true + }, + + async addNewToken() { + const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) + + await metamask.addNewToken() + + await expect(metamaskExtensionPage.locator(Selectors.portfolio.singleToken).nth(1)).toContainText('TST') + + return true + }, + + // Others + async providePublicEncryptionKey() { const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) @@ -183,6 +265,19 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr .catch(() => { return false }) + }, + + async confirmTransaction() { + const metamask = getPlaywrightMetamask(context, metamaskExtensionPage, metamaskExtensionId) + + return await metamask + .confirmTransaction() + .then(() => { + return true + }) + .catch(() => { + return false + }) } }) diff --git a/wallets/metamask/src/cypress/support/synpressCommands.ts b/wallets/metamask/src/cypress/support/synpressCommands.ts index 000c60d1c..a0cf879f7 100644 --- a/wallets/metamask/src/cypress/support/synpressCommands.ts +++ b/wallets/metamask/src/cypress/support/synpressCommands.ts @@ -9,6 +9,9 @@ // https://on.cypress.io/custom-commands // *********************************************** +import type { Anvil, CreateAnvilOptions } from '@viem/anvil' +import type { Network } from '../../type/Network' + declare global { namespace Cypress { interface Chainable { @@ -22,9 +25,21 @@ declare global { renameAccount(currentAccountName: string, newAccountName: string): Chainable switchNetwork(networkName: string, isTestnet?: boolean): Chainable + createAnvilNode(options?: CreateAnvilOptions): Chainable<{ + anvil: Anvil + rpcUrl: string + chainId: number + }> + connectToAnvil(): Chainable + addNetwork(network: Network): Chainable + + deployToken(): Chainable + addNewToken(): Chainable + providePublicEncryptionKey(): Chainable decrypt(): Chainable confirmSignature(): Chainable + confirmTransaction(): Chainable } } } @@ -41,6 +56,8 @@ export default function synpressCommands() { return cy.task('connectToDapp') }) + // Account + Cypress.Commands.add('addNewAccount', (accountName: string) => { return cy.task('addNewAccount', accountName) }) @@ -51,9 +68,48 @@ export default function synpressCommands() { return cy.task('renameAccount', { currentAccountName, newAccountName }) }) + // Network + Cypress.Commands.add('switchNetwork', (networkName: string, isTestnet = false) => { return cy.task('switchNetwork', { networkName, isTestnet }) }) + Cypress.Commands.add('createAnvilNode', (options?: CreateAnvilOptions) => { + return cy.task('createAnvilNode', options) + }) + Cypress.Commands.add('connectToAnvil', () => { + return cy.task('createAnvilNode').then((anvilNetwork) => { + const anvilNetworkDetails = anvilNetwork as { + anvil: Anvil + rpcUrl: string + chainId: number + } + + const network = { + name: 'Anvil', + rpcUrl: anvilNetworkDetails.rpcUrl, + chainId: anvilNetworkDetails.chainId, + symbol: 'ETH', + blockExplorerUrl: 'https://etherscan.io/' + } + + return cy.task('addNetwork', network) + }) + }) + Cypress.Commands.add('addNetwork', (network: Network) => { + return cy.task('addNetwork', network) + }) + + // Token + + Cypress.Commands.add('deployToken', () => { + return cy.task('deployToken') + }) + Cypress.Commands.add('addNewToken', () => { + return cy.task('addNewToken') + }) + + // Others + Cypress.Commands.add('providePublicEncryptionKey', () => { return cy.task('providePublicEncryptionKey') }) @@ -63,4 +119,7 @@ export default function synpressCommands() { Cypress.Commands.add('confirmSignature', () => { return cy.task('confirmSignature') }) + Cypress.Commands.add('confirmTransaction', () => { + return cy.task('confirmTransaction') + }) } diff --git a/wallets/metamask/src/playwright/MetaMask.ts b/wallets/metamask/src/playwright/MetaMask.ts index 784f2322b..3afea4d8a 100644 --- a/wallets/metamask/src/playwright/MetaMask.ts +++ b/wallets/metamask/src/playwright/MetaMask.ts @@ -1,8 +1,8 @@ import type { BrowserContext, Page } from '@playwright/test' import { SettingsSidebarMenus } from '../selectors/pages/HomePage/settings' import { MetaMaskAbstract } from '../type/MetaMaskAbstract' +import type { Network } from '../type/Network' import { CrashPage, HomePage, LockPage, NotificationPage, OnboardingPage } from './pages' -import type { Network } from './pages/HomePage/actions' import type { GasSetting } from './pages/NotificationPage/actions' import { SettingsPage } from './pages/SettingsPage/page' diff --git a/wallets/metamask/src/playwright/pages/HomePage/actions/addNetwork.ts b/wallets/metamask/src/playwright/pages/HomePage/actions/addNetwork.ts index 7a23f2cf4..0162ca5f0 100644 --- a/wallets/metamask/src/playwright/pages/HomePage/actions/addNetwork.ts +++ b/wallets/metamask/src/playwright/pages/HomePage/actions/addNetwork.ts @@ -1,21 +1,11 @@ import type { Page } from '@playwright/test' -import { z } from 'zod' import Selectors from '../../../../selectors/pages/HomePage' +import { type Network, NetworkValidation } from '../../../../type/Network' import { waitFor } from '../../../utils/waitFor' import { closeNetworkAddedPopover, closeNewNetworkInfoPopover } from './popups' -const Network = z.object({ - name: z.string(), - rpcUrl: z.string(), - chainId: z.number(), - symbol: z.string(), - blockExplorerUrl: z.string().optional() -}) - -export type Network = z.infer - export async function addNetwork(page: Page, network: Network) { - const { name, rpcUrl, chainId, symbol, blockExplorerUrl } = Network.parse(network) + const { name, rpcUrl, chainId, symbol, blockExplorerUrl } = NetworkValidation.parse(network) await page.locator(Selectors.networkDropdown.dropdownButton).click() await page.locator(Selectors.networkDropdown.addNetworkButton).click() diff --git a/wallets/metamask/src/playwright/pages/HomePage/page.ts b/wallets/metamask/src/playwright/pages/HomePage/page.ts index 15ea8477b..cdae09fa7 100644 --- a/wallets/metamask/src/playwright/pages/HomePage/page.ts +++ b/wallets/metamask/src/playwright/pages/HomePage/page.ts @@ -1,6 +1,7 @@ import type { Page } from '@playwright/test' import Selectors from '../../../selectors/pages/HomePage' import type { SettingsSidebarMenus } from '../../../selectors/pages/HomePage/settings' +import type { Network } from '../../../type/Network' import { addNetwork, addNewAccount, @@ -14,7 +15,6 @@ import { toggleShowTestNetworks, transactionDetails } from './actions' -import type { Network } from './actions' export class HomePage { static readonly selectors = Selectors diff --git a/wallets/metamask/src/selectors/pages/HomePage/index.ts b/wallets/metamask/src/selectors/pages/HomePage/index.ts index d3a8a10b1..61e04cf29 100644 --- a/wallets/metamask/src/selectors/pages/HomePage/index.ts +++ b/wallets/metamask/src/selectors/pages/HomePage/index.ts @@ -54,7 +54,7 @@ const popover = { } const networkAddedPopover = { - switchToNetworkButton: '.home__new-network-added button.btn-primary', + switchToNetworkButton: '.home__new-network-added__switch-to-button', dismissButton: '.home__new-network-added button.btn-secondary' } diff --git a/wallets/metamask/src/type/MetaMaskAbstract.ts b/wallets/metamask/src/type/MetaMaskAbstract.ts index be64e4b90..52321c9b6 100644 --- a/wallets/metamask/src/type/MetaMaskAbstract.ts +++ b/wallets/metamask/src/type/MetaMaskAbstract.ts @@ -1,6 +1,6 @@ -import type { Network } from '../playwright/pages/HomePage/actions' import type { GasSetting } from '../playwright/pages/NotificationPage/actions' import { SettingsSidebarMenus } from '../selectors/pages/HomePage/settings' +import type { Network } from './Network' export abstract class MetaMaskAbstract { /** diff --git a/wallets/metamask/src/type/Network.ts b/wallets/metamask/src/type/Network.ts new file mode 100644 index 000000000..0621fbd00 --- /dev/null +++ b/wallets/metamask/src/type/Network.ts @@ -0,0 +1,11 @@ +import { z } from 'zod' + +export const NetworkValidation = z.object({ + name: z.string(), + rpcUrl: z.string(), + chainId: z.number(), + symbol: z.string(), + blockExplorerUrl: z.string().optional() +}) + +export type Network = z.infer diff --git a/wallets/metamask/test/cypress/addNetwork.cy.ts b/wallets/metamask/test/cypress/addNetwork.cy.ts new file mode 100644 index 000000000..b180d6e20 --- /dev/null +++ b/wallets/metamask/test/cypress/addNetwork.cy.ts @@ -0,0 +1,31 @@ +it('should add network and close network added popup', () => { + cy.createAnvilNode().then(({ rpcUrl, chainId }) => { + const network = { + name: 'Anvil', + rpcUrl, + chainId, + symbol: 'ETH', + blockExplorerUrl: 'https://etherscan.io/' + } + + cy.addNetwork(network).then(() => cy.getNetwork().should('eq', 'Anvil')) + }) +}) + +it('should add network without block explorer', () => { + cy.createAnvilNode().then(({ rpcUrl, chainId }) => { + const network = { + name: 'Anvil2', + rpcUrl, + chainId, + symbol: 'ETH', + blockExplorerUrl: undefined + } + + cy.addNetwork(network).then(() => cy.getNetwork().should('eq', 'Anvil2')) + }) +}) + +after(() => { + cy.switchNetwork('Anvil', true) +}) diff --git a/wallets/metamask/test/cypress/addNewToken.cy.ts b/wallets/metamask/test/cypress/addNewToken.cy.ts new file mode 100644 index 000000000..9a8803fff --- /dev/null +++ b/wallets/metamask/test/cypress/addNewToken.cy.ts @@ -0,0 +1,42 @@ +before(() => { + cy.getNetwork().then((network) => { + console.log(network) + if (network !== 'Anvil') { + cy.switchNetwork('Anvil') + } + }) + + cy.get('#connectButton').click() + + cy.connectToDapp() +}) + +it('should add new token to MetaMask', () => { + cy.get('#createToken').click() + + // wait for the blockchain - todo: replace with an event handler + cy.wait(5000) + + cy.deployToken().then(() => { + // wait for the blockchain - todo: replace with an event handler + cy.wait(5000) + + cy.get('#tokenAddresses').should('have.text', '0x7ef8E99980Da5bcEDcF7C10f41E55f759F6A174B') + + cy.get('#watchAssets').click() + + cy.addNewToken() + }) +}) + +it('should add new token using EIP747', () => { + cy.get('#eip747ContractAddress').type('0x5FbDB2315678afecb367f032d93F642f64180aa3') + cy.get('#eip747Symbol').type('TST') + cy.get('#eip747Decimals').type('4') + + cy.get('#eip747WatchButton').click() + + cy.addNewToken().then(() => { + cy.get('#eip747Status').should('have.text', 'NFT added successfully') + }) +}) diff --git a/wallets/metamask/test/cypress/confirmSignature.cy.ts b/wallets/metamask/test/cypress/confirmSignature.cy.ts index cfbc81727..d336ecdc6 100644 --- a/wallets/metamask/test/cypress/confirmSignature.cy.ts +++ b/wallets/metamask/test/cypress/confirmSignature.cy.ts @@ -1,6 +1,5 @@ before(() => { - cy.get('#connectButton').click() - cy.connectToDapp() + cy.switchNetwork('Anvil') }) it('should confirm `personal_sign`', () => { @@ -10,13 +9,13 @@ it('should confirm `personal_sign`', () => { cy.get('#personalSignResult').should( 'have.text', - '0xf95b3efc808585303e20573e960993cde30c7f5a0f1c25cfab0379d5a14311d17898199814c8ebe66ec80b2b11690f840bde539f862ff4f04468d2a40f15178a1b' + '0x9979ca3126a989995ac4a824bb91a2b624c4fb8d55934a76a269be56227c1d725379cd37ae83d8955bc3bf1d6dd5887cc036fcb4ca7ebac6d78cb7441bb1e9ad1c' ) cy.get('#personalSignVerify').click() - cy.get('#personalSignVerifySigUtilResult').should('have.text', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') - cy.get('#personalSignVerifyECRecoverResult').should('have.text', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') + cy.get('#personalSignVerifySigUtilResult').should('have.text', '0x976ea74026e726554db657fa54763abd0c3a0aa9') + cy.get('#personalSignVerifyECRecoverResult').should('have.text', '0x976ea74026e726554db657fa54763abd0c3a0aa9') }) it('should confirm `eth_signTypedData`', () => { @@ -26,12 +25,12 @@ it('should confirm `eth_signTypedData`', () => { cy.get('#signTypedDataResult').should( 'have.text', - '0xd75eece0d337f4e425f87bd112c849561956afe4f154cdd07d1d4cba7a979b481ba6ceede5c0eb9daa66bec4eea6e7ecfee5496274ef2a93b69abd97531519b21c' + '0xb1658385404c1f7730369ea91bd0272e7fb4ea6450257887e8a287ac7e412ec14cf4938d5422d29dab3bc1b1cdef21d27f95a12ff823e8efc27af8d788c347a91c' ) cy.get('#signTypedDataVerify').click() - cy.get('#signTypedDataVerifyResult').should('have.text', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') + cy.get('#signTypedDataVerifyResult').should('have.text', '0x976ea74026e726554db657fa54763abd0c3a0aa9') }) it('should confirm `eth_signTypedData_v3`', () => { @@ -41,12 +40,12 @@ it('should confirm `eth_signTypedData_v3`', () => { cy.get('#signTypedDataV3Result').should( 'have.text', - '0x6ea8bb309a3401225701f3565e32519f94a0ea91a5910ce9229fe488e773584c0390416a2190d9560219dab757ecca2029e63fa9d1c2aebf676cc25b9f03126a1b' + '0x9f74431e66c2c63fdb1fe32c0fa20d02b903ec9e1f6e25cf9cc45b2f8fcd00057b01758611e07fc70d41f1347f9f5b307e25951c39ba8a0b226c280cea9a84751b' ) cy.get('#signTypedDataV3Verify').click() - cy.get('#signTypedDataV3VerifyResult').should('have.text', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') + cy.get('#signTypedDataV3VerifyResult').should('have.text', '0x976ea74026e726554db657fa54763abd0c3a0aa9') }) it('should confirm `eth_signTypedData_v4`', () => { @@ -56,12 +55,12 @@ it('should confirm `eth_signTypedData_v4`', () => { cy.get('#signTypedDataV4Result').should( 'have.text', - '0x1cf422c4a319c19ecb89c960e7c296810278fa2bef256c7e9419b285c8216c547b3371fa1ec3987ce08561d3ed779845393d8d3e4311376d0bc0846f37d1b2821c' + '0x3145a267aa3e6ba1b5f6374ea2ff62bb8de433096e4c4bed23bf9532a208690e3580fb9076ad40814a8e3ba23e0db2444dfc0baf7dddc5c72a440f1e65b85a331c' ) cy.get('#signTypedDataV4Verify').click() - cy.get('#signTypedDataV4VerifyResult').should('have.text', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266') + cy.get('#signTypedDataV4VerifyResult').should('have.text', '0x976ea74026e726554db657fa54763abd0c3a0aa9') }) // TODO: `unsafe_enableEthSign` needs to be implemented diff --git a/wallets/metamask/test/cypress/connectToDapp.cy.ts b/wallets/metamask/test/cypress/connectToDapp.cy.ts index a3f6f881a..dcc9d60de 100644 --- a/wallets/metamask/test/cypress/connectToDapp.cy.ts +++ b/wallets/metamask/test/cypress/connectToDapp.cy.ts @@ -1,3 +1,7 @@ +before(() => { + cy.get('#revokeAccountsPermission').click() +}) + it('should connect account to the app', () => { cy.get('#connectButton').click() cy.connectToDapp() diff --git a/wallets/metamask/test/cypress/switchNetwork.cy.ts b/wallets/metamask/test/cypress/switchNetwork.cy.ts index cd771ee5f..0efeb279f 100644 --- a/wallets/metamask/test/cypress/switchNetwork.cy.ts +++ b/wallets/metamask/test/cypress/switchNetwork.cy.ts @@ -1,3 +1,7 @@ +before(() => { + cy.switchNetwork('Ethereum Mainnet') +}) + it('should switch network', () => { cy.getNetwork().should('eq', 'Ethereum Mainnet')