diff --git a/cypress.config.js b/cypress.config.js index 753ec36daa..ff3bf4baf8 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,7 +5,7 @@ export default defineConfig({ trashAssetsBeforeRuns: true, retries: { - runMode: 3, + runMode: 1, openMode: 0, }, diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index 1385761e8b..fb7401609b 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -3,6 +3,10 @@ import * as constants from '../../support/constants' const searchAppInput = 'input[id="search-by-name"]' const appUrlInput = 'input[name="appUrl"]' const closePreviewWindowBtn = 'button[aria-label*="Close"][aria-label*="preview"]' +export const contractMethodIndex = '[name="contractMethodIndex"]' +export const saveToLibraryBtn = 'button[title="Save to Library"]' +export const downloadBatchBtn = 'button[title="Download batch"]' +export const deleteBatchBtn = 'button[title="Delete Batch"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -19,13 +23,72 @@ const accessToAddressBookStr = /access to your address book/i const acceptBtnStr = /accept/i const clearAllBtnStr = /clear all/i const allowAllPermissions = /allow all/i +export const enterAddressStr = /enter address or ens name/i +export const addTransactionStr = /add transaction/i +export const createBatchStr = /create batch/i +export const sendBatchStr = /send batch/i +export const transactionDetailsStr = /transaction details/i +export const addOwnerWithThreshold = /add owner with threshold/i +export const enterABIStr = /Enter ABI/i +export const toAddressStr = /to address/i +export const gorValue = /gor value */i +export const dataStr = /data */i +export const clearTransactionListStr = /Clear transaction list?/i +export const confirmClearTransactionListStr = /Yes, clear/i +export const cancelBtnStr = 'Cancel' +export const confirmDeleteBtnStr = 'Yes, delete' +export const backBtnStr = /Back/i +export const simulateBtnStr = /Simulate/i +export const reviewAndConfirmStr = /Review and confirm/i +export const backToTransactionStr = /Back to Transaction Creation/i +export const batchNameStr = /Batch name/i +export const transactionLibraryStr = /Your transaction library/i +export const noSavedBatchesStr = /You don't have any saved batches./i +export const keepProxiABIStr = /Keep Proxy ABI/i +export const selectAllRowsChbxStr = /Select All Rows checkbox/i +export const selectRowChbxStr = /Select Row checkbox/i +export const recipientStr = /recipient/i +export const validRecipientAddressStr = /please enter a valid recipient address/i +export const testAddressValue2 = 'testAddressValue' +export const testBooleanValue = 'testBooleanValue' +export const testBooleanValue1 = '1 testBooleanValue' +export const testBooleanValue2 = '2 testBooleanValue' +export const testBooleanValue3 = '3 testBooleanValue' +export const transfer2AssetsStr = 'Transfer 2 assets' + +export const testTransfer1 = '1 transfer' +export const testTransfer2 = '2 transfer' +export const testNativeTransfer2 = '2 native transfer' +export const testNativeTransfer1 = '1 native transfer' + +export const newValueBool = 'newValue(bool):' +export const ownerAddressStr = 'owner (address)' +export const ownerAddressStr2 = 'owner(address)' +export const thresholdStr = '_threshold (uint256) *' +export const thresholdStr2 = '_threshold(uint256):' const appNotSupportedMsg = "The app doesn't support Safe App functionality" +export const changedPropertiesStr = 'This batch contains some changed properties since you saved or downloaded it' +export const anotherChainStr = 'This batch is from another Chain (1)!' +export const useImplementationABI = 'Use Implementation ABI' +export const addressNotValidStr = 'The address is not valid' +export const transferEverythingStr = 'Transfer everything' +export const noTokensSelectedStr = 'No tokens selected' +export const requiredStr = 'Required' +export const e3eTestStr = 'E2E test' +export const createBtnStr = 'Create' +export const warningStr = 'Warning' +export const transferStr = 'Transfer' +export const successStr = 'Success' +export const failedStr = 'Failed' export const pinWalletConnectStr = /pin walletconnect/i export const transactionBuilderStr = 'Transaction Builder' +export const testAddressValueStr = 'test Address Value' export const logoWalletConnect = /logo.*walletconnect/i export const walletConnectHeadlinePreview = /walletconnect/i +export const newAddressValueStr = 'newValue (address)' +export const newAddressValueStr2 = 'newValue(address)' export const transactiobUilderHeadlinePreview = 'Transaction Builder' export const availableNetworksPreview = 'Available networks' export const connecttextPreview = 'Compose custom contract interactions and batch them into a single transaction' @@ -36,6 +99,8 @@ export const gridItem = 'main .MuiPaper-root > .MuiGrid-item' export const linkNames = { logo: /logo/i, } +export const abi = + '[{{}"inputs":[{{}"internalType":"address","name":"_singleton","type":"address"{}}],"stateMutability":"nonpayable","type":"constructor"{}},{{}"stateMutability":"payable","type":"fallback"{}}]' export const permissionCheckboxes = { camera: 'input[name="camera"]', diff --git a/cypress/e2e/safe-apps/drain_account.spec.cy.js b/cypress/e2e/safe-apps/drain_account.spec.cy.js new file mode 100644 index 0000000000..87d878f307 --- /dev/null +++ b/cypress/e2e/safe-apps/drain_account.spec.cy.js @@ -0,0 +1,93 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safeapps from '../pages/safeapps.pages' + +describe('Drain Account Safe App tests', { defaultCommandTimeout: 12000 }, () => { + const appUrl = constants.drainAccount_url + const iframeSelector = `iframe[id="iframe-${appUrl}"]` + const visitUrl = `/apps/open?safe=${constants.GOERLI_SAFE_APPS_SAFE}&appUrl=${encodeURIComponent(appUrl)}` + + beforeEach(() => { + cy.intercept(`**//v1/chains/5/safes/${constants.GOERLI_SAFE_APPS_SAFE.substring(4)}/balances/**`, { + fixture: 'balances.json', + }) + + cy.clearLocalStorage() + cy.visit(visitUrl) + main.acceptCookies(1) + safeapps.clickOnContinueBtn() + }) + + it('Verify drain can be created [C56627]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer1 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer2 }) + }) + + it('Verify partial drain can be created [C56628]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(1).click() + getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(2).click() + getBody().findByLabelText(safeapps.recipientStr).clear().type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transfer2AssetsStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer2 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer1 }) + }) + + it('Verify a drain can be created when a ENS is specified [C56629]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type('goerli-test-safe.eth').wait(2000) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer1 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer2 }) + }) + + // TODO: Adjust safe - owner + it.skip('Verify when cancelling a drain, previous data is preserved [C56630]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.cancelBtnStr }).click() + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText(safeapps.transferEverythingStr).should('be.visible') + }) + }) + + it('Verify a drain cannot be created with no recipient selected [C56631]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText(safeapps.transferEverythingStr).click() + getBody().findByText(safeapps.validRecipientAddressStr) + }) + }) + + it('Verify a drain cannot be created with invalid recipient selected [C56632]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2.substring(1)) + getBody().findAllByText(safeapps.transferEverythingStr).click() + getBody().findByText(safeapps.validRecipientAddressStr) + }) + }) + + it('Verify a drain cannot be created when no assets are selected [C56633]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.noTokensSelectedStr).should('be.visible') + }) + }) + + it('should not allow to perform a drain when no assets and recipient are selected', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findAllByText(safeapps.noTokensSelectedStr).should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js new file mode 100644 index 0000000000..fd850c112f --- /dev/null +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -0,0 +1,252 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safeapps from '../pages/safeapps.pages' + +describe('Tx-builder Safe App tests', { defaultCommandTimeout: 20000 }, () => { + const appUrl = constants.TX_Builder_url + const iframeSelector = `iframe[id="iframe-${appUrl}"]` + const visitUrl = `/apps/open?safe=${constants.GOERLI_SAFE_APPS_SAFE}&appUrl=${encodeURIComponent(appUrl)}` + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(visitUrl) + main.acceptCookies(1) + safeapps.clickOnContinueBtn() + }) + + it('Verify a simple batch can be created [C56609]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByRole('button', { name: safeapps.transactionDetailsStr }).click() + cy.findByRole('region').should('exist') + cy.findByText(safeapps.testAddressValueStr).should('exist') + cy.contains(safeapps.newAddressValueStr2).should('exist') + cy.findAllByText(constants.SAFE_APP_ADDRESS_2_SHORT).should('have.length', 1) + }) + + it('Verify a complex batch can be created [C56610]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.findByRole('button', { name: safeapps.testBooleanValue1 }).click() + cy.findByRole('button', { name: safeapps.testBooleanValue2 }).click() + cy.findByRole('button', { name: safeapps.testBooleanValue3 }).click() + cy.findAllByText(safeapps.newValueBool).should('have.length', 3) + cy.findAllByText('True').should('have.length', 3) + }) + + it('Verify a batch can be created using ENS name [C56611]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.ENS_TEST_GOERLI) + getBody().findByRole('button', { name: safeapps.useImplementationABI }).click() + getBody().findByLabelText(safeapps.ownerAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByLabelText(safeapps.thresholdStr).type('1') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.findByRole('button', { name: safeapps.transactionDetailsStr }).click() + cy.findByRole('region').should('exist') + cy.findByText(safeapps.addOwnerWithThreshold).should('exist') + cy.contains(safeapps.ownerAddressStr2).should('exist') + cy.findAllByText(constants.SAFE_APP_ADDRESS_2_SHORT).should('have.length', 1) + cy.findByText(safeapps.thresholdStr2).should('exist') + }) + + it('Verify a batch can be created from an ABI [C56612]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterABIStr).type(safeapps.abi) + getBody().findByLabelText(safeapps.toAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByText(constants.SAFE_APP_ADDRESS_2).should('be.visible') + }) + + it('Verify a batch with custom data can be created [C56613]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().find('.MuiSwitch-root').click() + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByLabelText(safeapps.dataStr).type('0x') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByText(constants.SAFE_APP_ADDRESS_3).should('be.visible') + }) + + it('Verify a batch can be cancelled [C56614]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByRole('button', { name: safeapps.cancelBtnStr }).click() + getBody().findByText(safeapps.clearTransactionListStr) + getBody().findByRole('button', { name: safeapps.confirmClearTransactionListStr }).click() + getBody().findAllByText('choose a file').should('be.visible') + }) + }) + + it('Verify cancel operation can be reverted [C56615]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByRole('button', { name: safeapps.cancelBtnStr }).click() + getBody().findByText(safeapps.clearTransactionListStr) + getBody().findByRole('button', { name: safeapps.backBtnStr }).click() + getBody().findByText(safeapps.reviewAndConfirmStr).should('be.visible') + }) + }) + + it('Verify it is allowed to go back without removing data and add more transactions to the batch [C56616]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.backToTransactionStr).click() + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('p').contains('1').should('be.visible') + cy.get('p').contains('2').should('be.visible') + }) + + it('Verify a batch cannot be created with invalid address [C56617]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findAllByText(safeapps.addressNotValidStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch cannot be created without asset amount [C56618]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch cannot be created without method data [C56619]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch can be uploaded, saved, downloaded and removed [C56620]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-working-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText('uploaded').wait(300) + getBody().find(safeapps.saveToLibraryBtn).click() + getBody().findByLabelText(safeapps.batchNameStr).type(safeapps.e3eTestStr) + getBody().findAllByText(safeapps.createBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.transactionLibraryStr).click() + getBody().find(safeapps.downloadBatchBtn).click() + getBody().find(safeapps.deleteBatchBtn).click() + getBody().findAllByText(safeapps.confirmDeleteBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.noSavedBatchesStr).should('be.visible') + getBody().findByText(safeapps.backToTransactionStr).should('be.visible') + }) + cy.readFile('cypress/downloads/E2E test.json').should('exist') + }) + + it('Verify there is notification if uploaded batch is from a different chain [C56621]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-mainnet-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.warningStr).should('be.visible') + getBody().findAllByText(safeapps.anotherChainStr).should('be.visible') + }) + }) + + it('Verify there is error message when a modified batch is uploaded [C56622]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-modified-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.changedPropertiesStr) + getBody().findAllByText('choose a file').should('be.visible') + }) + }) + + it('Verify an invalid batch cannot be uploaded [C56623]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-invalid-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify an empty batch cannot be uploaded [C56624]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-empty-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify a valid batch as successful can be simulated [C56625]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.transferStr).should('be.visible') + getBody().findByText(safeapps.successStr).should('be.visible') + }) + }) + + it('Verify an invalid batch as failed can be simulated [C56626]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.TEST_SAFE_2) + getBody().findByText(safeapps.keepProxiABIStr).click() + getBody().findByLabelText(safeapps.gorValue).type('100') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.failedStr).should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index 578fc639ee..aae5c455ad 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -215,6 +215,6 @@ describe('Assets tests', () => { cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_4) balances.selectTokenList(balances.tokenListOptions.allTokens) balances.showSendBtn(0) - owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) + owner.verifyTooltiptext(owner.nonOwnerErrorMsg) }) }) diff --git a/cypress/e2e/smoke/balances_pagination.cy.js b/cypress/e2e/smoke/balances_pagination.cy.js index 92539761cd..b1033b9242 100644 --- a/cypress/e2e/smoke/balances_pagination.cy.js +++ b/cypress/e2e/smoke/balances_pagination.cy.js @@ -8,12 +8,10 @@ describe('Balance tests', () => { before(() => { cy.clearLocalStorage() // Open the Safe used for testing - cy.visit(constants.BALANCE_URL + constants.PAGINATION_TEST_SAFE) + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) main.acceptCookies(2) - cy.contains('div', 'Default tokens').click() - cy.wait(100) - cy.contains('div', 'All tokens').click() + balances.selectTokenList(balances.tokenListOptions.allTokens) }) it('Verify a user can change rows per page and navigate to next and previous page [C56073]', () => { diff --git a/cypress/fixtures/balances.json b/cypress/fixtures/balances.json new file mode 100644 index 0000000000..ffcb4343b9 --- /dev/null +++ b/cypress/fixtures/balances.json @@ -0,0 +1,96 @@ +{ + "fiatTotal": "118.36679999999998", + "items": [ + { + "tokenInfo": { + "type": "ERC20", + "address": "0x02ABBDbAaa7b1BB64B5c878f7ac17f8DDa169532", + "decimals": 18, + "symbol": "GNO", + "name": "Gnosis Token", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x02ABBDbAaa7b1BB64B5c878f7ac17f8DDa169532.png" + }, + "balance": "500000000000000000", + "fiatBalance": "65.70705", + "fiatConversion": "131.4141198301599" + }, + { + "tokenInfo": { + "type": "NATIVE_TOKEN", + "address": "0x0000000000000000000000000000000000000000", + "decimals": 18, + "symbol": "GOR", + "name": "Görli Ether", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/chains/5/currency_logo.png" + }, + "balance": "21000000000000000", + "fiatBalance": "35.14308", + "fiatConversion": "1673.48" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", + "decimals": 18, + "symbol": "WETH", + "name": "Wrapped Ether", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6.png" + }, + "balance": "10000000000000000", + "fiatBalance": "16.73480", + "fiatConversion": "1673.48" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "decimals": 18, + "symbol": "UNI", + "name": "Uniswap", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984.png" + }, + "balance": "10000000000000000", + "fiatBalance": "0.78187", + "fiatConversion": "78.18749370339182" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x3430d04E42a722c5Ae52C5Bffbf1F230C2677600", + "decimals": 18, + "symbol": "COW", + "name": "CoW Protocol Token", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x3430d04E42a722c5Ae52C5Bffbf1F230C2677600.png" + }, + "balance": "10000000000000000000", + "fiatBalance": "0.00000", + "fiatConversion": "0.0" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60", + "decimals": 18, + "symbol": "DAI", + "name": "Dai", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60.png" + }, + "balance": "200000000000000000000", + "fiatBalance": "0.00000", + "fiatConversion": "0.00000000002294647997868982" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x1B809925ba90c541d895D19f0b7D70eE281a987F", + "decimals": 0, + "symbol": "VanityTRX.org", + "name": "VanityTRX.org", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1B809925ba90c541d895D19f0b7D70eE281a987F.png" + }, + "balance": "888888", + "fiatBalance": "0.00000", + "fiatConversion": "0.0" + } + ] +} diff --git a/cypress/fixtures/test-empty-batch.json b/cypress/fixtures/test-empty-batch.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cypress/fixtures/test-empty-batch.json @@ -0,0 +1 @@ +{} diff --git a/cypress/fixtures/test-invalid-batch.json b/cypress/fixtures/test-invalid-batch.json new file mode 100644 index 0000000000..51ed055cf0 --- /dev/null +++ b/cypress/fixtures/test-invalid-batch.json @@ -0,0 +1,3 @@ +{ + "test": "I am not a valid batch" +} diff --git a/cypress/fixtures/test-mainnet-batch.json b/cypress/fixtures/test-mainnet-batch.json new file mode 100644 index 0000000000..d623b2678a --- /dev/null +++ b/cypress/fixtures/test-mainnet-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } + } + ] +} diff --git a/cypress/fixtures/test-modified-batch.json b/cypress/fixtures/test-modified-batch.json new file mode 100644 index 0000000000..fc9baf3047 --- /dev/null +++ b/cypress/fixtures/test-modified-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "", + "value": "", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "", + "value": "", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "" } + } + ] +} diff --git a/cypress/fixtures/test-working-batch.json b/cypress/fixtures/test-working-batch.json new file mode 100644 index 0000000000..a77bf55c3d --- /dev/null +++ b/cypress/fixtures/test-working-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "5", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } + } + ] +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ed51e2fe5e..b8c8e64fee 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -57,3 +57,157 @@ const waitForSelector = (item, options = {}) => { } Cypress.Commands.add('waitForSelector', waitForSelector) + +const DEFAULT_OPTS = { + log: true, + timeout: 30000, +} + +const DEFAULT_IFRAME_SELECTOR = 'iframe' + +function sleep(timeout) { + return new Promise((resolve) => setTimeout(resolve, timeout)) +} + +// This command checks that an iframe has loaded onto the page +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.frameLoaded() + +// - This will verify that the iframe is loaded to any url containing the given path part +// cy.frameLoaded({ url: 'https://google.com' }) +// cy.frameLoaded({ url: '/join' }) +// cy.frameLoaded({ url: '?some=query' }) +// cy.frameLoaded({ url: '#/hash/path' }) + +// - You can also give it a selector to check that a specific iframe has loaded +// cy.frameLoaded('#my-frame') +// cy.frameLoaded('#my-frame', { url: '/join' }) +Cypress.Commands.add('frameLoaded', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + const log = fullOpts.log + ? Cypress.log({ + name: 'frame loaded', + displayName: 'frame loaded', + message: [selector], + }).snapshot() + : null + return cy.get(selector, { log: false }).then({ timeout: fullOpts.timeout }, async ($frame) => { + log?.set('$el', $frame) + if ($frame.length !== 1) { + throw new Error( + `cypress-iframe commands can only be applied to exactly one iframe at a time. Instead found ${$frame.length}`, + ) + } + + const contentWindow = $frame.prop('contentWindow') + const hasNavigated = fullOpts.url + ? () => + typeof fullOpts.url === 'string' + ? contentWindow.location.toString().includes(fullOpts.url) + : fullOpts.url?.test(contentWindow.location.toString()) + : () => contentWindow.location.toString() !== 'about:blank' + + while (!hasNavigated()) { + await sleep(100) + } + + if (contentWindow.document.readyState === 'complete') { + return $frame + } + + const loadLog = Cypress.log({ + name: 'Frame Load', + message: [contentWindow.location.toString()], + event: true, + }).snapshot() + await new Promise((resolve) => { + Cypress.$(contentWindow).on('load', resolve) + }) + loadLog.end() + log?.finish() + return $frame + }) +}) + +// This will cause subsequent commands to be executed inside of the given iframe +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.iframe().find('.some-button').should('be.visible').click() +// cy.iframe().contains('Some hidden element').should('not.be.visible') +// cy.find('#outside-iframe').click() // this will be executed outside the iframe + +// - You can also give it a selector to find elements inside of a specific iframe +// cy.iframe('#my-frame').find('.some-button').should('be.visible').click() +// cy.iframe('#my-second-frame').contains('Some hidden element').should('not.be.visible') +Cypress.Commands.add('iframe', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + const log = fullOpts.log + ? Cypress.log({ + name: 'iframe', + displayName: 'iframe', + message: [selector], + }).snapshot() + : null + return cy.frameLoaded(selector, { ...fullOpts, log: false }).then(($frame) => { + log?.set('$el', $frame).end() + const contentWindow = $frame.prop('contentWindow') + return Cypress.$(contentWindow.document.body) + }) +}) + +// This can be used to execute a group of commands within an iframe +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.enter().then(getBody => { +// getBody().find('.some-button').should('be.visible').click() +// getBody().contains('Some hidden element').should('not.be.visible') +// }) +// - You can also give it a selector to find elements inside of a specific iframe +// cy.enter('#my-iframe').then(getBody => { +// getBody().find('.some-button').should('be.visible').click() +// getBody().contains('Some hidden element').should('not.be.visible') +// }) +Cypress.Commands.add('enter', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + + const log = fullOpts.log + ? Cypress.log({ + name: 'enter', + displayName: 'enter', + message: [selector], + }).snapshot() + : null + + return cy.iframe(selector, { ...fullOpts, log: false }).then(($body) => { + log?.set('$el', $body).end() + return () => cy.wrap($body, { log: false }) + }) +}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 8aff12d459..6daeacfc92 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -1,5 +1,6 @@ import { LS_NAMESPACE } from '../../src/config/constants' export const RECIPIENT_ADDRESS = '0x6a5602335a878ADDCa4BF63a050E34946B56B5bC' +export const GOERLI_SAFE_APPS_SAFE = 'gor:0x168ca275d1103cb0a30980813140053c7566932F' export const GOERLI_TEST_SAFE = 'gor:0x97d314157727D517A706B5D08507A1f9B44AaaE9' // SEPOLIA_TEST_SAFE_1 should always have no transactions, tokens and NFTs export const SEPOLIA_TEST_SAFE_1 = 'sep:0xBb26E3717172d5000F87DeFd391994f789D80aEB' @@ -8,15 +9,21 @@ export const SEPOLIA_TEST_SAFE_2 = 'sep:0x33C4AA5729D91FfB3B87AEf8a324bb6304Fb90 export const SEPOLIA_TEST_SAFE_3 = 'sep:0x6E834E9D04ad6b26e1525dE1a37BFd9b215f40B7' export const SEPOLIA_TEST_SAFE_4 = 'sep:0x03042B890b99552b60A073F808100517fb148F60' export const SEPOLIA_TEST_SAFE_5 = 'sep:0xBd69b0a9DC90eB6F9bAc3E4a5875f437348b6415' +export const SEPOLIA_TEST_SAFE_6 = 'sep:0x6d0b6F96f665Bb4490f9ddb2e450Da2f7e546dC1' export const GNO_TEST_SAFE = 'gno:0xB8d760a90a5ed54D3c2b3EFC231277e99188642A' export const PAGINATION_TEST_SAFE = 'gor:0x850493a15914aAC05a821A3FAb973b4598889A7b' export const TEST_SAFE = 'gor:0x04f8b1EA3cBB315b87ced0E32deb5a43cC151a91' export const EOA = '0x03042B890b99552b60A073F808100517fb148F60' +export const SAFE_APP_ADDRESS = '0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4' +export const SAFE_APP_ADDRESS_2 = '0x49d4450977E2c95362C13D3a31a09311E0Ea26A6' +export const SAFE_APP_ADDRESS_3 = '0xc6b82bA149CFA113f8f48d5E3b1F78e933e16DfD' +export const SAFE_APP_ADDRESS_2_SHORT = '0x49d4...26A6' export const DEFAULT_OWNER_ADDRESS = '0xC16Db0251654C0a72E91B190d81eAD367d2C6fED' export const SEPOLIA_OWNER_2 = '0x96D4c6fFC338912322813a77655fCC926b9A5aC5' export const TEST_SAFE_2 = 'gor:0xE96C43C54B08eC528e9e815fC3D02Ea94A320505' export const SIDEBAR_ADDRESS = '0x04f8...1a91' export const ENS_TEST_SEPOLIA = 'testenssepolia.eth' +export const ENS_TEST_GOERLI = 'goerli-safe-test.eth' export const ENS_TEST_SEPOLIA_INVALID = 'ivladitestenssepolia.eth' export const BROWSER_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__browserPermissions` @@ -28,6 +35,8 @@ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ +export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' +export const drainAccount_url = 'https://safe-apps.dev.5afe.dev/drain-safe' export const testAppUrl = 'https://safe-test-app.com' export const addressBookUrl = '/address-book?safe=' export const BALANCE_URL = '/balances?safe='