From 629dedfe66f741c43fa6e2557ef8a2a27bb41a12 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:26:01 +0100 Subject: [PATCH 1/3] Tests: add csv import tests (#3232) * tests: add csv import tests * tests: change network from gnosis to etherium --- cypress/e2e/pages/address_book.page.js | 81 ++++++++++++++----- cypress/e2e/pages/sidebar.pages.js | 2 +- cypress/e2e/regression/address_book.cy.js | 38 ++++++++- cypress/e2e/regression/sidebar_2.cy.js | 15 ++-- cypress/e2e/smoke/address_book.cy.js | 39 ++++++++- cypress/fixtures/address_book_addedsafes.csv | 2 + cypress/fixtures/address_book_duplicated.csv | 3 + cypress/fixtures/address_book_empty_test.csv | 1 + cypress/fixtures/address_book_networks.csv | 5 ++ cypress/support/localstorage_data.js | 20 +---- .../address-book/ImportDialog/index.tsx | 14 +++- src/components/common/EnhancedTable/index.tsx | 3 +- src/components/tx/ErrorMessage/index.tsx | 2 +- 13 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 cypress/fixtures/address_book_addedsafes.csv create mode 100644 cypress/fixtures/address_book_duplicated.csv create mode 100644 cypress/fixtures/address_book_empty_test.csv create mode 100644 cypress/fixtures/address_book_networks.csv diff --git a/cypress/e2e/pages/address_book.page.js b/cypress/e2e/pages/address_book.page.js index 611b7d826b..d333a0d505 100644 --- a/cypress/e2e/pages/address_book.page.js +++ b/cypress/e2e/pages/address_book.page.js @@ -1,41 +1,86 @@ -export const acceptSelection = 'Save settings' -export const addressBook = 'Address book' -const createEntryBtn = 'Create entry' +import * as main from '../pages/main.page' +export const addressBookRecipient = '[data-testid="address-book-recipient"]' const beameriFrameContainer = '#beamerOverlay .iframeCointaner' const beamerInput = 'input[id="beamer"]' const nameInput = 'input[name="name"]' const addressInput = 'input[name="address"]' -export const addressBookRecipient = '[data-testid="address-book-recipient"]' -const saveBtn = 'Save' +const exportModalBtn = '[data-testid="export-modal-btn"]' export const editEntryBtn = 'button[aria-label="Edit entry"]' export const deleteEntryBtn = 'button[aria-label="Delete entry"]' export const deleteEntryModalBtnSection = '.MuiDialogActions-root' +const tableContainer = '[data-testid="table-container"]' +const tableRow = '[data-testid="table-row"]' +const importBtn = '[data-testid="import-btn"]' +const cancelImportBtn = '[data-testid="cancel-btn"]' +const uploadErrorMsg = '[data-testid="error-message"]' +const modalSummaryMessage = '[data-testid="summary-message"]' + +export const acceptSelection = 'Save settings' +export const addressBook = 'Address book' +const createEntryBtn = 'Create entry' export const delteEntryModaldeleteBtn = 'Delete' -const importBtn = 'Import' const exportBtn = 'Export' -const exportModalBtn = '[data-testid="export-modal-btn"]' +const saveBtn = 'Save' const whatsNewBtnStr = "What's new" const beamrCookiesStr = 'accept the "Beamer" cookies' +const headerImportBtnStr = 'Import' + +export const emptyCSVFile = '../fixtures/address_book_empty_test.csv' +export const nonCSVFile = '../fixtures/balances.json' +export const duplicatedCSVFile = 'address_book_duplicated.csv' +export const validCSVFile = '../fixtures/address_book_test.csv' +export const networksCSVFile = '../fixtures/address_book_networks.csv' +export const addedSafesCSVFile = '../fixtures/address_book_addedsafes.csv' + +export const entries = [ + '0x6E834E9D04ad6b26e1525dE1a37BFd9b215f40B7', + 'test-sepolia-3', + '0xf405BC611F4a4c89CCB3E4d083099f9C36D966f8', + 'sepolia-test-4', + '0x03042B890b99552b60A073F808100517fb148F60', + 'sepolia-test-5', + '0xBd69b0a9DC90eB6F9bAc3E4a5875f437348b6415', + 'assets-test-sepolia', +] + +export function verifyModalSummaryMessage(entryCount, chainCount) { + cy.get(modalSummaryMessage).should( + 'contain', + `Found ${entryCount} entries on ${chainCount} ${chainCount > 1 ? 'chains' : 'chain'}`, + ) +} +export const uploadErrorMessages = { + fileType: 'File type must be text/csv', + emptyFile: 'No entries found in address book', +} + +export function verifyUploadExportMessage(msg) { + main.verifyValuesExist(uploadErrorMsg, msg) +} + +export function verifyImportBtnStatus(status) { + main.verifyElementsStatus([importBtn], status) +} + +export function verifyNumberOfRows(number) { + main.verifyElementsCount(tableRow, number) +} export function clickOnImportFileBtn() { - cy.contains(importBtn).click() + cy.contains(headerImportBtnStr).click() } -export function importFile() { - cy.get('[type="file"]').attachFile('../fixtures/address_book_test.csv') - // Import button should be enabled - cy.get('.MuiDialogActions-root').contains('Import').should('not.be.disabled') - cy.get('.MuiDialogActions-root').contains('Import').click() +export function importCSVFile(file) { + cy.get('[type="file"]').attachFile(file) } -export function verifyImportModalIsClosed() { - cy.get('Import address book').should('not.exist') +export function clickOnImportBtn() { + cy.get(importBtn).click() } -export function verifyDataImported(name, address) { - cy.contains(name).should('exist') - cy.contains(address).should('exist') +export function verifyDataImported(data) { + main.verifyValuesExist(tableContainer, data) } export function clickOnExportFileBtn() { diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index 5dd6d2bbf5..354d940178 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -29,7 +29,7 @@ const currencySection = '[data-testid="currency-section"]' const missingSignatureInfo = '[data-testid="missing-signature-info"]' const queuedTxInfo = '[data-testid="queued-tx-info"]' -export const addedSafesGnosis = ['0x17b3...98C8', '0x11A6...F1BB', '0xB8d7...642A'] +export const addedSafesEth = ['0x8675...a19b'] export const addedSafesSepolia = ['0x6d0b...6dC1', '0x5912...fFdb', '0x0637...708e', '0xD157...DE9a'] export const sideBarListItems = ['Home', 'Assets', 'Transactions', 'Address book', 'Apps', 'Settings'] export const testSafeHeaderDetails = ['2/2', constants.SEPOLIA_TEST_SAFE_13_SHORT] diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js index edfbc15ba4..49a669ce24 100644 --- a/cypress/e2e/regression/address_book.cy.js +++ b/cypress/e2e/regression/address_book.cy.js @@ -5,9 +5,11 @@ import * as constants from '../../support/constants' import * as addressBook from '../../e2e/pages/address_book.page' import * as main from '../../e2e/pages/main.page' import * as ls from '../../support/localstorage_data.js' +import * as sidebar from '../pages/sidebar.pages.js' const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' +const importedSafe = 'imported-safe' describe('Address book tests', () => { beforeEach(() => { @@ -51,8 +53,7 @@ describe('Address book tests', () => { cy.contains(constants.GNO_CSV_ENTRY.address).should('exist') }) - // TODO: Change title in Testrail. New title "...exported" - it('Verify the address book file can be downloaded', () => { + it('Verify the address book file can be exported', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.dataSet).then(() => { main .isItemInLocalstorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.dataSet) @@ -72,4 +73,37 @@ describe('Address book tests', () => { }) }) }) + + it('Verify that importing a csv file does not alter addresses in the Address book not present in the file', () => { + main + .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1) + .then(() => { + main + .isItemInLocalstorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1) + .then(() => { + cy.wait(1000) + cy.reload() + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.validCSVFile) + addressBook.clickOnImportBtn() + addressBook.verifyDataImported([constants.RECIPIENT_ADDRESS]) + }) + }) + }) + + it('Verify Safe name changes after uploading a csv file', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set4).then(() => { + main + .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.addedSafesImport) + .then(() => { + cy.wait(1000) + cy.reload() + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.addedSafesCSVFile) + addressBook.clickOnImportBtn() + sidebar.openSidebar() + sidebar.verifyAddedSafesExist([importedSafe]) + }) + }) + }) }) diff --git a/cypress/e2e/regression/sidebar_2.cy.js b/cypress/e2e/regression/sidebar_2.cy.js index fef7fab98c..87ac18b101 100644 --- a/cypress/e2e/regression/sidebar_2.cy.js +++ b/cypress/e2e/regression/sidebar_2.cy.js @@ -5,8 +5,7 @@ import * as ls from '../../support/localstorage_data.js' import * as assets from '../pages/assets.pages.js' const newSafeName = 'Added safe 3' -const oldSafeName = 'Added safe 900' -const staticSafe100 = 'Added safe 100' +const addedSafe900 = 'Added safe 900' const staticSafe200 = 'Added safe 200' describe('Sidebar added sidebar tests', () => { @@ -21,31 +20,31 @@ describe('Sidebar added sidebar tests', () => { it('Verify the safe added are listed in the sidebar', () => { sideBar.openSidebar() - sideBar.verifyAddedSafesExist(sideBar.addedSafesGnosis, sideBar.addedSafesSepolia) + sideBar.verifyAddedSafesExist(sideBar.addedSafesSepolia) }) it('Verify Safes are separated by networks', () => { sideBar.openSidebar() - sideBar.verifySafesByNetwork(constants.networks.gnosis, sideBar.addedSafesGnosis) + sideBar.verifySafesByNetwork(constants.networks.ethereum, sideBar.addedSafesEth) sideBar.verifySafesByNetwork(constants.networks.sepolia, sideBar.addedSafesSepolia) }) it('Verify a safe can be renamed', () => { sideBar.openSidebar() - sideBar.renameSafeItem(oldSafeName, newSafeName) + sideBar.renameSafeItem(addedSafe900, newSafeName) sideBar.clickOnSaveBtn() sideBar.verifySafeNameExists(newSafeName) }) it('Verify a safe can be removed', () => { sideBar.openSidebar() - sideBar.removeSafeItem(oldSafeName) - sideBar.verifySafeRemoved([oldSafeName]) + sideBar.removeSafeItem(addedSafe900) + sideBar.verifySafeRemoved([addedSafe900]) }) it('Verify the "Read only" tag if the connected user is not an owner of a safe', () => { sideBar.openSidebar() - sideBar.verifySafeReadOnlyState(staticSafe100) + sideBar.verifySafeReadOnlyState(addedSafe900) }) it('Verify Fiat currency changes when edited in the assets tab', () => { diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 2f396ab06d..90db240a85 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -6,6 +6,7 @@ import * as ls from '../../support/localstorage_data.js' const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' +const duplicateEntry = 'test-sepolia-90' describe('[SMOKE] Address book tests', () => { beforeEach(() => { @@ -37,8 +38,40 @@ describe('[SMOKE] Address book tests', () => { it('[SMOKE] Verify csv file can be imported', () => { addressBook.clickOnImportFileBtn() - addressBook.importFile() - addressBook.verifyImportModalIsClosed() - addressBook.verifyDataImported(constants.SEPOLIA_CSV_ENTRY.name, constants.SEPOLIA_CSV_ENTRY.address) + addressBook.importCSVFile(addressBook.validCSVFile) + addressBook.verifyImportBtnStatus(constants.enabledStates.enabled) + addressBook.clickOnImportBtn() + addressBook.verifyDataImported(addressBook.entries) + addressBook.verifyNumberOfRows(4) + }) + + it('[SMOKE] Import a csv file with an empty address/name/network in one row', () => { + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.emptyCSVFile) + addressBook.verifyImportBtnStatus(constants.enabledStates.disabled) + addressBook.verifyUploadExportMessage([addressBook.uploadErrorMessages.emptyFile]) + }) + + it('[SMOKE] Import a non-csv file', () => { + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.nonCSVFile) + addressBook.verifyImportBtnStatus(constants.enabledStates.disabled) + addressBook.verifyUploadExportMessage([addressBook.uploadErrorMessages.fileType]) + }) + + it('[SMOKE] Import a csv file with a repeated address and same network', () => { + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.duplicatedCSVFile) + addressBook.verifyImportBtnStatus(constants.enabledStates.enabled) + addressBook.clickOnImportBtn() + addressBook.verifyDataImported([duplicateEntry]) + addressBook.verifyNumberOfRows(1) + }) + + it('[SMOKE] Verify modal shows the amount of entries and networks detected', () => { + addressBook.clickOnImportFileBtn() + addressBook.importCSVFile(addressBook.networksCSVFile) + addressBook.verifyImportBtnStatus(constants.enabledStates.enabled) + addressBook.verifyModalSummaryMessage(4, 3) }) }) diff --git a/cypress/fixtures/address_book_addedsafes.csv b/cypress/fixtures/address_book_addedsafes.csv new file mode 100644 index 0000000000..afe54dcd76 --- /dev/null +++ b/cypress/fixtures/address_book_addedsafes.csv @@ -0,0 +1,2 @@ +address,name,chainId +0x6d0b6F96f665Bb4490f9ddb2e450Da2f7e546dC1,imported-safe,11155111 \ No newline at end of file diff --git a/cypress/fixtures/address_book_duplicated.csv b/cypress/fixtures/address_book_duplicated.csv new file mode 100644 index 0000000000..2302d1848a --- /dev/null +++ b/cypress/fixtures/address_book_duplicated.csv @@ -0,0 +1,3 @@ +address,name,chainId +0x6E834E9D04ad6b26e1525dE1a37BFd9b215f40B7,test-sepolia-9,11155111 +0x6E834E9D04ad6b26e1525dE1a37BFd9b215f40B7,test-sepolia-90,11155111 \ No newline at end of file diff --git a/cypress/fixtures/address_book_empty_test.csv b/cypress/fixtures/address_book_empty_test.csv new file mode 100644 index 0000000000..4918b1de41 --- /dev/null +++ b/cypress/fixtures/address_book_empty_test.csv @@ -0,0 +1 @@ +address,name,chainId \ No newline at end of file diff --git a/cypress/fixtures/address_book_networks.csv b/cypress/fixtures/address_book_networks.csv new file mode 100644 index 0000000000..4624dfb476 --- /dev/null +++ b/cypress/fixtures/address_book_networks.csv @@ -0,0 +1,5 @@ +address,name,chainId +0x8675B754342754A30A2AeF474D114d8460bca19b,"mainnet safe ",1 +0xB8d760a90a5ed54D3c2b3EFC231277e99188642A,"xDai Safe B8", 100 +0x91e11585c114129f3Ec940Aa648A4ac13668d0c2,"Biance safe91", 56 +0x61a0c717d18232711bC788F19C9Cd56a43cc8872,"MM account 1", 1 \ No newline at end of file diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 96c80044e6..78772bf0ce 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -317,10 +317,8 @@ export const addressBookData = { }, }, addedSafes: { - 100: { - '0x17b34aEf1428A358bA2eA360a098b8A3BEb698C8': 'Added safe 1', - '0x11A6B41322C57Bd0e56cEe06abB11A1E5c1FF1BB': 'Added safe 900', - '0xB8d760a90a5ed54D3c2b3EFC231277e99188642A': 'Added safe 100', + 1: { + '0x8675B754342754A30A2AeF474D114d8460bca19b': 'Added safe 900', }, 11155111: { '0x0A0EEb6fBCc7c82259E548Fc4617175A357b3e71': 'Added safe 200', @@ -529,22 +527,12 @@ export const addedSafes = { ethBalance: '0', }, }, - 100: { - '0x17b34aEf1428A358bA2eA360a098b8A3BEb698C8': { + 1: { + '0x8675B754342754A30A2AeF474D114d8460bca19b': { owners: [{ value: '0x11B1D54B66e5e226D6f89069c21A569A22D98cfd' }], threshold: 1, ethBalance: '0.001000002', }, - '0x11A6B41322C57Bd0e56cEe06abB11A1E5c1FF1BB': { - owners: [{ value: '0x7724b234c9099C205F03b458944942bcEBA13408' }], - threshold: 1, - ethBalance: '0', - }, - '0xB8d760a90a5ed54D3c2b3EFC231277e99188642A': { - owners: [{ value: '0x11B1D54B66e5e226D6f89069c21A569A22D98cfd' }], - threshold: 1, - ethBalance: '0.92132507668989', - }, }, }, set3: { diff --git a/src/components/address-book/ImportDialog/index.tsx b/src/components/address-book/ImportDialog/index.tsx index ac6d665eda..7b83a40d8b 100644 --- a/src/components/address-book/ImportDialog/index.tsx +++ b/src/components/address-book/ImportDialog/index.tsx @@ -128,7 +128,7 @@ const ImportDialog = ({ handleClose }: { handleClose: () => void }): ReactElemen name: acceptedFile.name, additionalInfo: formatFileSize(acceptedFile.size), summary: [ - + {`Found ${entryCount} entries on ${chainCount} ${chainCount > 1 ? 'chains' : 'chain'}`} , ], @@ -163,8 +163,16 @@ const ImportDialog = ({ handleClose }: { handleClose: () => void }): ReactElemen - - + diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 33e6258a36..30a5130830 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -135,13 +135,14 @@ function EnhancedTable({ rows, headCells, mobileVariant }: EnhancedTableProps) { return ( - + {pagedRows.length > 0 ? ( pagedRows.map((row, index) => ( +
Date: Fri, 9 Feb 2024 16:10:31 +0100 Subject: [PATCH 2/3] feat: [Counterfactual] Add backup option (#3202) * feat: Create counterfactual 1/1 safes * fix: Add feature flag * fix: Lint issues * fix: Use incremental saltNonce for all safe creations * fix: Replace useCounterfactualBalance hook with get function and write tests * refactor: Move creation logic out of Review component * fix: useLoadBalance check for undefined value * fix: Extract saltNonce, safeAddress calculation into a hook * refactor: Rename redux slice * fix: Show error message in case saltNonce can't be retrieved * feat: Add backup option for counterfactual safes * fix: Adjust wording * fix: Remove feature flag check for recovery * refactor: Extract file upload logic from safe loading component * fix: Add recover option to welcome page * fix: Fallback to readonly provider to fetch balances and cache response * fix: Navigate to dashboard after recovery * fix: Remove restore feature * fix: link prevent default --- .../dashboard/CreationDialog/index.tsx | 50 +++++++++++++++++- .../counterfactual/__tests__/utils.test.ts | 52 +++++++++++++------ src/features/counterfactual/utils.ts | 34 +++++++++--- 3 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/components/dashboard/CreationDialog/index.tsx b/src/components/dashboard/CreationDialog/index.tsx index 07a417f4e5..9133386e14 100644 --- a/src/components/dashboard/CreationDialog/index.tsx +++ b/src/components/dashboard/CreationDialog/index.tsx @@ -1,5 +1,10 @@ +import { selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import useChainId from '@/hooks/useChainId' +import useSafeInfo from '@/hooks/useSafeInfo' +import { useAppSelector } from '@/store' +import type { PredictedSafeProps } from '@safe-global/protocol-kit' import React, { type ElementType } from 'react' -import { Box, Button, Dialog, DialogContent, Grid, SvgIcon, Typography } from '@mui/material' +import { Alert, Box, Button, Dialog, DialogContent, Grid, Link, SvgIcon, Typography } from '@mui/material' import { useRouter } from 'next/router' import HomeIcon from '@/public/images/sidebar/home.svg' @@ -27,11 +32,34 @@ const HintItem = ({ Icon, title, description }: { Icon: ElementType; title: stri ) } +const getExportFileName = () => { + const today = new Date().toISOString().slice(0, 10) + return `safe-backup-${today}.json` +} + +const backupSafe = (chainId: string, safeAddress: string, undeployedSafe: PredictedSafeProps) => { + const data = JSON.stringify({ chainId, safeAddress, safeProps: undeployedSafe }, null, 2) + + const blob = new Blob([data], { type: 'text/json' }) + const link = document.createElement('a') + + link.download = getExportFileName() + link.href = window.URL.createObjectURL(blob) + link.dataset.downloadurl = ['text/json', link.download, link.href].join(':') + link.dispatchEvent(new MouseEvent('click')) + + // TODO: Track this as an event + // trackEvent(COUNTERFACTUAL_EVENTS.EXPORT_SAFE) +} + const CreationDialog = () => { const router = useRouter() const [open, setOpen] = React.useState(true) const [remoteSafeApps = []] = useRemoteSafeApps() const chain = useCurrentChain() + const chainId = useChainId() + const { safeAddress } = useSafeInfo() + const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, chainId, safeAddress)) const onClose = () => { const { [CREATION_MODAL_QUERY_PARM]: _, ...query } = router.query @@ -49,7 +77,8 @@ const CreationDialog = () => { Congratulations on your first step to truly unlock ownership. Enjoy the experience and discover our app. - + + { description="Have any questions? Check out our collection of articles." /> + + {undeployedSafe && ( + + We recommend{' '} + { + e.preventDefault() + backupSafe(chainId, safeAddress, undeployedSafe) + }} + > + backing up your Safe Account + {' '} + in case you lose access to this device. + + )} +