diff --git a/cypress/e2e/pages/batches.pages.js b/cypress/e2e/pages/batches.pages.js new file mode 100644 index 0000000000..f614fb93ef --- /dev/null +++ b/cypress/e2e/pages/batches.pages.js @@ -0,0 +1,98 @@ +const tokenSelectorText = 'G(ö|oe)rli Ether' +const noLaterString = 'No, later' +const yesExecuteString = 'Yes, execute' +const newTransactionTitle = 'New transaction' +const sendTokensButn = 'Send tokens' +const nextBtn = 'Next' +const executeBtn = 'Execute' +const addToBatchBtn = 'Add to batch' +const confirmBatchBtn = 'Confirm batch' + +export const closeModalBtnBtn = '[data-testid="CloseIcon"]' +export const deleteTransactionbtn = '[title="Delete transaction"]' +export const batchTxTopBar = '[data-track="batching: Batch sidebar open"]' +export const batchTxCounter = '[data-track="batching: Batch sidebar open"] span > span' +export const addNewTxBatch = '[data-track="batching: Add new tx to batch"]' +export const batchedTransactionsStr = 'Batched transactions' +export const addInitialTransactionStr = 'Add an initial transaction to the batch' +export const transactionAddedToBatchStr = 'Transaction is added to batch' +export const addNewStransactionStr = 'Add new transaction' + +const recipientInput = 'input[name="recipient"]' +const tokenAddressInput = 'input[name="tokenAddress"]' +const listBox = 'ul[role="listbox"]' +const amountInput = '[name="amount"]' +const nonceInput = 'input[name="nonce"]' + +export function addToBatch(EOA, currentNonce, amount, verify = false) { + fillTransactionData(EOA, amount) + setNonceAndProceed(currentNonce) + // Execute the transaction if verification is required + if (verify) { + executeTransaction() + } + addToBatchButton() +} + +function fillTransactionData(EOA, amount) { + cy.get(recipientInput).type(EOA, { delay: 1 }) + // Click on the Token selector + cy.get(tokenAddressInput).prev().click() + cy.get(listBox).contains(new RegExp(tokenSelectorText)).click() + cy.get(amountInput).type(amount) + cy.contains(nextBtn).click() +} + +function setNonceAndProceed(currentNonce) { + cy.get(nonceInput).clear().type(currentNonce, { force: true }).type('{enter}', { force: true }) + cy.contains(executeBtn).scrollIntoView() +} + +function executeTransaction() { + cy.contains(yesExecuteString, { timeout: 4000 }).click() + cy.contains(addToBatchBtn).should('not.exist') +} + +function addToBatchButton() { + cy.contains(noLaterString, { timeout: 4000 }).click() + cy.contains(addToBatchBtn).should('be.visible').and('not.be.disabled').click() +} + +export function openBatchtransactionsModal() { + cy.get(batchTxTopBar).should('be.visible').click() + cy.contains(batchedTransactionsStr).should('be.visible') + cy.contains(addInitialTransactionStr) +} + +export function openNewTransactionModal() { + cy.get(addNewTxBatch).click() + cy.contains('h1', newTransactionTitle).should('be.visible') + cy.contains(sendTokensButn).click() +} + +export function verifyAmountTransactionsInBatch(count) { + cy.contains(batchedTransactionsStr, { timeout: 7000 }) + .should('be.visible') + .parents('aside') + .find('ul > li') + .should('have.length', count) +} + +export function clickOnConfirmBatchBtn() { + cy.contains(confirmBatchBtn).click() +} + +export function verifyBatchTransactionsCount(count) { + cy.contains(`This batch contains ${count} transactions`).should('be.visible') +} + +export function clickOnBatchCounter() { + cy.get(batchTxCounter).click() +} +export function verifyTransactionAdded() { + cy.contains(transactionAddedToBatchStr).should('be.visible') +} + +export function verifyBatchIconCount(count) { + cy.get(batchTxCounter).contains(count) +} diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js new file mode 100644 index 0000000000..fc2a0905be --- /dev/null +++ b/cypress/e2e/pages/create_tx.pages.js @@ -0,0 +1,151 @@ +import * as constants from '../../support/constants' + +const newTransactionBtnStr = 'New transaction' +const recepientInput = 'input[name="recipient"]' +const sendTokensBtnStr = 'Send tokens' +const tokenAddressInput = 'input[name="tokenAddress"]' +const amountInput = 'input[name="amount"]' +const nonceInput = 'input[name="nonce"]' +const gasLimitInput = '[name="gasLimit"]' +const rotateLeftIcon = '[data-testid="RotateLeftIcon"]' + +const viewTransactionBtn = 'View transaction' +const transactionDetailsTitle = 'Transaction details' +const QueueLabel = 'needs to be executed first' +const TransactionSummary = 'Send-' + +const maxAmountBtnStr = 'Max' +const nextBtnStr = 'Next' +const nativeTokenTransferStr = 'Native token transfer' +const yesStr = 'Yes, ' +const estimatedFeeStr = 'Estimated fee' +const executeStr = 'Execute' +const transactionsPerHrStr = 'Transactions per hour' +const transactionsPerHr5Of5Str = '5 of 5' +const editBtnStr = 'Edit' +const executionParamsStr = 'Execution parameters' +const noLaterStr = 'No, later' +const signBtnStr = 'Sign' + +export function clickOnNewtransactionBtn() { + // Assert that "New transaction" button is visible + cy.contains(newTransactionBtnStr, { + timeout: 60_000, // `lastWallet` takes a while initialize in CI + }) + .should('be.visible') + .and('not.be.disabled') + + // Open the new transaction modal + cy.contains(newTransactionBtnStr).click() + cy.contains('h1', newTransactionBtnStr).should('be.visible') +} + +export function typeRecipientAddress(address) { + cy.get(recepientInput).type(address).should('have.value', address) +} +export function clickOnSendTokensBtn() { + cy.contains(sendTokensBtnStr).click() +} + +export function clickOnTokenselectorAndSelectGoerli() { + cy.get(tokenAddressInput).prev().click() + cy.get('ul[role="listbox"]').contains(constants.goerliToken).click() +} + +export function setMaxAmount() { + cy.contains(maxAmountBtnStr).click() +} + +export function verifyMaxAmount(token, tokenAbbreviation) { + cy.get(tokenAddressInput) + .prev() + .find('p') + .contains(token) + .next() + .then((element) => { + const maxBalance = element.text().replace(tokenAbbreviation, '').trim() + cy.get(amountInput).should('have.value', maxBalance) + console.log(maxBalance) + }) +} + +export function setSendValue(value) { + cy.get(amountInput).clear().type(value) +} + +export function clickOnNextBtn() { + cy.contains(nextBtnStr).click() +} + +export function verifySubmitBtnIsEnabled() { + cy.get('button[type="submit"]').should('not.be.disabled') +} + +export function verifyNativeTokenTransfer() { + cy.contains(nativeTokenTransferStr).should('be.visible') +} + +export function changeNonce(value) { + cy.get(nonceInput).clear().type(value, { force: true }).type('{enter}', { force: true }) +} + +export function verifyConfirmTransactionData() { + cy.contains(yesStr).should('exist') + cy.contains(estimatedFeeStr).should('exist') + + // Asserting the sponsored info is present + cy.contains(executeStr).scrollIntoView().should('be.visible') + + cy.get('span').contains(estimatedFeeStr).next().should('have.css', 'text-decoration-line', 'line-through') + cy.contains(transactionsPerHrStr) + cy.contains(transactionsPerHr5Of5Str) +} + +export function openExecutionParamsModal() { + cy.contains(estimatedFeeStr).click() + cy.contains(editBtnStr).click() +} + +export function verifyAndSubmitExecutionParams() { + cy.contains(executionParamsStr).parents('form').as('Paramsform') + + // Only gaslimit should be editable when the relayer is selected + const arrayNames = ['Wallet nonce', 'Max priority fee (Gwei)', 'Max fee (Gwei)'] + arrayNames.forEach((element) => { + cy.get('@Paramsform').find('label').contains(`${element}`).next().find('input').should('be.disabled') + }) + + cy.get('@Paramsform').find(gasLimitInput).clear().type('300000').invoke('prop', 'value').should('equal', '300000') + cy.get('@Paramsform').find(gasLimitInput).parent('div').find(rotateLeftIcon).click() + cy.get('@Paramsform').submit() +} + +export function clickOnNoLaterOption() { + // Asserts the execute checkbox is uncheckable (???) + cy.contains(noLaterStr).click() +} + +export function clickOnSignTransactionBtn() { + cy.contains(signBtnStr).click() +} + +export function waitForProposeRequest() { + cy.intercept('POST', constants.proposeEndPoint).as('ProposeTx') + cy.wait('@ProposeTx') +} + +export function clickViewTransaction() { + cy.contains(viewTransactionBtn).click() +} + +export function verifySingleTxPage() { + cy.get('h3').contains(transactionDetailsTitle).should('be.visible') +} + +export function verifyQueueLabel() { + cy.contains(QueueLabel).should('be.visible') +} + +export function verifyTransactionSummary(sendValue) { + cy.contains(TransactionSummary + `${sendValue} ${constants.tokenAbbreviation.gor}`).should('exist') +} diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js new file mode 100644 index 0000000000..57ad95b318 --- /dev/null +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -0,0 +1,89 @@ +import * as constants from '../../support/constants' + +const newAccountBtnStr = 'Create new Account' + +const nameInput = 'input[name="name"]' +const selectNetworkBtn = '[data-cy="create-safe-select-network"]' +const ownerInput = 'input[name^="owners"][name$="name"]' +const ownerAddress = 'input[name^="owners"][name$="address"]' +const thresholdInput = 'input[name="threshold"]' +const removeOwnerBtn = 'button[aria-label="Remove owner"]' + +export function clickOnCreateNewAccuntBtn() { + cy.contains(newAccountBtnStr).click() +} + +export function typeWalletName(name) { + cy.get(nameInput).should('have.attr', 'placeholder').should('match', constants.goerlySafeName) + cy.get(nameInput).type(name).should('have.value', name) +} + +export function selectNetwork(network, regex = false) { + cy.get(selectNetworkBtn).click() + cy.contains(network).click() + + if (regex) { + regex = constants.networks.goerli + cy.get(selectNetworkBtn).click().invoke('text').should('match', regex) + } else { + cy.get(selectNetworkBtn).click().should('have.text', network) + } + cy.get('body').click() +} + +export function clickOnNextBtn() { + cy.contains('button', 'Next').click() +} + +export function verifyOwnerName(name, index) { + cy.get(ownerInput).eq(index).should('have.value', name) +} + +export function verifyOwnerAddress(address, index) { + cy.get(ownerAddress).eq(index).should('have.value', address) +} + +export function verifyThreshold(number) { + cy.get(thresholdInput).should('have.value', number) +} + +export function typeOwnerName(name, index) { + cy.get(getOwnerNameInput(index)).type(name).should('have.value', name) +} + +function typeOwnerAddress(address, index) { + cy.get(getOwnerAddressInput(index)).type(address).should('have.value', address) +} + +function clickOnAddNewOwnerBtn() { + cy.contains('button', 'Add new owner').click() +} + +export function addNewOwner(name, address, index) { + clickOnAddNewOwnerBtn() + typeOwnerName(name, index) + typeOwnerAddress(address, index) +} + +export function updateThreshold(number) { + cy.get(thresholdInput).parent().click() + cy.contains('li', number).click() +} + +export function removeOwner(index) { + cy.get(removeOwnerBtn).eq(index).click() +} + +export function verifySummaryData(safeName, ownerAddress, startThreshold, endThreshold) { + cy.contains(safeName) + cy.contains(ownerAddress) + cy.contains(`${startThreshold} out of ${endThreshold}`) +} + +function getOwnerNameInput(index) { + return `input[name="owners.${index}.name"]` +} + +function getOwnerAddressInput(index) { + return `input[name="owners.${index}.address"]` +} diff --git a/cypress/e2e/pages/dashboard.pages.js b/cypress/e2e/pages/dashboard.pages.js new file mode 100644 index 0000000000..a2d90a3984 --- /dev/null +++ b/cypress/e2e/pages/dashboard.pages.js @@ -0,0 +1,87 @@ +import * as constants from '../../support/constants' + +const connectAndTransactStr = 'Connect & transact' +const transactionQueueStr = 'Transaction queue' +const noTransactionStr = 'This Safe has no queued transactions' +const overviewStr = 'Overview' +const viewAssetsStr = 'View assets' +const tokensStr = 'Tokens' +const nftStr = 'NFTs' +const viewAllStr = 'View all' +const transactionBuilderStr = 'Use Transaction Builder' +const walletConnectStr = 'Use WalletConnect' +const safeAppStr = 'Safe Apps' +const exploreSafeApps = 'Explore Safe Apps' + +const txBuilder = 'a[href*="tx-builder"]' +const walletConnect = 'a[href*="wallet-connect"]' +const safeSpecificLink = 'a[href*="&appUrl=http"]' + +export function verifyConnectTransactStrIsVisible() { + cy.contains(connectAndTransactStr).should('be.visible') +} + +export function verifyOverviewWidgetData() { + // Alias for the Overview section + cy.contains('h2', overviewStr).parents('section').as('overviewSection') + + cy.get('@overviewSection').within(() => { + // Prefix is separated across elements in EthHashInfo + cy.contains(constants.TEST_SAFE).should('exist') + cy.contains('1/2') + cy.get(`a[href="${constants.BALANCE_URL}${encodeURIComponent(constants.TEST_SAFE)}"]`).contains(viewAssetsStr) + // Text next to Tokens contains a number greater than 0 + cy.contains('p', tokensStr).next().contains('1') + cy.contains('p', nftStr).next().contains('0') + }) +} + +export function verifyTxQueueWidget() { + // Alias for the Transaction queue section + cy.contains('h2', transactionQueueStr).parents('section').as('txQueueSection') + + cy.get('@txQueueSection').within(() => { + // There should be queued transactions + cy.contains(noTransactionStr).should('not.exist') + + // Queued txns + cy.contains(`a[href^="/transactions/tx?id=multisig_0x"]`, '13' + 'Send' + '-0.00002 GOR' + '1/1').should('exist') + + cy.contains(`a[href="${constants.transactionQueueUrl}${encodeURIComponent(constants.TEST_SAFE)}"]`, viewAllStr) + }) +} + +export function verifyFeaturedAppsSection() { + // Alias for the featured Safe Apps section + cy.contains('h2', connectAndTransactStr).parents('section').as('featuredSafeAppsSection') + + // Tx Builder app + cy.get('@featuredSafeAppsSection').within(() => { + // Transaction Builder + cy.contains(transactionBuilderStr) + cy.get(txBuilder).should('exist') + + // WalletConnect app + cy.contains(walletConnectStr) + cy.get(walletConnect).should('exist') + + // Featured apps have a Safe-specific link + cy.get(safeSpecificLink).should('have.length', 2) + }) +} + +export function verifySafeAppsSection() { + // Create an alias for the Safe Apps section + cy.contains('h2', safeAppStr).parents('section').as('safeAppsSection') + + cy.get('@safeAppsSection').contains(exploreSafeApps) + + // Regular safe apps + cy.get('@safeAppsSection').within(() => { + // Find exactly 5 Safe Apps cards inside the Safe Apps section + cy.get(`a[href^="${constants.openAppsUrl}${encodeURIComponent(constants.TEST_SAFE)}&appUrl=http"]`).should( + 'have.length', + 5, + ) + }) +} diff --git a/cypress/e2e/pages/import_export.pages.js b/cypress/e2e/pages/import_export.pages.js new file mode 100644 index 0000000000..2a0c82fd52 --- /dev/null +++ b/cypress/e2e/pages/import_export.pages.js @@ -0,0 +1,104 @@ +import { format } from 'date-fns' +const path = require('path') + +const addressBookBtnStr = 'Address book' +const dataImportModalStr = 'Data import' +const appsBtnStr = 'Apps' +const bookmarkedAppsBtnStr = 'Bookmarked apps' +const settingsBtnStr = 'Settings' +const appearenceTabStr = 'Appearance' +const dataTabStr = 'Data' +const tab = 'div[role="tablist"] a' +export const prependChainPrefixStr = 'Prepend chain prefix to addresses' +export const copyAddressStr = 'Copy addresses with chain prefix' +export const darkModeStr = 'Dark mode' + +export function verifyImportBtnIsVisible() { + cy.contains('button', 'Import').should('be.visible') +} +export function clickOnImportBtn() { + verifyImportBtnIsVisible() + cy.contains('button', 'Import').click() +} + +export function clickOnImportBtnDataImportModal() { + cy.contains(dataImportModalStr).parent().contains('button', 'Import').click() +} + +export function uploadFile(filePath) { + cy.get('[type="file"]').attachFile(filePath) +} + +export function verifyImportModalData() { + //verifies that the modal says the amount of chains/addressbook values it uploaded for file ../fixtures/data_import.json + cy.contains('Added Safe Accounts on 3 chains').should('be.visible') + cy.contains('Address book for 3 chains').should('be.visible') + cy.contains('Settings').should('be.visible') + cy.contains('Bookmarked Safe Apps').should('be.visible') +} + +export function clickOnImportedSafe(safe) { + cy.contains(safe).click() +} + +export function clickOnAddressBookBtn() { + cy.contains(addressBookBtnStr).click() +} + +export function verifyImportedAddressBookData() { + //Verifies imported owners in the Address book for file ../fixtures/data_import.json + cy.get('tbody tr:nth-child(1) td:nth-child(1)').contains('test1') + cy.get('tbody tr:nth-child(1) td:nth-child(2)').contains('0x61a0c717d18232711bC788F19C9Cd56a43cc8872') + cy.get('tbody tr:nth-child(2) td:nth-child(1)').contains('test2') + cy.get('tbody tr:nth-child(2) td:nth-child(2)').contains('0x7724b234c9099C205F03b458944942bcEBA13408') +} + +export function clickOnAppsBtn() { + cy.get('aside').contains('li', appsBtnStr).click() +} + +export function clickOnBookmarkedAppsBtn() { + cy.contains(bookmarkedAppsBtnStr).click() + //Takes a some time to load the apps page, It waits for bookmark to be lighted up + cy.waitForSelector(() => { + return cy + .get('[aria-selected="true"] p') + .invoke('html') + .then((text) => text === 'Bookmarked apps') + }) +} + +export function verifyAppsAreVisible(appNames) { + appNames.forEach((appName) => { + cy.contains(appName).should('be.visible') + }) +} + +export function clickOnSettingsBtn() { + cy.contains(settingsBtnStr).click() +} + +export function clickOnAppearenceBtn() { + cy.contains(tab, appearenceTabStr).click() +} + +export function clickOnDataTab() { + cy.contains(tab, dataTabStr).click() +} + +export function verifyCheckboxes(checkboxes, checked = false) { + checkboxes.forEach((checkbox) => { + cy.contains('label', checkbox) + .find('input[type="checkbox"]') + .should(checked ? 'be.checked' : 'not.be.checked') + }) +} + +export function verifyFileDownload() { + const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) + const fileName = `safe-${date}.json` + cy.contains('div', fileName).next().click() + const downloadsFolder = Cypress.config('downloadsFolder') + //File reading is failing in the CI. Can be tested locally + cy.readFile(path.join(downloadsFolder, fileName)).should('exist') +} diff --git a/cypress/e2e/pages/load_safe.pages.js b/cypress/e2e/pages/load_safe.pages.js new file mode 100644 index 0000000000..2f5b22e618 --- /dev/null +++ b/cypress/e2e/pages/load_safe.pages.js @@ -0,0 +1,111 @@ +import * as constants from '../../support/constants' + +const addExistingAccountBtnStr = 'Add existing Account' +const contactStr = 'Name, address & network' +const invalidAddressFormatErrorMsg = 'Invalid address format' + +const safeDataForm = '[data-testid=load-safe-form]' +const nameInput = 'input[name="name"]' +const addressInput = 'input[name="address"]' +const sideBarIcon = '[data-testid=ChevronRightIcon]' +const sidebarCheckIcon = '[data-testid=CheckIcon]' +const nextBtnStr = 'Next' +const addBtnStr = 'Add' +const settingsBtnStr = 'Settings' +const ownersConfirmationsStr = 'Owners and confirmations' +const transactionStr = 'Transactions' + +export function openLoadSafeForm() { + cy.contains('button', addExistingAccountBtnStr).click() + cy.contains(contactStr) +} + +export function clickNetworkSelector(networkName) { + cy.get(safeDataForm).contains(networkName).click() +} + +export function selectGoerli() { + cy.get('ul li').contains(constants.networks.goerli).click() + cy.contains('span', constants.networks.goerli) +} + +export function verifyNameInputHasPlceholder() { + cy.get(nameInput).should('have.attr', 'placeholder').should('match', constants.goerlySafeName) +} + +export function inputName(name) { + cy.get(nameInput).type(name).should('have.value', name) +} + +export function verifyIncorrectAddressErrorMessage() { + inputAddress('Random text') + cy.get(addressInput).parent().prev('label').contains(invalidAddressFormatErrorMsg) +} + +export function inputAddress(address) { + cy.get(addressInput).clear().type(address) +} + +export function verifyAddressInputValue() { + // The address field should be filled with the "bare" QR code's address + const [, address] = constants.GOERLI_TEST_SAFE.split(':') + cy.get('input[name="address"]').should('have.value', address) +} + +export function clickOnNextBtn() { + cy.contains(nextBtnStr).click() +} + +export function verifyDataInReviewSection(safeName, ownerName) { + cy.findByText(safeName).should('be.visible') + cy.findByText(ownerName).should('be.visible') +} + +export function clickOnAddBtn() { + cy.contains('button', addBtnStr).click() +} + +export function veriySidebarSafeNameIsVisible(safeName) { + cy.get('aside').contains(safeName).should('be.visible') +} + +export function verifyOwnerNamePresentInSettings(ownername) { + clickOnSettingsBtn() + cy.contains(ownername).should('exist') +} + +function clickOnSettingsBtn() { + cy.get('aside ul').contains(settingsBtnStr).click() +} + +export function verifyOwnersModalIsVisible() { + cy.contains(ownersConfirmationsStr).should('be.visible') +} + +export function openSidebar() { + cy.get('aside').within(() => { + cy.get(sideBarIcon).click({ force: true }) + }) +} + +export function verifyAddressInsidebar(address) { + cy.get('li').within(() => { + cy.contains(address).should('exist') + }) +} + +export function verifySidebarIconNumber(number) { + cy.get(sidebarCheckIcon).next().contains(number).should('exist') +} + +export function clickOnPendingActions() { + cy.get(sidebarCheckIcon).next().click() +} + +export function verifyTransactionSectionIsVisible() { + cy.contains('h3', transactionStr).should('be.visible') +} + +export function verifyNumberOfTransactions(startNumber, endNumber) { + cy.get(`span:contains("${startNumber} out of ${endNumber}")`).should('have.length', 1) +} diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 1d7d2c24ef..c4c628e053 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -1,7 +1,17 @@ +import * as constants from '../../support/constants' + const acceptSelection = 'Accept selection' -const gotItBtn = 'Got it' export function acceptCookies() { cy.contains(acceptSelection).click() cy.contains(acceptSelection).should('not.exist') + cy.wait(500) +} + +export function verifyGoerliWalletHeader() { + cy.contains(constants.goerlyE2EWallet) +} + +export function verifyHomeSafeUrl(safe) { + cy.location('href', { timeout: 10000 }).should('include', constants.homeUrl + safe) } diff --git a/cypress/e2e/pages/nfts.pages.js b/cypress/e2e/pages/nfts.pages.js new file mode 100644 index 0000000000..185224b70f --- /dev/null +++ b/cypress/e2e/pages/nfts.pages.js @@ -0,0 +1,116 @@ +import * as constants from '../../support/constants' + +const nftModal = 'div[role="dialog"]' +const nftModalCloseBtn = 'button[aria-label="close"]' +const recipientInput = 'input[name="recipient"]' + +const noneNFTSelected = '0 NFTs selected' +const sendNFTStr = 'Send NFTs' +const recipientAddressStr = 'Recipient address or ENS' +const selectedNFTStr = 'Selected NFTs' +const executeBtnStr = 'Execute' +const nextBtnStr = 'Next' +const sendStr = 'Send' +const toStr = 'To' +const transferFromStr = 'safeTransferFrom' + +function verifyTableRows(number) { + cy.get('tbody tr').should('have.length', number) +} + +export function verifyNFTNumber(number) { + verifyTableRows(number) +} + +export function verifyDataInTable(name, address, tokenID, link) { + cy.get('tbody tr:first-child').contains('td:first-child', name) + cy.get('tbody tr:first-child').contains('td:first-child', address) + cy.get('tbody tr:first-child').contains('td:nth-child(2)', tokenID) + cy.get(`tbody tr:first-child td:nth-child(3) a[href="${link}"]`) +} + +export function openFirstNFT() { + cy.get('tbody tr:first-child td:nth-child(2)').click() +} + +export function verifyNameInNFTModal(name) { + cy.get(nftModal).contains(name) +} + +export function preventBaseMainnetGoerliFromBeingSelected() { + cy.get(nftModal).contains(constants.networks.goerli) +} + +export function verifyNFTModalLink(link) { + cy.get(nftModal).contains(`a[href="${link}"]`, 'View on OpenSea') +} + +export function closeNFTModal() { + cy.get(nftModalCloseBtn).click() + cy.get(nftModal).should('not.exist') +} + +export function clickOnThirdNFT() { + cy.get('tbody tr:nth-child(3) td:nth-child(2)').click() +} +export function verifyNFTModalDoesNotExist() { + cy.get(nftModal).should('not.exist') +} + +export function selectNFTs(numberOfNFTs) { + for (let i = 1; i <= numberOfNFTs; i++) { + cy.get(`tbody tr:nth-child(${i}) input[type="checkbox"]`).click() + cy.contains(`${i} NFT${i > 1 ? 's' : ''} selected`) + } + cy.contains('button', `Send ${numberOfNFTs} NFT${numberOfNFTs > 1 ? 's' : ''}`) +} + +export function deselectNFTs(checkboxIndexes, checkedItems) { + let total = checkedItems - checkboxIndexes.length + + checkboxIndexes.forEach((index) => { + cy.get(`tbody tr:nth-child(${index}) input[type="checkbox"]`).uncheck() + }) + + cy.contains(`${total} NFT${total !== 1 ? 's' : ''} selected`) + if (total === 0) { + verifyInitialNFTData() + } +} + +export function verifyInitialNFTData() { + cy.contains(noneNFTSelected) + cy.contains('button[disabled]', 'Send') +} + +export function sendNFT(numberOfCheckedNFTs) { + cy.contains('button', `Send ${numberOfCheckedNFTs} NFT${numberOfCheckedNFTs !== 1 ? 's' : ''}`).click() +} + +export function verifyNFTModalData() { + cy.contains(sendNFTStr) + cy.contains(recipientAddressStr) + cy.contains(selectedNFTStr) +} + +export function typeRecipientAddress(address) { + cy.get(recipientInput).type(address) +} + +export function clikOnNextBtn() { + cy.contains('button', nextBtnStr).click() +} + +export function verifyReviewModalData(NFTcount) { + cy.contains(sendStr) + cy.contains(toStr) + cy.wait(1000) + cy.get(`b:contains(${transferFromStr})`).should('have.length', NFTcount) + cy.contains('button:not([disabled])', executeBtnStr) + if (NFTcount > 1) { + const numbersArr = Array.from({ length: NFTcount }, (_, index) => index + 1) + numbersArr.forEach((number) => { + cy.contains(number.toString()).should('be.visible') + }) + } +} diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index 83ebd9b6d2..a410ae6986 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -1,46 +1,42 @@ -const SAFE = 'gor:0x04f8b1EA3cBB315b87ced0E32deb5a43cC151a91' -const EOA = '0xE297437d6b53890cbf004e401F3acc67c8b39665' +import * as batch from '../pages/batches.pages' +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' const currentNonce = 3 -const BATCH_TX_TOPBAR = '[data-track="batching: Batch sidebar open"]' -const BATCH_TX_COUNTER = '[data-track="batching: Batch sidebar open"] span > span' -const ADD_NEW_TX_BATCH = '[data-track="batching: Add new tx to batch"]' const funds_first_tx = '0.001' const funds_second_tx = '0.002' -var transactionsInBatchList = 0 describe('Create batch transaction', () => { before(() => { - cy.visit(`/home?safe=${SAFE}`) - cy.contains('Accept selection').click() - - cy.contains(/E2E Wallet @ G(ö|oe)rli/, { timeout: 10000 }) + cy.visit(constants.homeUrl + constants.TEST_SAFE) + main.acceptCookies() + cy.contains(constants.goerlyE2EWallet, { timeout: 10000 }) }) it('Should open an empty batch list', () => { - cy.get(BATCH_TX_TOPBAR).should('be.visible').click() - cy.contains('Batched transactions').should('be.visible') - cy.contains('Add an initial transaction to the batch') - cy.get(ADD_NEW_TX_BATCH).click() + batch.openBatchtransactionsModal() + batch.openNewTransactionModal() }) it('Should see the add batch button in a transaction form', () => { //The "true" is to validate that the add to batch button is not visible if "Yes, execute" is selected - addToBatch(EOA, currentNonce, funds_first_tx, true) + batch.addToBatch(constants.EOA, currentNonce, funds_first_tx, true) }) it('Should see the transaction being added to the batch', () => { - cy.contains('Transaction is added to batch').should('be.visible') + cy.contains(batch.transactionAddedToBatchStr).should('be.visible') //The batch button in the header shows the transaction count - cy.get(BATCH_TX_COUNTER).contains('1').click() - amountTransactionsInBatch(1) + batch.verifyBatchIconCount(1) + batch.clickOnBatchCounter() + batch.verifyAmountTransactionsInBatch(1) }) it('Should add a second transaction to the batch', () => { - cy.contains('Add new transaction').click() - addToBatch(EOA, currentNonce, funds_second_tx) - cy.get(BATCH_TX_COUNTER).contains('2').click() - amountTransactionsInBatch(2) + batch.openNewTransactionModal() + batch.addToBatch(constants.EOA, currentNonce, funds_second_tx) + batch.verifyBatchIconCount(2) + batch.clickOnBatchCounter() + batch.verifyAmountTransactionsInBatch(2) }) it.skip('Should swap transactions order', () => { @@ -48,53 +44,18 @@ describe('Create batch transaction', () => { }) it('Should confirm the batch and see 2 transactions in the form', () => { - cy.contains('Confirm batch').click() - cy.contains(`This batch contains ${transactionsInBatchList} transactions`).should('be.visible') + batch.clickOnConfirmBatchBtn() + batch.verifyBatchTransactionsCount(2) cy.contains(funds_first_tx).parents('ul').as('TransactionList') cy.get('@TransactionList').find('li').eq(0).find('span').eq(0).contains(funds_first_tx) cy.get('@TransactionList').find('li').eq(1).find('span').eq(0).contains(funds_second_tx) }) it('Should remove a transaction from the batch', () => { - cy.get(BATCH_TX_COUNTER).click() - cy.contains('Batched transactions').should('be.visible').parents('aside').find('ul > li').as('BatchList') - cy.get('@BatchList').find('[title="Delete transaction"]').eq(0).click() + batch.clickOnBatchCounter() + cy.contains(batch.batchedTransactionsStr).should('be.visible').parents('aside').find('ul > li').as('BatchList') + cy.get('@BatchList').find(batch.deleteTransactionbtn).eq(0).click() cy.get('@BatchList').should('have.length', 1) cy.get('@BatchList').contains(funds_first_tx).should('not.exist') }) }) - -const amountTransactionsInBatch = (count) => { - cy.contains('Batched transactions', { timeout: 7000 }) - .should('be.visible') - .parents('aside') - .find('ul > li') - .should('have.length', count) - transactionsInBatchList = count -} - -const addToBatch = (EOA, currentNonce, amount, verify = false) => { - // Modal is open - cy.contains('h1', 'New transaction').should('be.visible') - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(EOA, { delay: 1 }) - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]') - - .contains(/G(ö|oe)rli Ether/) - .click() - cy.get('[name="amount"]').type(amount) - cy.contains('Next').click() - cy.get('input[name="nonce"]').clear().type(currentNonce, { force: true }).type('{enter}', { force: true }) - cy.contains('Execute').scrollIntoView() - //Only validates the button not showing once in the entire run - if (verify) { - cy.contains('Yes, execute', { timeout: 4000 }).click() - cy.contains('Add to batch').should('not.exist') - } - cy.contains('No, later', { timeout: 4000 }).click() - cy.contains('Add to batch').should('be.visible').and('not.be.disabled').click() -} diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 9b248c84df..da63618b5e 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -1,81 +1,53 @@ import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' + +const safeName = 'Test safe name' +const ownerName = 'Test Owner Name' +const ownerName2 = 'Test Owner Name 2' describe('Create Safe form', () => { it('should navigate to the form', () => { - cy.visit('/welcome') - - // Close cookie banner - cy.contains('button', 'Accept selection').click() - - // Ensure wallet is connected to correct chain via header - cy.contains(/E2E Wallet @ G(ö|oe)rli/) - - cy.contains('Create new Account').click() + cy.visit(constants.welcomeUrl) + main.acceptCookies() + main.verifyGoerliWalletHeader() + createwallet.clickOnCreateNewAccuntBtn() }) it('should allow setting a name', () => { - // Name input should have a placeholder ending in 'goerli-safe' - cy.get('input[name="name"]') - .should('have.attr', 'placeholder') - .should('match', /g(ö|oe)rli-safe/) - - // Input a custom name - cy.get('input[name="name"]').type('Test safe name').should('have.value', 'Test safe name') + createwallet.typeWalletName(safeName) }) it('should allow changing the network', () => { - // Switch to a different network - cy.get('[data-cy="create-safe-select-network"]').click() - cy.contains('Ethereum').click() - - // Switch back to Görli - cy.get('[data-cy="create-safe-select-network"]').click() - - // Prevent Base Mainnet Goerli from being selected - cy.contains('li span', /^G(ö|oe)rli$/).click() - - cy.contains('button', 'Next').click() + createwallet.selectNetwork(constants.networks.ethereum) + createwallet.selectNetwork(constants.networks.goerli, true) + createwallet.clickOnNextBtn() }) it('should display a default owner and threshold', () => { - // Default owner - cy.get('input[name="owners.0.address"]').should('have.value', constants.DEFAULT_OWNER_ADDRESS) - - // Default threshold - cy.get('input[name="threshold"]').should('have.value', 1) + createwallet.verifyOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 0) + createwallet.verifyThreshold(1) }) it('should allow changing the owner name', () => { - cy.get('input[name="owners.0.name"]').type('Test Owner Name') + createwallet.typeOwnerName(ownerName, 0) cy.contains('button', 'Back').click() cy.contains('button', 'Next').click() - cy.get('input[name="owners.0.name"]').should('have.value', 'Test Owner Name') + createwallet.verifyOwnerName(ownerName, 0) }) it('should add a new owner and update threshold', () => { - // Add new owner - cy.contains('button', 'Add new owner').click() - cy.get('input[name="owners.1.address"]').should('exist') - cy.get('input[name="owners.1.address"]').type(constants.EOA) - - // Update threshold - cy.get('input[name="threshold"]').parent().click() - cy.contains('li', '2').click() + createwallet.addNewOwner(ownerName2, constants.EOA, 1) + createwallet.updateThreshold(2) }) it('should remove an owner and update threshold', () => { - // Remove owner - cy.get('button[aria-label="Remove owner"]').click() - - // Threshold should change back to 1 - cy.get('input[name="threshold"]').should('have.value', 1) - - cy.contains('button', 'Next').click() + createwallet.removeOwner(0) + createwallet.verifyThreshold(1) + createwallet.clickOnNextBtn() }) it('should display summary on review page', () => { - cy.contains('Test safe name') - cy.contains(constants.DEFAULT_OWNER_ADDRESS) - cy.contains('1 out of 1') + createwallet.verifySummaryData(safeName, constants.DEFAULT_OWNER_ADDRESS, 1, 1) }) }) diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index 659839bfdc..43c27090ca 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -1,126 +1,45 @@ import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as createtx from '../../e2e/pages/create_tx.pages' const sendValue = 0.00002 const currentNonce = 3 describe('Queue a transaction on 1/N', () => { before(() => { - cy.visit(`/home?safe=${constants.TEST_SAFE}`) - - cy.contains('Accept selection').click() + cy.visit(constants.homeUrl + constants.TEST_SAFE) + main.acceptCookies() }) it('should create and queue a transaction', () => { - // Assert that "New transaction" button is visible - cy.contains('New transaction', { - timeout: 60_000, // `lastWallet` takes a while initialize in CI - }) - .should('be.visible') - .and('not.be.disabled') - - // Open the new transaction modal - cy.contains('New transaction').click() - - // Modal is open - cy.contains('h1', 'New transaction').should('be.visible') - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(constants.EOA) - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]') - .contains(/G(ö|oe)rli Ether/) - .click() - - // Insert max amount - cy.contains('Max').click() - - // Validates the "Max" button action, then clears and sets the actual sendValue - cy.get('input[name="tokenAddress"]') - .prev() - .find('p') - .contains(/G(ö|oe)rli Ether/) - .next() - .then((element) => { - const maxBalance = element.text().replace(' GOR', '').trim() - cy.get('input[name="amount"]').should('have.value', maxBalance) - }) - - cy.get('input[name="amount"]').clear().type(sendValue) - - cy.contains('Next').click() + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() + createtx.typeRecipientAddress(constants.EOA) + createtx.clickOnTokenselectorAndSelectGoerli() + createtx.setMaxAmount() + createtx.verifyMaxAmount(constants.goerliToken, constants.tokenAbbreviation.gor) + createtx.setSendValue(sendValue) + createtx.clickOnNextBtn() }) it('should create a queued transaction', () => { - cy.get('button[type="submit"]').should('not.be.disabled') - + createtx.verifySubmitBtnIsEnabled() cy.wait(1000) - - cy.contains('Native token transfer').should('be.visible') - - // Changes nonce to next one - cy.get('input[name="nonce"]').clear().type(currentNonce, { force: true }).type('{enter}', { force: true }) - - // Execution - cy.contains('Yes, ').should('exist') - cy.contains('Estimated fee').should('exist') - - // Asserting the sponsored info is present - cy.contains('Execute').scrollIntoView().should('be.visible') - - cy.get('span').contains('Estimated fee').next().should('have.css', 'text-decoration-line', 'line-through') - cy.contains('Transactions per hour') - cy.contains('5 of 5') - - cy.contains('Estimated fee').click() - cy.contains('Edit').click() - cy.contains('Execution parameters').parents('form').as('Paramsform') - - // Only gaslimit should be editable when the relayer is selected - const arrayNames = ['Wallet nonce', 'Max priority fee (Gwei)', 'Max fee (Gwei)'] - arrayNames.forEach((element) => { - cy.get('@Paramsform').find('label').contains(`${element}`).next().find('input').should('be.disabled') - }) - - cy.get('@Paramsform') - .find('[name="gasLimit"]') - .clear() - .type('300000') - .invoke('prop', 'value') - .should('equal', '300000') - cy.get('@Paramsform').find('[name="gasLimit"]').parent('div').find('[data-testid="RotateLeftIcon"]').click() - - cy.get('@Paramsform').submit() - - // Asserts the execute checkbox is uncheckable - cy.contains('No, later').click() - - cy.get('input[name="nonce"]') - .clear({ force: true }) - .type(currentNonce + 10, { force: true }) - .type('{enter}', { force: true }) - - cy.contains('Sign').click() + createtx.verifyNativeTokenTransfer() + createtx.changeNonce(currentNonce) + createtx.verifyConfirmTransactionData() + createtx.openExecutionParamsModal() + createtx.verifyAndSubmitExecutionParams() + createtx.clickOnNoLaterOption() + createtx.changeNonce(13) + createtx.clickOnSignTransactionBtn() }) it('should click the notification and see the transaction queued', () => { - // Wait for the /propose request - cy.intercept('POST', '/**/propose').as('ProposeTx') - cy.wait('@ProposeTx') - - // Click on the notification - cy.contains('View transaction').click() - - //cy.contains('Queue').click() - - // Single Tx page - cy.contains('h3', 'Transaction details').should('be.visible') - - // Queue label - cy.contains(`needs to be executed first`).should('be.visible') - - // Transaction summary - cy.contains('Send' + '-' + `${sendValue} GOR`).should('exist') + createtx.waitForProposeRequest() + createtx.clickViewTransaction() + createtx.verifySingleTxPage() + createtx.verifyQueueLabel() + createtx.verifyTransactionSummary(sendValue) }) }) diff --git a/cypress/e2e/smoke/dashboard.cy.js b/cypress/e2e/smoke/dashboard.cy.js index 73f5a61bb3..88f4249aa5 100644 --- a/cypress/e2e/smoke/dashboard.cy.js +++ b/cypress/e2e/smoke/dashboard.cy.js @@ -1,78 +1,27 @@ import * as constants from '../../support/constants' +import * as dashboard from '../pages/dashboard.pages' +import * as main from '../pages/main.page' describe('Dashboard', () => { before(() => { - // Go to the test Safe home page - cy.visit(`/home?safe=${constants.TEST_SAFE}`) - - cy.contains('button', 'Accept selection').click() - - // Wait for dashboard to initialize - cy.contains('Connect & transact') + cy.visit(constants.homeUrl + constants.TEST_SAFE) + main.acceptCookies() + dashboard.verifyConnectTransactStrIsVisible() }) it('should display the overview widget', () => { - // Alias for the Overview section - cy.contains('h2', 'Overview').parents('section').as('overviewSection') - - cy.get('@overviewSection').within(() => { - // Prefix is separated across elements in EthHashInfo - cy.contains(constants.TEST_SAFE).should('exist') - cy.contains('1/2') - cy.get(`a[href="/balances?safe=${encodeURIComponent(constants.TEST_SAFE)}"]`).contains('View assets') - // Text next to Tokens contains a number greater than 0 - cy.contains('p', 'Tokens').next().contains('1') - cy.contains('p', 'NFTs').next().contains('0') - }) + dashboard.verifyOverviewWidgetData() }) it('should display the tx queue widget', () => { - // Alias for the Transaction queue section - cy.contains('h2', 'Transaction queue').parents('section').as('txQueueSection') - - cy.get('@txQueueSection').within(() => { - // There should be queued transactions - cy.contains('This Safe has no queued transactions').should('not.exist') - - // Queued txns - cy.contains(`a[href^="/transactions/tx?id=multisig_0x"]`, '13' + 'Send' + '-0.00002 GOR' + '1/1').should('exist') - - cy.contains(`a[href="/transactions/queue?safe=${encodeURIComponent(constants.TEST_SAFE)}"]`, 'View all') - }) + dashboard.verifyTxQueueWidget() }) it('should display the featured Safe Apps', () => { - // Alias for the featured Safe Apps section - cy.contains('h2', 'Connect & transact').parents('section').as('featuredSafeAppsSection') - - // Tx Builder app - cy.get('@featuredSafeAppsSection').within(() => { - // Transaction Builder - cy.contains('Use Transaction Builder') - cy.get(`a[href*='tx-builder']`).should('exist') - - // WalletConnect app - cy.contains('Use WalletConnect') - cy.get(`a[href*='wallet-connect']`).should('exist') - - // Featured apps have a Safe-specific link - cy.get(`a[href*="&appUrl=http"]`).should('have.length', 2) - }) + dashboard.verifyFeaturedAppsSection() }) it('should show the Safe Apps Section', () => { - // Create an alias for the Safe Apps section - cy.contains('h2', 'Safe Apps').parents('section').as('safeAppsSection') - - cy.get('@safeAppsSection').contains('Explore Safe Apps') - - // Regular safe apps - cy.get('@safeAppsSection').within(() => { - // Find exactly 5 Safe Apps cards inside the Safe Apps section - cy.get(`a[href^="/apps/open?safe=${encodeURIComponent(constants.TEST_SAFE)}&appUrl=http"]`).should( - 'have.length', - 5, - ) - }) + dashboard.verifySafeAppsSection() }) }) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index f35de9f962..5cc2636ca9 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -1,71 +1,51 @@ import 'cypress-file-upload' -const path = require('path') -import { format } from 'date-fns' +import * as file from '../pages/import_export.pages' +import * as main from '../pages/main.page' +import * as constants from '../../support/constants' describe('Import Export Data', () => { before(() => { - cy.visit(`/welcome`) - cy.contains('Accept selection').click() - // Waits for the Import button to be visible - cy.contains('button', 'Import').should('be.visible') + cy.visit(constants.welcomeUrl) + main.acceptCookies() + file.verifyImportBtnIsVisible() }) it('Uploads test file and access safe', () => { - cy.contains('button', 'Import').click() - //Uploads the file - cy.get('[type="file"]').attachFile('../fixtures/data_import.json') - //verifies that the modal says the amount of chains/addressbook values it uploaded - cy.contains('Added Safe Accounts on 3 chains').should('be.visible') - cy.contains('Address book for 3 chains').should('be.visible') - cy.contains('Settings').should('be.visible') - cy.contains('Bookmarked Safe Apps').should('be.visible') - cy.contains('Data import').parent().contains('button', 'Import').click() - //Click in one of the imported safes - cy.contains('safe 1 goerli').click() + const filePath = '../fixtures/data_import.json' + const safe = 'safe 1 goerli' + + file.clickOnImportBtn() + file.uploadFile(filePath) + file.verifyImportModalData() + file.clickOnImportBtnDataImportModal() + file.clickOnImportedSafe(safe) }) it("Verify safe's address book imported data", () => { - //Verifies imported owners in the Address book - cy.contains('Address book').click() - cy.get('tbody tr:nth-child(1) td:nth-child(1)').contains('test1') - cy.get('tbody tr:nth-child(1) td:nth-child(2)').contains('0x61a0c717d18232711bC788F19C9Cd56a43cc8872') - cy.get('tbody tr:nth-child(2) td:nth-child(1)').contains('test2') - cy.get('tbody tr:nth-child(2) td:nth-child(2)').contains('0x7724b234c9099C205F03b458944942bcEBA13408') + file.clickOnAddressBookBtn() + file.verifyImportedAddressBookData() }) it('Verify pinned apps', () => { - cy.get('aside').contains('li', 'Apps').click() - cy.contains('Bookmarked apps').click() - //Takes a some time to load the apps page, It waits for bookmark to be lighted up - cy.waitForSelector(() => { - return cy - .get('[aria-selected="true"] p') - .invoke('html') - .then((text) => text === 'Bookmarked apps') - }) - cy.contains('Drain Account').should('be.visible') - cy.contains('Transaction Builder').should('be.visible') + const appNames = ['Drain Account', 'Transaction Builder'] + + file.clickOnAppsBtn() + file.clickOnBookmarkedAppsBtn() + file.verifyAppsAreVisible(appNames) }) it('Verify imported data in settings', () => { - //In the settings checks the checkboxes and darkmode enabled - cy.contains('Settings').click() - cy.contains('Appearance').click() - cy.contains('label', 'Prepend chain prefix to addresses').find('input[type="checkbox"]').should('not.be.checked') - cy.contains('label', 'Copy addresses with chain prefix').find('input[type="checkbox"]').should('not.be.checked') - cy.get('main').contains('label', 'Dark mode').find('input[type="checkbox"]').should('be.checked') + const unchecked = [file.prependChainPrefixStr, file.copyAddressStr] + const checked = [file.darkModeStr] + file.clickOnSettingsBtn() + file.clickOnAppearenceBtn() + file.verifyCheckboxes(unchecked) + file.verifyCheckboxes(checked, true) }) it('Verifies data for export in Data tab', () => { - cy.contains('div[role="tablist"] a', 'Data').click() - cy.contains('Added Safe Accounts on 3 chains').should('be.visible') - cy.contains('Address book for 3 chains').should('be.visible') - cy.contains('Bookmarked Safe Apps').should('be.visible') - const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) - const fileName = `safe-${date}.json` - cy.contains('div', fileName).next().click() - const downloadsFolder = Cypress.config('downloadsFolder') - //File reading is failing in the CI. Can be tested locally - cy.readFile(path.join(downloadsFolder, fileName)).should('exist') + file.clickOnDataTab() + file.verifyImportModalData() + file.verifyFileDownload() }) }) diff --git a/cypress/e2e/smoke/landing.cy.js b/cypress/e2e/smoke/landing.cy.js index 4dd60b90d1..18da5edffb 100644 --- a/cypress/e2e/smoke/landing.cy.js +++ b/cypress/e2e/smoke/landing.cy.js @@ -1,7 +1,7 @@ +import * as constants from '../../support/constants' describe('Landing page', () => { it('redirects to welcome page', () => { cy.visit('/') - - cy.url().should('include', '/welcome') + cy.url().should('include', constants.welcomeUrl) }) }) diff --git a/cypress/e2e/smoke/load_safe.cy.js b/cypress/e2e/smoke/load_safe.cy.js index 4a29665860..4456d60a6c 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -1,13 +1,17 @@ import 'cypress-file-upload' import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safe from '../pages/load_safe.pages' +import * as createwallet from '../pages/create_wallet.pages' +const testSafeName = 'Test safe name' +const testOwnerName = 'Test Owner Name' // TODO const SAFE_ENS_NAME = 'test20.eth' const SAFE_ENS_NAME_TRANSLATED = constants.EOA const EOA_ADDRESS = constants.EOA -const INVALID_INPUT_ERROR_MSG = 'Invalid address format' const INVALID_ADDRESS_ERROR_MSG = 'Address given is not a valid Safe address' // TODO @@ -16,41 +20,24 @@ const OWNER_ADDRESS = constants.EOA describe('Load existing Safe', () => { before(() => { - cy.visit('/welcome?chain=matic') - cy.contains('Accept selection').click() - - // Enters Loading Safe form - cy.contains('button', 'Add existing Account').click() - cy.contains('Name, address & network') + cy.visit(constants.chainMaticUrl) + main.acceptCookies() + safe.openLoadSafeForm() }) it('should allow choosing the network where the Safe exists', () => { - // Click the network selector inside the Stepper content - cy.get('[data-testid=load-safe-form]').contains('Polygon').click() - - // Selects Goerli - cy.get('ul li') - .contains(/^G(ö|oe)rli$/) - .click() - cy.contains('span', /^G(ö|oe)rli$/) + safe.clickNetworkSelector(constants.networks.polygon) + safe.selectGoerli() }) it('should accept name the Safe', () => { // alias the address input label cy.get('input[name="address"]').parent().prev('label').as('addressLabel') - // Name input should have a placeholder ending in 'goerli-safe' - cy.get('input[name="name"]') - .should('have.attr', 'placeholder') - .should('match', /g(ö|oe)rli-safe/) - // Input a custom name - cy.get('input[name="name"]').type('Test safe name').should('have.value', 'Test safe name') - - // Input incorrect Safe address - cy.get('input[name="address"]').type('RandomText') - cy.get('@addressLabel').contains(INVALID_INPUT_ERROR_MSG) - - cy.get('input[name="address"]').clear().type(constants.GOERLI_TEST_SAFE) + safe.verifyNameInputHasPlceholder(testSafeName) + safe.inputName(testSafeName) + safe.verifyIncorrectAddressErrorMessage() + safe.inputAddress(constants.GOERLI_TEST_SAFE) // Type an invalid address // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) @@ -68,11 +55,8 @@ describe('Load existing Safe', () => { // cy.contains('Upload an image').click() // cy.get('[type="file"]').attachFile('../fixtures/goerli_safe_QR.png') - // The address field should be filled with the "bare" QR code's address - const [, address] = constants.GOERLI_TEST_SAFE.split(':') - cy.get('input[name="address"]').should('have.value', address) - - cy.contains('Next').click() + safe.verifyAddressInputValue() + safe.clickOnNextBtn() }) // TODO: register the goerli ENS for the Safe owner when possible @@ -85,29 +69,18 @@ describe('Load existing Safe', () => { }) it('should set custom name in the first owner', () => { - // Sets a custom name for the first owner - cy.get('input[name="owners.0.name"]').type('Test Owner Name').should('have.value', 'Test Owner Name') - cy.contains('Next').click() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() }) it('should have Safe and owner names in the Review step', () => { - // Finds Safe name - cy.findByText('Test safe name').should('exist') - // Finds custom owner name - cy.findByText('Test Owner Name').should('exist') - - cy.contains('button', 'Add').click() + safe.verifyDataInReviewSection(testSafeName, testOwnerName) + safe.clickOnAddBtn() }) it('should load successfully the custom Safe name', () => { - // Safe loaded - cy.location('href', { timeout: 10000 }).should('include', `/home?safe=${constants.GOERLI_TEST_SAFE}`) - - // Finds Safe name in the sidebar - cy.get('aside').contains('Test safe name') - - // Safe name is present in Settings - cy.get('aside ul').contains('Settings').click() - cy.contains('Test Owner Name').should('exist') + main.verifyHomeSafeUrl(constants.GOERLI_TEST_SAFE) + safe.veriySidebarSafeNameIsVisible(testSafeName) + safe.verifyOwnerNamePresentInSettings(testOwnerName) }) }) diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index eea51b9d84..b920c6c1ed 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -1,88 +1,50 @@ import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as nfts from '../pages/nfts.pages' + +const nftsName = 'BillyNFT721' +const nftsAddress = '0x0000...816D' +const nftsTokenID = 'Kitaro World #261' +const nftsLink = 'https://testnets.opensea.io/assets/0x000000000faE8c6069596c9C805A1975C657816D/443' describe('Assets > NFTs', () => { before(() => { - cy.visit(`/balances/nfts?safe=${constants.GOERLI_TEST_SAFE}`) - cy.contains('button', 'Accept selection').click() - cy.contains(/E2E Wallet @ G(ö|oe)rli/) + cy.visit(constants.balanceNftsUrl + constants.GOERLI_TEST_SAFE) + main.acceptCookies() + cy.contains(constants.goerlyE2EWallet) }) describe('should have NFTs', () => { it('should have NFTs in the table', () => { - cy.get('tbody tr').should('have.length', 5) + nfts.verifyNFTNumber(5) }) it('should have info in the NFT row', () => { - cy.get('tbody tr:first-child').contains('td:first-child', 'BillyNFT721') - cy.get('tbody tr:first-child').contains('td:first-child', '0x0000...816D') - - cy.get('tbody tr:first-child').contains('td:nth-child(2)', 'Kitaro World #261') - - cy.get( - 'tbody tr:first-child td:nth-child(3) a[href="https://testnets.opensea.io/assets/0x000000000faE8c6069596c9C805A1975C657816D/443"]', - ) + nfts.verifyDataInTable(nftsName, nftsAddress, nftsTokenID, nftsLink) }) it('should open an NFT preview', () => { - // Preview the first NFT - cy.get('tbody tr:first-child td:nth-child(2)').click() - - // Modal - cy.get('div[role="dialog"]').contains('Kitaro World #261') - - // Prevent Base Mainnet Goerli from being selected - cy.get('div[role="dialog"]').contains(/^G(ö|oe)rli$/) - cy.get('div[role="dialog"]').contains( - 'a[href="https://testnets.opensea.io/assets/0x000000000faE8c6069596c9C805A1975C657816D/443"]', - 'View on OpenSea', - ) - - // Close the modal - cy.get('div[role="dialog"] button').click() - cy.get('div[role="dialog"]').should('not.exist') + nfts.openFirstNFT() + nfts.verifyNameInNFTModal(nftsTokenID) + nfts.preventBaseMainnetGoerliFromBeingSelected() + nfts.verifyNFTModalLink(nftsLink) + nfts.closeNFTModal() }) it('should not open an NFT preview for NFTs without one', () => { - // Click on the third NFT - cy.get('tbody tr:nth-child(3) td:nth-child(2)').click() - cy.get('div[role="dialog"]').should('not.exist') + nfts.clickOnThirdNFT() + nfts.verifyNFTModalDoesNotExist() }) it('should select and send multiple NFTs', () => { - // Select three NFTs - cy.contains('0 NFTs selected') - cy.contains('button[disabled]', 'Send') - cy.get('tbody tr:first-child input[type="checkbox"]').click() - cy.contains('1 NFT selected') - cy.contains('button:not([disabled])', 'Send 1 NFT') - cy.get('tbody tr:nth-child(2) input[type="checkbox"]').click() - cy.contains('2 NFTs selected') - cy.contains('button', 'Send 2 NFTs') - cy.get('tbody tr:last-child input[type="checkbox"]').click() - cy.contains('3 NFTs selected') - - // Deselect one NFT - cy.get('tbody tr:nth-child(2) input[type="checkbox"]').click() - cy.contains('2 NFTs selected') - - // Send NFTs - cy.contains('button', 'Send 2 NFTs').click() - - // Modal appears - cy.contains('Send NFTs') - cy.contains('Recipient address or ENS') - cy.contains('Selected NFTs') - cy.get('input[name="recipient"]').type('0x97d314157727D517A706B5D08507A1f9B44AaaE9') - cy.contains('button', 'Next').click() - - // Review modal appears - cy.contains('Send') - cy.contains('To') - cy.wait(1000) - cy.contains('1') - cy.contains('2') - cy.get('b:contains("safeTransferFrom")').should('have.length', 2) - cy.contains('button:not([disabled])', 'Execute') + nfts.verifyInitialNFTData() + nfts.selectNFTs(3) + nfts.deselectNFTs([2], 3) + nfts.sendNFT(2) + nfts.verifyNFTModalData() + nfts.typeRecipientAddress(constants.GOERLI_TEST_SAFE) + nfts.clikOnNextBtn() + nfts.verifyReviewModalData(2) }) }) }) diff --git a/cypress/e2e/smoke/pending_actions.cy.js b/cypress/e2e/smoke/pending_actions.cy.js index 34843b880c..73f7a6a9ef 100644 --- a/cypress/e2e/smoke/pending_actions.cy.js +++ b/cypress/e2e/smoke/pending_actions.cy.js @@ -1,9 +1,11 @@ import * as constants from '../../support/constants' +import * as safe from '../pages/load_safe.pages' +import * as main from '../pages/main.page' describe('Pending actions', () => { before(() => { - cy.visit(`/welcome`) - cy.contains('button', 'Accept selection').click() + cy.visit(constants.welcomeUrl) + main.acceptCookies() }) beforeEach(() => { @@ -17,41 +19,24 @@ describe('Pending actions', () => { }) it('should add the Safe with the pending actions', () => { - // Enters Loading Safe form - cy.contains('button', 'Add').click() - cy.contains('Name, address & network') - - // Inputs the Safe address - cy.get('input[name="address"]').type(constants.TEST_SAFE) - cy.contains('Next').click() - - cy.contains('Owners and confirmations') - cy.contains('Next').click() - - cy.contains('Add').click() + safe.openLoadSafeForm() + safe.inputAddress(constants.TEST_SAFE) + safe.clickOnNextBtn() + safe.verifyOwnersModalIsVisible() + safe.clickOnNextBtn() + safe.clickOnAddBtn() }) it('should display the pending actions in the Safe list sidebar', () => { - cy.get('aside').within(() => { - cy.get('[data-testid=ChevronRightIcon]').click({ force: true }) - }) - - cy.get('li').within(() => { - cy.contains('0x04f8...1a91').should('exist') - - //cy.get('img[alt="E2E Wallet logo"]').next().contains('2').should('exist') - cy.get('[data-testid=CheckIcon]').next().contains('1').should('exist') - - // click on the pending actions - cy.get('[data-testid=CheckIcon]').next().click() - }) + safe.openSidebar() + safe.verifyAddressInsidebar(constants.SIDEBAR_ADDRESS) + safe.verifySidebarIconNumber(1) + safe.clickOnPendingActions() + //cy.get('img[alt="E2E Wallet logo"]').next().contains('2').should('exist') }) it('should have the right number of queued and signable transactions', () => { - // Navigates to the tx queue - cy.contains('h3', 'Transactions').should('be.visible') - - // contains 1 queued transaction - cy.get('span:contains("1 out of 1")').should('have.length', 1) + safe.verifyTransactionSectionIsVisible() + safe.verifyNumberOfTransactions(1, 1) }) }) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 57eaf2691a..feea0642a2 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -7,14 +7,26 @@ export const TEST_SAFE = 'gor:0x04f8b1EA3cBB315b87ced0E32deb5a43cC151a91' export const EOA = '0xE297437d6b53890cbf004e401F3acc67c8b39665' export const DEFAULT_OWNER_ADDRESS = '0xC16Db0251654C0a72E91B190d81eAD367d2C6fED' export const TEST_SAFE_2 = 'gor:0xE96C43C54B08eC528e9e815fC3D02Ea94A320505' +export const SIDEBAR_ADDRESS = '0x04f8...1a91' export const BROWSER_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__browserPermissions` export const SAFE_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__safePermissions` export const INFO_MODAL_KEY = `${LS_NAMESPACE}SafeApps__infoModal` +export const goerlyE2EWallet = /E2E Wallet @ G(ö|oe)rli/ +export const goerlySafeName = /g(ö|oe)rli-safe/ +export const goerliToken = /G(ö|oe)rli Ether/ + export const appUrlProd = 'https://safe-test-app.com' export const addressBookUrl = '/address-book?safe=' export const BALANCE_URL = '/balances?safe=' +export const balanceNftsUrl = '/balances/nfts?safe=' +export const transactionQueueUrl = '/transactions/queue?safe=' +export const openAppsUrl = '/apps/open?safe=' +export const homeUrl = '/home?safe=' +export const welcomeUrl = '/welcome' +export const chainMaticUrl = '/welcome?chain=matic' +export const proposeEndPoint = '/**/propose' export const GOERLI_CSV_ENTRY = { name: 'goerli user 1', @@ -24,3 +36,14 @@ export const GNO_CSV_ENTRY = { name: 'gno user 1', address: '0x61a0c717d18232711bC788F19C9Cd56a43cc8872', } + +export const networks = { + ethereum: 'Ethereum', + goerli: /^G(ö|oe)rli$/, + sepolia: 'Sepolia', + polygon: 'Polygon', +} + +export const tokenAbbreviation = { + gor: 'GOR', +}