diff --git a/.gitignore b/.gitignore index 564fad60..90c9c80a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # VS Code .vscode +.history # JetBrains .idea diff --git a/biome.json b/biome.json index 6397ca6f..de665c82 100644 --- a/biome.json +++ b/biome.json @@ -13,7 +13,8 @@ "**/playwright-report", "**/.cache-synpress", "**/.vitepress/cache", - "**/downloads" + "**/downloads", + "**/.history" ] }, "formatter": { diff --git a/wallets/metamask/src/cypress/MetaMask.ts b/wallets/metamask/src/cypress/MetaMask.ts index 45c67888..16cded46 100644 --- a/wallets/metamask/src/cypress/MetaMask.ts +++ b/wallets/metamask/src/cypress/MetaMask.ts @@ -26,6 +26,10 @@ export default class MetaMask { .innerText() } + async getAccountAddress() { + return await this.metamaskPlaywright.getAccountAddress() + } + async getNetwork() { return await this.metamaskExtensionPage .locator(this.metamaskPlaywright.homePage.selectors.currentNetwork) @@ -188,6 +192,12 @@ export default class MetaMask { }) } + async rejectTokenPermission() { + await this.metamaskPlaywright.rejectTokenPermission() + + return true + } + // Network async approveNewNetwork() { @@ -202,6 +212,18 @@ export default class MetaMask { return true } + async rejectNewNetwork() { + await this.metamaskPlaywright.rejectNewNetwork() + + return true + } + + async rejectSwitchNetwork() { + await this.metamaskPlaywright.rejectSwitchNetwork() + + return true + } + // Others async providePublicEncryptionKey() { @@ -237,15 +259,29 @@ export default class MetaMask { }) } - async confirmTransaction() { - return await this.metamaskPlaywright - .confirmTransaction() - .then(() => { - return true - }) - .catch(() => { - return false - }) + async rejectSignature() { + await this.metamaskPlaywright.rejectSignature() + + return true + } + + async confirmTransaction(options?: { gasSetting?: GasSettings }) { + await waitFor( + () => + this.metamaskExtensionPage.locator(TransactionPage.nftApproveAllConfirmationPopup.approveButton).isVisible(), + 5_000, + false + ) + + await this.metamaskPlaywright.confirmTransaction(options) + + return true + } + + async rejectTransaction() { + await this.metamaskPlaywright.rejectTransaction() + + return true } async confirmTransactionAndWaitForMining() { diff --git a/wallets/metamask/src/cypress/configureSynpress.ts b/wallets/metamask/src/cypress/configureSynpress.ts index 88e9931f..464190cf 100644 --- a/wallets/metamask/src/cypress/configureSynpress.ts +++ b/wallets/metamask/src/cypress/configureSynpress.ts @@ -61,12 +61,12 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr } }) + // Synpress API on('task', { - // Synpress API - // Account connectToDapp: () => metamask?.connectToDapp(), getAccount: () => metamask?.getAccount(), + getAccountAddress: () => metamask?.getAccountAddress(), addNewAccount: (accountName: string) => metamask?.addNewAccount(accountName), switchAccount: (accountName: string) => metamask?.switchAccount(accountName), renameAccount: ({ @@ -93,6 +93,8 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr addNetwork: (network: Network) => metamask?.addNetwork(network), approveNewNetwork: () => metamask?.approveNewNetwork(), approveSwitchNetwork: () => metamask?.approveSwitchNetwork(), + rejectNewNetwork: () => metamask?.rejectNewNetwork(), + rejectSwitchNetwork: () => metamask?.rejectSwitchNetwork(), // Anvil createAnvilNode: (options?: CreateAnvilOptions) => metamask?.createAnvilNode(options), @@ -105,6 +107,7 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr spendLimit?: number | 'max' gasSetting?: GasSettings }) => metamask?.approveTokenPermission(options), + rejectTokenPermission: () => metamask?.rejectTokenPermission(), // Encryption providePublicEncryptionKey: () => metamask?.providePublicEncryptionKey(), @@ -112,7 +115,9 @@ export default function configureSynpress(on: Cypress.PluginEvents, config: Cypr // Transactions confirmSignature: () => metamask?.confirmSignature(), - confirmTransaction: () => metamask?.confirmTransaction(), + rejectSignature: () => metamask?.rejectSignature(), + confirmTransaction: (options?: { gasSetting?: GasSettings }) => metamask?.confirmTransaction(options), + rejectTransaction: () => metamask?.rejectTransaction(), confirmTransactionAndWaitForMining: () => metamask?.confirmTransactionAndWaitForMining(), openTransactionDetails: (txIndex: number) => metamask?.openTransactionDetails(txIndex), closeTransactionDetails: () => metamask?.closeTransactionDetails() diff --git a/wallets/metamask/src/cypress/support/synpressCommands.ts b/wallets/metamask/src/cypress/support/synpressCommands.ts index 167e5363..089863af 100644 --- a/wallets/metamask/src/cypress/support/synpressCommands.ts +++ b/wallets/metamask/src/cypress/support/synpressCommands.ts @@ -24,6 +24,7 @@ declare global { addNewAccount(accountName: string): Chainable switchAccount(accountName: string): Chainable renameAccount(currentAccountName: string, newAccountName: string): Chainable + getAccountAddress(): Chainable switchNetwork(networkName: string, isTestnet?: boolean): Chainable createAnvilNode(options?: CreateAnvilOptions): Chainable<{ @@ -36,6 +37,8 @@ declare global { addNetwork(network: Network): Chainable approveNewNetwork(): Chainable approveSwitchNetwork(): Chainable + rejectNewNetwork(): Chainable + rejectSwitchNetwork(): Chainable deployToken(): Chainable addNewToken(): Chainable @@ -43,11 +46,14 @@ declare global { spendLimit?: number | 'max' gasSetting?: GasSettings }): Chainable + rejectTokenPermission(): Chainable providePublicEncryptionKey(): Chainable decrypt(): Chainable confirmSignature(): Chainable - confirmTransaction(): Chainable + rejectSignature(): Chainable + confirmTransaction(options?: { gasSetting?: GasSettings }): Chainable + rejectTransaction(): Chainable confirmTransactionAndWaitForMining(): Chainable openTransactionDetails(txIndex: number): Chainable closeTransactionDetails(): Chainable @@ -78,6 +84,9 @@ export default function synpressCommands() { Cypress.Commands.add('renameAccount', (currentAccountName: string, newAccountName: string) => { return cy.task('renameAccount', { currentAccountName, newAccountName }) }) + Cypress.Commands.add('getAccountAddress', () => { + return cy.task('getAccountAddress') + }) // Network @@ -118,6 +127,12 @@ export default function synpressCommands() { Cypress.Commands.add('approveSwitchNetwork', () => { return cy.task('approveSwitchNetwork') }) + Cypress.Commands.add('rejectNewNetwork', () => { + return cy.task('rejectNewNetwork') + }) + Cypress.Commands.add('rejectSwitchNetwork', () => { + return cy.task('rejectSwitchNetwork') + }) // Token @@ -136,6 +151,9 @@ export default function synpressCommands() { return cy.task('approveTokenPermission', options) } ) + Cypress.Commands.add('rejectTokenPermission', () => { + return cy.task('rejectTokenPermission') + }) // Others @@ -148,8 +166,14 @@ export default function synpressCommands() { Cypress.Commands.add('confirmSignature', () => { return cy.task('confirmSignature') }) - Cypress.Commands.add('confirmTransaction', () => { - return cy.task('confirmTransaction') + Cypress.Commands.add('rejectSignature', () => { + return cy.task('rejectSignature') + }) + Cypress.Commands.add('confirmTransaction', (options?: { gasSetting?: GasSettings }) => { + return cy.task('confirmTransaction', options) + }) + Cypress.Commands.add('rejectTransaction', () => { + return cy.task('rejectTransaction') }) Cypress.Commands.add('confirmTransactionAndWaitForMining', () => { return cy.task('confirmTransactionAndWaitForMining') diff --git a/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts b/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts index 64a38145..bba71a04 100644 --- a/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts +++ b/wallets/metamask/src/playwright/pages/NotificationPage/actions/approvePermission.ts @@ -1,7 +1,7 @@ import type { Page } from '@playwright/test' import Selectors from '../../../../selectors/pages/NotificationPage' -import { transaction } from './transaction' import type { GasSettings } from '../../../../type/GasSettings' +import { transaction } from './transaction' const editTokenPermission = async (notificationPage: Page, customSpendLimit: 'max' | number) => { if (customSpendLimit === 'max') { diff --git a/wallets/metamask/src/playwright/utils/waitFor.ts b/wallets/metamask/src/playwright/utils/waitFor.ts index 04e66a1f..b6b1b73b 100644 --- a/wallets/metamask/src/playwright/utils/waitFor.ts +++ b/wallets/metamask/src/playwright/utils/waitFor.ts @@ -32,7 +32,7 @@ export const waitForMetaMaskLoad = async (page: Page) => { }) ) .then(() => { - console.log('All loading indicators are hidden') + return true }) .catch((error) => { console.error('Error: ', error) diff --git a/wallets/metamask/src/type/MetaMaskAbstract.ts b/wallets/metamask/src/type/MetaMaskAbstract.ts index 6ae0cdf8..9404e363 100644 --- a/wallets/metamask/src/type/MetaMaskAbstract.ts +++ b/wallets/metamask/src/type/MetaMaskAbstract.ts @@ -1,6 +1,6 @@ import { SettingsSidebarMenus } from '../selectors/pages/HomePage/settings' +import type { GasSettings } from './GasSettings' import type { Network } from './Network' -import type { GasSettings } from './GasSettings'; export abstract class MetaMaskAbstract { /** diff --git a/wallets/metamask/test/cypress/confirmTransaction.cy.ts b/wallets/metamask/test/cypress/confirmTransaction.cy.ts new file mode 100644 index 00000000..84d693e8 --- /dev/null +++ b/wallets/metamask/test/cypress/confirmTransaction.cy.ts @@ -0,0 +1,222 @@ +const triggerEIP1559Transaction = () => { + cy.get('#sendEIP1559Button').click() +} + +const connectDeployAndMintNft = () => { + cy.get('#deployNFTsButton').click() + + return cy.confirmTransaction().then(() => { + cy.wait(5000) + + cy.get('#mintButton').click() + cy.confirmTransaction() + + cy.wait(5000) + }) +} + +before(() => { + cy.connectToAnvil().then(() => { + cy.get('#connectButton').click() + + cy.connectToDapp() + }) +}) + +describe('with default gas setting', () => { + it('should confirm contract deployment', () => { + cy.get('#tokenAddresses').should('be.empty') + + cy.get('#createToken').click() + + cy.confirmTransaction().then(() => { + cy.get('#tokenAddresses').should('include', /^0x/) + }) + }) + + it('should confirm legacy transaction', () => { + cy.get('#sendButton').click() + + cy.confirmTransaction() + }) + + it('should confirm EIP-1559 transaction', () => { + triggerEIP1559Transaction() + + cy.confirmTransaction() + }) +}) + +describe('NFTs', () => { + it('should confirm `watch NFT` request', () => { + connectDeployAndMintNft().then(() => { + cy.get('#watchNFTButton').click() + + cy.confirmTransaction() + }) + }) + + it('should confirm `watch all NFTs` request', () => { + connectDeployAndMintNft().then(() => { + cy.get('#watchNFTsButton').click() + + cy.confirmTransaction() + }) + }) + + it('should confirm `approve` transaction', () => { + connectDeployAndMintNft().then(() => { + cy.get('#approveButton').click() + + cy.confirmTransaction().then(() => { + cy.get('#nftsStatus').should('have.text', 'Approve initiated') + + cy.wait(5000) + + cy.get('#nftsStatus').should('have.text', 'Approve completed') + }) + }) + }) + + it('should confirm `set approval for all` transaction', () => { + connectDeployAndMintNft().then(() => { + cy.get('#setApprovalForAllButton').click() + + cy.confirmTransaction().then(() => { + cy.get('#nftsStatus').should('have.text', 'Set Approval For All completed') + }) + }) + }) + + it('should confirm `revoke` transaction', () => { + connectDeployAndMintNft().then(() => { + cy.get('#revokeButton').click() + + cy.confirmTransaction().then(() => { + cy.wait(5000) + + cy.get('#nftsStatus').should('have.text', 'Revoke completed') + }) + }) + }) + + it('should confirm `transfer from` transaction', () => { + connectDeployAndMintNft().then(() => { + cy.get('#transferFromButton').click() + + cy.confirmTransaction().then(() => { + cy.wait(5000) + + cy.get('#nftsStatus').should('have.text', 'Transfer From completed') + }) + }) + }) +}) + +describe('with custom gas setting', () => { + it('should confirm transaction with "site" gas setting', () => { + triggerEIP1559Transaction() + + cy.confirmTransaction({ gasSetting: 'site' }).then(() => { + return true + }) + }) +}) + +describe('with advanced (manual) gas setting', () => { + it('should confirm transaction with custom gas limit', () => { + triggerEIP1559Transaction() + + cy.confirmTransaction({ + gasSetting: { + maxBaseFee: 250, + priorityFee: 150, + gasLimit: 250_000 + } + }) + }) + + it('should confirm transaction with small gas fee', () => { + triggerEIP1559Transaction() + + cy.confirmTransaction({ + gasSetting: { + maxBaseFee: 250, + priorityFee: 150 + } + }) + }) + + // We're testing huge gas fee here, due to a bug in MetaMask. See comment inside the `confirmTransaction` method. + it('should confirm transaction with huge gas fee', () => { + triggerEIP1559Transaction() + + cy.confirmTransaction({ + gasSetting: { + maxBaseFee: 250_000, + priorityFee: 150_000 + } + }) + }) + + it('should confirm `set approval for all` transaction', () => { + connectDeployAndMintNft().then(() => { + cy.get('#setApprovalForAllButton').click() + + cy.confirmTransaction({ + gasSetting: { + maxBaseFee: 250, + priorityFee: 150 + } + }).then(() => { + cy.get('#nftsStatus').should('have.text', 'Set Approval For All completed') + }) + }) + }) +}) + +describe('with `from` and `to` specified', () => { + it('should confirm from/to transfer', () => { + cy.get('#createToken').click() + cy.deployToken().then(() => { + cy.wait(5000) // wait for the blockchain - todo: replace with an event handler + + cy.getAccountAddress().then((accountAddress) => { + cy.get('#transferFromSenderInput').type(accountAddress) + cy.get('#transferFromRecipientInput').type('0x70997970C51812dc3A010C7d01b50e0d17dc79C8') + + cy.get('#transferFromTokens').click() + cy.confirmTransaction() + }) + }) + }) +}) + +describe('without gas limit', () => { + it('should approve tokens', () => { + cy.get('#createToken').click() + cy.deployToken().then(() => { + cy.get('#approveTokensWithoutGas').click() + cy.approveTokenPermission() + }) + }) + + it('should transfer tokens', () => { + cy.get('#createToken').click() + cy.deployToken().then(() => { + cy.get('#transferTokensWithoutGas').click() + cy.confirmTransaction() + }) + }) +}) + +describe('using custom transaction form', () => { + it('should send defined amount', () => { + cy.get('#toInput').type('0x70997970C51812dc3A010C7d01b50e0d17dc79C8') + cy.get('#amountInput').type('3') + cy.get('#gasInput').type('1000000000') + + cy.get('#submitForm').click() + cy.confirmTransaction() + }) +}) diff --git a/wallets/metamask/test/cypress/rejectAddNetwork.cy.ts b/wallets/metamask/test/cypress/rejectAddNetwork.cy.ts new file mode 100644 index 00000000..1e23a418 --- /dev/null +++ b/wallets/metamask/test/cypress/rejectAddNetwork.cy.ts @@ -0,0 +1,18 @@ +it('should reject new network request', () => { + cy.createAnvilNode({ + chainId: 1338, + port: 8546 + }).then(() => { + cy.get('#addEthereumChain').click() + + cy.rejectNewNetwork().then(() => { + cy.get('#chainId').should('have.text', '0x1') + + cy.emptyAnvilNode() + }) + }) +}) + +after(() => { + cy.switchNetwork('Ethereum Mainnet') +}) diff --git a/wallets/metamask/test/cypress/rejectPermission.cy.ts b/wallets/metamask/test/cypress/rejectPermission.cy.ts new file mode 100644 index 00000000..062146ee --- /dev/null +++ b/wallets/metamask/test/cypress/rejectPermission.cy.ts @@ -0,0 +1,10 @@ +it('should reject approve request', () => { + cy.get('#tokenAddresses').should('be.empty') + cy.get('#createToken').click() + + cy.confirmTransaction().then(() => { + cy.get('#approveTokens').click() + + cy.rejectTokenPermission() + }) +}) diff --git a/wallets/metamask/test/cypress/rejectSignature.cy.ts b/wallets/metamask/test/cypress/rejectSignature.cy.ts new file mode 100644 index 00000000..7def5160 --- /dev/null +++ b/wallets/metamask/test/cypress/rejectSignature.cy.ts @@ -0,0 +1,44 @@ +it('should reject `personal_sign`', () => { + cy.get('#personalSign').click() + + cy.rejectSignature() + + cy.get('#personalSign').should( + 'have.text', + 'Error: MetaMask Personal Message Signature: User denied message signature.' + ) + cy.get('#personalSignResult').should('have.text', '') +}) + +it('should reject `eth_signTypedData`', () => { + cy.get('#signTypedData').click() + + cy.rejectSignature() + + cy.get('#signTypedDataResult').should( + 'have.text', + 'Error: MetaMask Typed Message Signature: User denied message signature.' + ) +}) + +it('should reject `eth_signTypedData_v3`', () => { + cy.get('#signTypedDataV3').click() + + cy.rejectSignature() + + cy.get('#signTypedDataV3Result').should( + 'have.text', + 'Error: MetaMask Typed Message Signature: User denied message signature.' + ) +}) + +it('should reject `eth_signTypedData_v4`', () => { + cy.get('#signTypedDataV4').click() + + cy.rejectSignature() + + cy.get('#signTypedDataV4Result').should( + 'have.text', + 'Error: MetaMask Typed Message Signature: User denied message signature.' + ) +}) diff --git a/wallets/metamask/test/cypress/rejectSwitchNetwork.cy.ts b/wallets/metamask/test/cypress/rejectSwitchNetwork.cy.ts new file mode 100644 index 00000000..b3fe24c8 --- /dev/null +++ b/wallets/metamask/test/cypress/rejectSwitchNetwork.cy.ts @@ -0,0 +1,11 @@ +it('should reject switch network request', () => { + cy.switchNetwork('Ethereum Mainnet').then(() => { + cy.get('#chainId').should('have.text', '0x1') + + cy.get('#switchEthereumChain').click() + + cy.rejectSwitchNetwork() + + cy.get('#chainId').should('have.text', '0x1') + }) +}) diff --git a/wallets/metamask/test/cypress/rejectTransaction.cy.ts b/wallets/metamask/test/cypress/rejectTransaction.cy.ts new file mode 100644 index 00000000..60d01106 --- /dev/null +++ b/wallets/metamask/test/cypress/rejectTransaction.cy.ts @@ -0,0 +1,8 @@ +it('should reject contract deployment', () => { + cy.get('#tokenAddresses').should('be.empty') + cy.get('#createToken').click() + + cy.rejectTransaction().then(() => { + cy.get('#tokenAddresses').should('have.text', 'Creation Failed') + }) +})