From 937dabb1ee603236bc5588bb0fd3d95064a7f750 Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 13 Nov 2023 12:13:53 +0100 Subject: [PATCH 01/15] fix: different configs for staging and prod --- src/services/mpc/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/mpc/config.ts b/src/services/mpc/config.ts index 5a2f4aa729..17dce21cc2 100644 --- a/src/services/mpc/config.ts +++ b/src/services/mpc/config.ts @@ -32,7 +32,7 @@ export const SOCIAL_WALLET_OPTIONS: any = (() => { const SOCIAL_WALLET_OPTIONS_STAGING = process.env.NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING || '' try { - return JSON.parse(IS_PRODUCTION ? SOCIAL_WALLET_OPTIONS_PRODUCTION : SOCIAL_WALLET_OPTIONS_PRODUCTION) + return JSON.parse(IS_PRODUCTION ? SOCIAL_WALLET_OPTIONS_PRODUCTION : SOCIAL_WALLET_OPTIONS_STAGING) } catch (error) { console.error('Error parsing SOCIAL_WALLET_OPTIONS', error) return {} From f3fc362f7cc196d58dbc3c23337e24927fc82a25 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:44:56 +0100 Subject: [PATCH 02/15] Fix: prefix switch for receive QR (#2795) --- .../balances/AssetsTable/NoAssets.tsx | 76 +++++++++++++++++++ src/components/balances/AssetsTable/index.tsx | 61 +-------------- 2 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 src/components/balances/AssetsTable/NoAssets.tsx diff --git a/src/components/balances/AssetsTable/NoAssets.tsx b/src/components/balances/AssetsTable/NoAssets.tsx new file mode 100644 index 0000000000..ebf337c6f6 --- /dev/null +++ b/src/components/balances/AssetsTable/NoAssets.tsx @@ -0,0 +1,76 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { Box, Button, FormControlLabel, Grid, Paper, Switch, Typography } from '@mui/material' +import EthHashInfo from '@/components/common/EthHashInfo' +import QRCode from '@/components/common/QRCode' +import { AppRoutes } from '@/config/routes' +import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import { useCurrentChain } from '@/hooks/useChains' +import useSafeAddress from '@/hooks/useSafeAddress' +import { useAppDispatch, useAppSelector } from '@/store' +import { selectSettings, setQrShortName } from '@/store/settingsSlice' +import AddIcon from '@mui/icons-material/Add' + +const NoAssets = () => { + const router = useRouter() + const safeAddress = useSafeAddress() + const chain = useCurrentChain() + const dispatch = useAppDispatch() + const settings = useAppSelector(selectSettings) + const qrPrefix = settings.shortName.qr ? `${chain?.shortName}:` : '' + const qrCode = `${qrPrefix}${safeAddress}` + const [apps] = useRemoteSafeApps() + + // @FIXME: use tags instead of name + const rampSafeApp = apps?.find((app) => app.name === 'Ramp Network') + + return ( + + + +
+ + + +
+ + dispatch(setQrShortName(e.target.checked))} /> + } + label={<>QR code with chain prefix} + /> +
+ + + + Add funds to get started + + + + Add funds directly from your bank account or copy your address to send tokens from a different account. + + + + + + + {rampSafeApp && ( + + + + + + )} + +
+
+ ) +} + +export default NoAssets diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 599c8c2704..57c0a28c16 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -1,15 +1,5 @@ -import EthHashInfo from '@/components/common/EthHashInfo' -import QRCode from '@/components/common/QRCode' -import { AppRoutes } from '@/config/routes' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' -import { useCurrentChain } from '@/hooks/useChains' -import useSafeAddress from '@/hooks/useSafeAddress' -import { useAppSelector } from '@/store' -import { selectSettings } from '@/store/settingsSlice' -import Link from 'next/link' -import { useRouter } from 'next/router' import { type ReactElement, useMemo, useContext } from 'react' -import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton, Paper, Grid } from '@mui/material' +import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' import css from './styles.module.css' @@ -30,7 +20,7 @@ import CheckWallet from '@/components/common/CheckWallet' import useSpendingLimit from '@/hooks/useSpendingLimit' import { TxModalContext } from '@/components/tx-flow' import TokenTransferFlow from '@/components/tx-flow/flows/TokenTransfer' -import AddIcon from '@mui/icons-material/Add' +import NoAssets from './NoAssets' const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = { asset: { @@ -271,51 +261,4 @@ const AssetsTable = ({ ) } -const NoAssets = () => { - const router = useRouter() - const safeAddress = useSafeAddress() - const chain = useCurrentChain() - const settings = useAppSelector(selectSettings) - const qrPrefix = settings.shortName.qr ? `${chain?.shortName}:` : '' - const qrCode = `${qrPrefix}${safeAddress}` - const [apps] = useRemoteSafeApps() - - const rampSafeApp = apps?.find((app) => app.name === 'Ramp Network') - - return ( - - - - - - - - - - Add funds to get started - - - Add funds directly from your bank account or copy your address to send tokens from a different account. - - - - - {rampSafeApp && ( - - - - - - )} - - - - ) -} - export default AssetsTable From 8419d23da3720e2c53f21f82a94c56205324d795 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 13 Nov 2023 15:51:48 +0100 Subject: [PATCH 03/15] fix: allow execution of web assembly (#2797) * fix: allow execution of web assembly * fix: CSP string --- src/config/securityHeaders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/securityHeaders.ts b/src/config/securityHeaders.ts index 69dd425090..8d3faa3bb1 100644 --- a/src/config/securityHeaders.ts +++ b/src/config/securityHeaders.ts @@ -15,7 +15,7 @@ export const ContentSecurityPolicy = ` script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com 'unsafe-inline' https://*.getbeamer.com https://www.googletagmanager.com https://*.ingest.sentry.io https://sentry.io ${ !IS_PRODUCTION || /* TODO: remove after moving cypress to görli and testing in staging again!! */ CYPRESS_MNEMONIC ? "'unsafe-eval'" - : '' + : "'wasm-unsafe-eval'" }; frame-src *; style-src 'self' 'unsafe-inline' https://*.getbeamer.com https://*.googleapis.com; From c8752b58731a15107a15c099a8948ce6cf9baffe Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 13 Nov 2023 15:52:05 +0100 Subject: [PATCH 04/15] 1.22.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 26ac24be3c..60669cabad 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", "type": "module", - "version": "1.22.0", + "version": "1.22.1", "scripts": { "dev": "next dev", "start": "next dev", From ced3211d6d5552e7c9087a476d6cd9f0f0db82ec Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:39:07 +0100 Subject: [PATCH 05/15] Fix: revert relaying message (#2799) --- package.json | 2 +- .../tx/ExecutionMethodSelector/index.tsx | 2 +- .../tx/SignOrExecuteForm/ExecuteForm.tsx | 25 +++--- src/components/tx/SponsoredBy/index.tsx | 87 +++++++------------ src/hooks/useWalletCanRelay.ts | 8 +- 5 files changed, 44 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 60669cabad..329f71c72e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", "type": "module", - "version": "1.22.1", + "version": "1.22.2", "scripts": { "dev": "next dev", "start": "next dev", diff --git a/src/components/tx/ExecutionMethodSelector/index.tsx b/src/components/tx/ExecutionMethodSelector/index.tsx index baae6e7855..95e56b9d4a 100644 --- a/src/components/tx/ExecutionMethodSelector/index.tsx +++ b/src/components/tx/ExecutionMethodSelector/index.tsx @@ -71,7 +71,7 @@ export const ExecutionMethodSelector = ({ - + {shouldRelay && relays ? : null} ) } diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index 628c5877f9..72ae840fc6 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -12,6 +12,7 @@ import { useIsExecutionLoop, useTxActions } from './hooks' import { useRelaysBySafe } from '@/hooks/useRemainingRelays' import useWalletCanRelay from '@/hooks/useWalletCanRelay' import { ExecutionMethod, ExecutionMethodSelector } from '../ExecutionMethodSelector' +import { hasRemainingRelays } from '@/utils/relaying' import type { SignOrExecuteProps } from '.' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import { TxModalContext } from '@/components/tx-flow' @@ -52,13 +53,15 @@ const ExecuteForm = ({ // Check that the transaction is executable const isExecutionLoop = useIsExecutionLoop() - // SC wallets can relay fully signed transactions - const [canWalletRelay] = useWalletCanRelay(safeTx) - // We default to relay + // We default to relay, but the option is only shown if we canRelay const [executionMethod, setExecutionMethod] = useState(ExecutionMethod.RELAY) + + // SC wallets can relay fully signed transactions + const [walletCanRelay] = useWalletCanRelay(safeTx) + // The transaction can/will be relayed - const willRelay = executionMethod === ExecutionMethod.RELAY - const hasRelays = !!relays?.remaining + const canRelay = walletCanRelay && hasRemainingRelays(relays) + const willRelay = canRelay && executionMethod === ExecutionMethod.RELAY // Estimate gas limit const { gasLimit, gasLimitError } = useGasLimit(safeTx) @@ -99,18 +102,12 @@ const ExecuteForm = ({ const cannotPropose = !isOwner && !onlyExecute const submitDisabled = - !safeTx || - !isSubmittable || - disableSubmit || - isValidExecutionLoading || - isExecutionLoop || - cannotPropose || - (willRelay && !hasRelays) + !safeTx || !isSubmittable || disableSubmit || isValidExecutionLoading || isExecutionLoop || cannotPropose return ( <>
-
+
- {canWalletRelay && ( + {canRelay && (
{ +const SponsoredBy = ({ relays, tooltip }: { relays: RelayResponse; tooltip?: string }) => { const chain = useCurrentChain() return ( - - {shouldRelay ? ( -
- - - Sponsored by - - - {chain?.chainName} - - {chain?.chainName} - - - {tooltip ? ( - - - - - - ) : null} - - - - Transactions per hour:{' '} - - {relays?.remaining ?? 0} of {relays?.limit ?? 0} - - {relays && !relays.remaining && ( - - {' '} - — will reset in the next hour - - )} - -
- ) : ( -
+
+ - Pay gas from the connected wallet + Sponsored by - - - Please make sure your wallet has sufficient funds. + {chain?.chainName} + + {chain?.chainName} -
- )} + {tooltip ? ( + + + + + + ) : null} + + + + Transactions per hour:{' '} + + {relays.remaining} of {relays.limit} + + +
) } diff --git a/src/hooks/useWalletCanRelay.ts b/src/hooks/useWalletCanRelay.ts index d451855b3c..e7261e0226 100644 --- a/src/hooks/useWalletCanRelay.ts +++ b/src/hooks/useWalletCanRelay.ts @@ -4,18 +4,14 @@ import useWallet from '@/hooks/wallets/useWallet' import { isSmartContractWallet } from '@/utils/wallets' import { Errors, logError } from '@/services/exceptions' import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' -import { FEATURES, hasFeature } from '@/utils/chains' -import { useCurrentChain } from './useChains' const useWalletCanRelay = (tx: SafeTransaction | undefined) => { const { safe } = useSafeInfo() const wallet = useWallet() - const chain = useCurrentChain() - const isFeatureEnabled = chain && hasFeature(chain, FEATURES.RELAYING) const hasEnoughSignatures = tx && tx.signatures.size >= safe.threshold return useAsync(() => { - if (!isFeatureEnabled || !tx || !wallet) return + if (!tx || !wallet) return return isSmartContractWallet(wallet) .then((isSCWallet) => { @@ -27,7 +23,7 @@ const useWalletCanRelay = (tx: SafeTransaction | undefined) => { logError(Errors._106, err.message) return false }) - }, [isFeatureEnabled, hasEnoughSignatures, tx, wallet]) + }, [hasEnoughSignatures, tx, wallet]) } export default useWalletCanRelay From b356d75cd4eba5af0b5aedefd7d8fc9b50869b3b Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:30:32 +0100 Subject: [PATCH 06/15] Tests: Add test IDs and modify NFT tests (#2804) * Add test IDs amd modify nft tests --- cypress/e2e/pages/main.page.js | 1 + cypress/e2e/pages/nfts.pages.js | 76 +++++++++++-------- cypress/e2e/safe-apps/tx-builder.spec.cy.js | 2 +- cypress/e2e/smoke/nfts.cy.js | 10 +-- src/components/common/ModalDialog/index.tsx | 4 +- src/components/nfts/NftGrid/index.tsx | 18 ++++- src/components/nfts/NftSendForm/index.tsx | 1 + .../tx-flow/common/TxLayout/index.tsx | 17 ++++- .../flows/NftTransfer/SendNftBatch.tsx | 21 ++++- .../tx/SignOrExecuteForm/SignForm.tsx | 2 +- 10 files changed, 105 insertions(+), 47 deletions(-) diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 438797b93c..2c6df5a423 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -1,6 +1,7 @@ import * as constants from '../../support/constants' const acceptSelection = 'Accept selection' +export const modalDialogCloseBtn = '[data-testid="modal-dialog-close-btn"]' export function clickOnSideMenuItem(item) { cy.get('p').contains(item).click() diff --git a/cypress/e2e/pages/nfts.pages.js b/cypress/e2e/pages/nfts.pages.js index 85775098cb..82796786f6 100644 --- a/cypress/e2e/pages/nfts.pages.js +++ b/cypress/e2e/pages/nfts.pages.js @@ -1,8 +1,22 @@ import * as constants from '../../support/constants' +import * as main from '../pages/main.page' -const nftModal = 'div[role="dialog"]' -const nftModalCloseBtn = 'button[aria-label="close"]' +const nftModalTitle = '[data-testid="modal-title"]' +const nftModal = '[data-testid="modal-view"]' + +const nftModalCloseBtn = main.modalDialogCloseBtn const recipientInput = 'input[name="recipient"]' +const nftsRow = '[data-testid^="nfts-table-row"]' +const inactiveNftIcon = '[data-testid="nft-icon-border"]' +const activeNftIcon = '[data-testid="nft-icon-primary"]' +const nftCheckBox = (index) => `[data-testid="nft-checkbox-${index}"] > input` +const activeSendNFTBtn = '[data-testid="nft-send-btn-false"]' +const modalTitle = '[data-testid="modal-title"]' +const modalHeader = '[data-testid="modal-header"]' +const modalSelectedNFTs = '[data-testid="selected-nfts"]' +const nftItemList = '[data-testid="nft-item-list"]' +const nftItemNane = '[data-testid="nft-item-name"]' +const signBtn = '[data-testid="sign-btn"]' const noneNFTSelected = '0 NFTs selected' const sendNFTStr = 'Send NFTs' @@ -19,7 +33,8 @@ export function clickOnNftsTab() { cy.get('p').contains('NFTs').click() } function verifyTableRows(number) { - cy.get('tbody tr').should('have.length', number) + cy.scrollTo('bottom').wait(500) + cy.get(nftsRow).should('have.length.at.least', number) } export function verifyNFTNumber(number) { @@ -27,44 +42,48 @@ export function verifyNFTNumber(number) { } export function verifyDataInTable(name, address, tokenID) { - 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(nftsRow).contains(name) + cy.get(nftsRow).contains(address) + cy.get(nftsRow).contains(tokenID) } -export function openNFT(index) { - cy.get('tbody').within(() => { - cy.get('tr').eq(index).click() - }) +export function waitForNftItems(count) { + cy.get(nftsRow).should('have.length.at.least', count) +} + +export function openActiveNFT(index) { + cy.get(activeNftIcon).eq(index).click() } export function verifyNameInNFTModal(name) { - cy.get(nftModal).contains(name) + cy.get(nftModalTitle).contains(name) } export function verifySelectedNetwrokSepolia() { - cy.get(nftModal).contains(constants.networks.sepolia) + cy.get(nftModal).within(() => { + cy.get(nftModalTitle).contains(constants.networks.sepolia) + }) } export function verifyNFTModalLink(link) { - cy.get(nftModal).contains(`a[href="${link}"]`, 'View on OpenSea') + cy.get(nftModalTitle).contains(`a[href="${link}"]`, 'View on OpenSea') } export function closeNFTModal() { cy.get(nftModalCloseBtn).click() - cy.get(nftModal).should('not.exist') + cy.get(nftModalTitle).should('not.exist') } -export function clickOn6thNFT() { - cy.get('tbody tr:nth-child(6) td:nth-child(2)').click() +export function clickOnInactiveNFT() { + cy.get(inactiveNftIcon).eq(0).click() } export function verifyNFTModalDoesNotExist() { - cy.get(nftModal).should('not.exist') + cy.get(nftModalTitle).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.get(nftCheckBox(i)).click() cy.contains(`${i} NFT${i > 1 ? 's' : ''} selected`) } cy.contains('button', `Send ${numberOfNFTs} NFT${numberOfNFTs > 1 ? 's' : ''}`) @@ -73,8 +92,8 @@ export function selectNFTs(numberOfNFTs) { export function deselectNFTs(checkboxIndexes, checkedItems) { let total = checkedItems - checkboxIndexes.length - checkboxIndexes.forEach((index) => { - cy.get(`tbody tr:nth-child(${index}) input[type="checkbox"]`).uncheck() + checkboxIndexes.forEach((i) => { + cy.get(nftCheckBox(i)).uncheck() }) cy.contains(`${total} NFT${total !== 1 ? 's' : ''} selected`) @@ -88,14 +107,12 @@ export function verifyInitialNFTData() { cy.contains('button[disabled]', 'Send') } -export function sendNFT(numberOfCheckedNFTs) { - cy.contains('button', `Send ${numberOfCheckedNFTs} NFT${numberOfCheckedNFTs !== 1 ? 's' : ''}`).click() +export function sendNFT() { + cy.get(activeSendNFTBtn).click() } export function verifyNFTModalData() { - cy.contains(sendNFTStr) - cy.contains(recipientAddressStr) - cy.contains(selectedNFTStr) + main.verifyElementsExist([modalTitle, modalHeader, modalSelectedNFTs]) } export function typeRecipientAddress(address) { @@ -107,11 +124,10 @@ export function clikOnNextBtn() { } 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])', signBtnStr) + main.verifyElementsExist([nftItemList]) + main.verifyElementsCount(nftItemNane, NFTcount) + cy.get(signBtn).should('not.be.disabled') + if (NFTcount > 1) { const numbersArr = Array.from({ length: NFTcount }, (_, index) => index + 1) numbersArr.forEach((number) => { diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js index da1bd43e40..057d12a217 100644 --- a/cypress/e2e/safe-apps/tx-builder.spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -147,7 +147,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.sendBatchStr).click() }) cy.get('p').contains('1').should('exist') - cy.get('p').contains('2').should('be.visible') + cy.get('p').contains('2').should('exist') }) it('Verify a batch cannot be created with invalid address', () => { diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index 01680d3f72..4a39e1a545 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -11,11 +11,11 @@ describe('NFTs tests', () => { cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + constants.SEPOLIA_TEST_SAFE_5) main.acceptCookies() - nfts.clickOnNftsTab() + nfts.waitForNftItems(2) }) it('Verify that NFTs exist in the table', () => { - nfts.verifyNFTNumber(20) + nfts.verifyNFTNumber(10) }) it('Verify NFT row contains data', () => { @@ -23,14 +23,14 @@ describe('NFTs tests', () => { }) it('Verify NFT preview window can be opened', () => { - nfts.openNFT(0) + nfts.openActiveNFT(0) nfts.verifyNameInNFTModal(nftsTokenID) nfts.verifySelectedNetwrokSepolia() nfts.closeNFTModal() }) it('Verify NFT open does not open if no NFT exits', () => { - nfts.clickOn6thNFT() + nfts.clickOnInactiveNFT() nfts.verifyNFTModalDoesNotExist() }) @@ -38,7 +38,7 @@ describe('NFTs tests', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(3) nfts.deselectNFTs([2], 3) - nfts.sendNFT(2) + nfts.sendNFT() nfts.verifyNFTModalData() nfts.typeRecipientAddress(constants.SEPOLIA_TEST_SAFE_4) nfts.clikOnNextBtn() diff --git a/src/components/common/ModalDialog/index.tsx b/src/components/common/ModalDialog/index.tsx index cede7fe2cf..c9d32eafe6 100644 --- a/src/components/common/ModalDialog/index.tsx +++ b/src/components/common/ModalDialog/index.tsx @@ -20,12 +20,13 @@ interface DialogTitleProps { export const ModalDialogTitle = ({ children, onClose, hideChainIndicator = false, ...other }: DialogTitleProps) => { return ( - + {children} {!hideChainIndicator && } {onClose ? ( { onClose(e, 'backdropClick') @@ -56,6 +57,7 @@ const ModalDialog = ({ return ( e.stopPropagation() const NftIndicator = ({ color }: { color: SvgIconProps['color'] }) => ( - + ) const activeNftIcon = @@ -191,7 +199,7 @@ const NftGrid = ({ const sx = item.imageUri ? { cursor: 'pointer' } : undefined return ( - + {/* Collection name */} @@ -233,7 +241,11 @@ const NftGrid = ({ {/* Checkbox */} - onCheckboxClick(e, item)} /> + onCheckboxClick(e, item)} + /> {/* Insert the children at the end of the table */} {index === filteredNfts.length - 1 && children} diff --git a/src/components/nfts/NftSendForm/index.tsx b/src/components/nfts/NftSendForm/index.tsx index edfb4f5735..9665dcd4e3 100644 --- a/src/components/nfts/NftSendForm/index.tsx +++ b/src/components/nfts/NftSendForm/index.tsx @@ -32,6 +32,7 @@ const NftSendForm = ({ selectedNfts }: NftSendFormProps): ReactElement => { {(isOk) => ( )} diff --git a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx index 3aeee4fab7..b058444f56 100644 --- a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx @@ -37,7 +37,14 @@ const NftItem = ({ image, name, description }: { image: string; name: string; de - + {name} @@ -59,7 +66,15 @@ const NftItem = ({ image, name, description }: { image: string; name: string; de export const NftItems = ({ tokens }: { tokens: SafeCollectibleResponse[] }) => { return ( - + {tokens.map((token) => ( { )} - + Selected NFTs diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx index 03cf9cbaff..410254da8e 100644 --- a/src/components/tx/SignOrExecuteForm/SignForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx @@ -103,7 +103,7 @@ const SignForm = ({ {/* Submit button */} {(isOk) => ( - )} From 34f1baeabd213db0cf877c9724f7d6d1986d17a2 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:49:35 +0100 Subject: [PATCH 07/15] chore: Analyze NextJS bundle size action (#2803) * chore: Analyze NextJS bundle size action * fix: Remove redundant caching * fix: Remove working directory --- .github/workflows/nextjs_bundle_analysis.yml | 98 ++++++++++++++++++++ package.json | 6 ++ 2 files changed, 104 insertions(+) create mode 100644 .github/workflows/nextjs_bundle_analysis.yml diff --git a/.github/workflows/nextjs_bundle_analysis.yml b/.github/workflows/nextjs_bundle_analysis.yml new file mode 100644 index 0000000000..2e9672b0df --- /dev/null +++ b/.github/workflows/nextjs_bundle_analysis.yml @@ -0,0 +1,98 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: 'Next.js Bundle Analysis' + +on: + pull_request: + push: + branches: + - dev + +permissions: + contents: read # for checkout repository + actions: read # for fetching base branch bundle stats + pull-requests: write # for comments + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + uses: ./.github/workflows/yarn + + - name: Build next.js app + uses: ./.github/workflows/build + with: + secrets: ${{ toJSON(secrets) }} + + # Here's the first place where next-bundle-analysis' own script is used + # This step pulls the raw bundle stats for the current bundle + - name: Analyze bundle + run: npx -p nextjs-bundle-analysis report + + - name: Upload bundle + uses: actions/upload-artifact@v3 + with: + name: bundle + path: .next/analyze/__bundle_analysis.json + + - name: Download base branch bundle stats + uses: dawidd6/action-download-artifact@v2 + if: success() && github.event.number + with: + workflow: nextjs_bundle_analysis.yml + branch: ${{ github.event.pull_request.base.ref }} + path: .next/analyze/base + + # And here's the second place - this runs after we have both the current and + # base branch bundle stats, and will compare them to determine what changed. + # There are two configurable arguments that come from package.json: + # + # - budget: optional, set a budget (bytes) against which size changes are measured + # it's set to 350kb here by default, as informed by the following piece: + # https://infrequently.org/2021/03/the-performance-inequality-gap/ + # + # - red-status-percentage: sets the percent size increase where you get a red + # status indicator, defaults to 20% + # + # Either of these arguments can be changed or removed by editing the `nextBundleAnalysis` + # entry in your package.json file. + - name: Compare with base branch bundle + if: success() && github.event.number + run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare + + - name: Get Comment Body + id: get-comment-body + if: success() && github.event.number + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings + run: | + echo "body<> $GITHUB_OUTPUT + echo "$(cat .next/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + + - name: Find Comment + uses: peter-evans/find-comment@v2 + if: success() && github.event.number + id: fc + with: + issue-number: ${{ github.event.number }} + body-includes: '' + + - name: Create Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.fc.outputs.comment-id == 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + + - name: Update Comment + uses: peter-evans/create-or-update-comment@v2 + if: success() && github.event.number && steps.fc.outputs.comment-id != 0 + with: + issue-number: ${{ github.event.number }} + body: ${{ steps.get-comment-body.outputs.body }} + comment-id: ${{ steps.fc.outputs.comment-id }} + edit-mode: replace diff --git a/package.json b/package.json index 329f71c72e..8cc875803d 100644 --- a/package.json +++ b/package.json @@ -135,5 +135,11 @@ "typescript": "4.9.4", "typescript-plugin-css-modules": "^4.2.2", "webpack": "^5.88.2" + }, + "nextBundleAnalysis": { + "budget": null, + "budgetPercentIncreaseRed": 20, + "minimumChangeThreshold": 0, + "showDetails": true } } From a27250031bcb2c90121aa2770687a3f764e9a23c Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:32:07 +0100 Subject: [PATCH 08/15] fix: Lazy load mpc core kit (#2808) * fix: Lazy load mpc core kit * fix: Only instantiate service once, extract address book function * fix: Add skeleton for socialSigner loading state * chore: Update bundle-analyzer action to update comment instead of creating new one --- .github/workflows/nextjs_bundle_analysis.yml | 41 +----- .gitignore | 3 +- .../common/ConnectWallet/WalletDetails.tsx | 9 +- src/components/common/SocialSigner/index.tsx | 7 +- src/components/welcome/WelcomeLogin/index.tsx | 8 +- .../wallets/mpc/__tests__/useMPC.test.ts | 134 +++++------------ src/hooks/wallets/mpc/useMPC.ts | 139 +++++++++--------- .../wallets/mpc/useRehydrateSocialWallet.ts | 68 +++++++++ src/hooks/wallets/mpc/useSocialWallet.ts | 56 +------ src/pages/_app.tsx | 4 +- src/services/mpc/SocialLoginModule.ts | 24 +-- 11 files changed, 219 insertions(+), 274 deletions(-) create mode 100644 src/hooks/wallets/mpc/useRehydrateSocialWallet.ts diff --git a/.github/workflows/nextjs_bundle_analysis.yml b/.github/workflows/nextjs_bundle_analysis.yml index 2e9672b0df..1840f8d380 100644 --- a/.github/workflows/nextjs_bundle_analysis.yml +++ b/.github/workflows/nextjs_bundle_analysis.yml @@ -28,8 +28,6 @@ jobs: with: secrets: ${{ toJSON(secrets) }} - # Here's the first place where next-bundle-analysis' own script is used - # This step pulls the raw bundle stats for the current bundle - name: Analyze bundle run: npx -p nextjs-bundle-analysis report @@ -47,19 +45,6 @@ jobs: branch: ${{ github.event.pull_request.base.ref }} path: .next/analyze/base - # And here's the second place - this runs after we have both the current and - # base branch bundle stats, and will compare them to determine what changed. - # There are two configurable arguments that come from package.json: - # - # - budget: optional, set a budget (bytes) against which size changes are measured - # it's set to 350kb here by default, as informed by the following piece: - # https://infrequently.org/2021/03/the-performance-inequality-gap/ - # - # - red-status-percentage: sets the percent size increase where you get a red - # status indicator, defaults to 20% - # - # Either of these arguments can be changed or removed by editing the `nextBundleAnalysis` - # entry in your package.json file. - name: Compare with base branch bundle if: success() && github.event.number run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare @@ -73,26 +58,8 @@ jobs: echo "$(cat .next/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT echo EOF >> $GITHUB_OUTPUT - - name: Find Comment - uses: peter-evans/find-comment@v2 - if: success() && github.event.number - id: fc - with: - issue-number: ${{ github.event.number }} - body-includes: '' - - - name: Create Comment - uses: peter-evans/create-or-update-comment@v2 - if: success() && github.event.number && steps.fc.outputs.comment-id == 0 - with: - issue-number: ${{ github.event.number }} - body: ${{ steps.get-comment-body.outputs.body }} - - - name: Update Comment - uses: peter-evans/create-or-update-comment@v2 - if: success() && github.event.number && steps.fc.outputs.comment-id != 0 + - name: Comment + uses: marocchino/sticky-pull-request-comment@v2 with: - issue-number: ${{ github.event.number }} - body: ${{ steps.get-comment-body.outputs.body }} - comment-id: ${{ steps.fc.outputs.comment-id }} - edit-mode: replace + header: next-bundle-analysis + message: ${{ steps.get-comment-body.outputs.body }} diff --git a/.gitignore b/.gitignore index 97916164f6..505387fd77 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ yalc.lock /public/worker-*.js /public/workbox-*.js /public/workbox-*.js.map -/public/fallback* \ No newline at end of file +/public/fallback* +/public/*.js.LICENSE.txt \ No newline at end of file diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 5f0442f50a..6a2ad4caf7 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,8 +1,13 @@ -import { Box, Divider, SvgIcon, Typography } from '@mui/material' +import { Box, Divider, Skeleton, SvgIcon, Typography } from '@mui/material' +import dynamic from 'next/dynamic' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -import SocialSigner from '@/components/common/SocialSigner' + +const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { + loading: () => , +}) + import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index c7f1138219..bef9d919b6 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -1,4 +1,7 @@ +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' +import { type ISocialWalletService } from '@/services/mpc/interfaces' import { Box, Button, LinearProgress, SvgIcon, Tooltip, Typography } from '@mui/material' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' import { useCallback, useContext, useMemo, useState } from 'react' import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' @@ -15,8 +18,6 @@ import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { CGW_NAMES } from '@/hooks/wallets/consts' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TxModalContext } from '@/components/tx-flow' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' import madProps from '@/utils/mad-props' import { asError } from '@/services/exceptions/utils' import ErrorMessage from '@/components/tx/ErrorMessage' @@ -41,7 +42,7 @@ const useIsSocialWalletEnabled = () => { } type SocialSignerLoginProps = { - socialWalletService: ReturnType + socialWalletService: ISocialWalletService | undefined wallet: ReturnType supportedChains: ReturnType isMPCLoginEnabled: ReturnType diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index ad768bf84a..9fcc869f44 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,9 +1,9 @@ -import SocialSigner from '@/components/common/SocialSigner' import { AppRoutes } from '@/config/routes' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' +import { Paper, SvgIcon, Typography, Divider, Link, Box, Skeleton } from '@mui/material' import SafeLogo from '@/public/images/logo-text.svg' +import dynamic from 'next/dynamic' import css from './styles.module.css' import { useRouter } from 'next/router' import WalletLogin from './WalletLogin' @@ -11,6 +11,10 @@ import { LOAD_SAFE_EVENTS, CREATE_SAFE_EVENTS } from '@/services/analytics/event import Track from '@/components/common/Track' import { trackEvent } from '@/services/analytics' +const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { + loading: () => , +}) + const WelcomeLogin = () => { const router = useRouter() const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN) diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index 2cdfd3f689..7c839a0cb8 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -1,8 +1,7 @@ import * as useOnboard from '@/hooks/wallets/useOnboard' import * as socialWalletOptions from '@/services/mpc/config' -import { renderHook, waitFor } from '@/tests/test-utils' -import { _getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' -import * as useChains from '@/hooks/useChains' +import { waitFor } from '@/tests/test-utils' +import { _getMPCCoreKitInstance, initMPC, setMPCCoreKitInstance } from '../useMPC' import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' import { hexZeroPad } from 'ethers/lib/utils' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' @@ -63,53 +62,55 @@ class EventEmittingMockProvider { } } -describe('useInitMPC', () => { +describe('initMPC', () => { + const mockOnboard = { + state: { + get: () => ({ + wallets: [], + walletModules: [], + }), + }, + } as unknown as OnboardAPI + + const mockChain = { + chainId: '5', + chainName: 'Goerli', + blockExplorerUriTemplate: { + address: 'https://goerli.someprovider.io/{address}', + txHash: 'https://goerli.someprovider.io/{txHash}', + api: 'https://goerli.someprovider.io/', + }, + nativeCurrency: { + decimals: 18, + logoUri: 'https://logo.goerli.com', + name: 'Goerli ETH', + symbol: 'ETH', + }, + rpcUri: { + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: 'https://goerli.somerpc.io', + }, + } as unknown as ChainInfo + beforeEach(() => { jest.resetAllMocks() jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) }) + it('should set the coreKit if user is not logged in yet', async () => { - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') mockWeb3AuthMpcCoreKit.mockImplementation(() => { return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, null) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(_getMPCCoreKitInstance()).toBeDefined() - expect(connectWalletSpy).not.toBeCalled() + expect(useOnboard.connectWallet).not.toBeCalled() }) }) @@ -117,33 +118,6 @@ describe('useInitMPC', () => { const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') const mockProvider = jest.fn() @@ -151,7 +125,7 @@ describe('useInitMPC', () => { return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(connectWalletSpy).toBeCalled() @@ -160,41 +134,13 @@ describe('useInitMPC', () => { }) it('should copy event handlers and emit chainChanged if the current chain is updated', async () => { - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue({ address: hexZeroPad('0x1', 20), label: ONBOARD_MPC_MODULE_LABEL, chainId: '1', provider: {} as unknown as EIP1193Provider, }) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') const mockChainChangedListener = jest.fn() @@ -215,12 +161,12 @@ describe('useInitMPC', () => { return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(mockChainChangedListener).toHaveBeenCalledWith('0x5') expect(_getMPCCoreKitInstance()).toBeDefined() - expect(connectWalletSpy).not.toBeCalled() + expect(useOnboard.connectWallet).not.toBeCalled() }) }) }) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index b0b9d8544a..c73a8e2fee 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -1,88 +1,83 @@ -import { useEffect } from 'react' +import { IS_PRODUCTION } from '@/config/constants' +import { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' import ExternalStore from '@/services/ExternalStore' -import { COREKIT_STATUS, Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' +import { SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type OnboardAPI } from '@web3-onboard/core' import { CHAIN_NAMESPACES } from '@web3auth/base' - -import { useCurrentChain } from '@/hooks/useChains' +import type { Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' import { getRpcServiceUrl } from '../web3' -import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import { useInitSocialWallet } from './useSocialWallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' -import { IS_PRODUCTION } from '@/config/constants' const { getStore, setStore, useStore } = new ExternalStore() -export const useInitMPC = () => { - const chain = useCurrentChain() - const onboard = useOnboard() - useInitSocialWallet() +export const initMPC = async (chain: ChainInfo, onboard: OnboardAPI) => { + const chainConfig = { + chainId: `0x${Number(chain.chainId).toString(16)}`, + chainNamespace: CHAIN_NAMESPACES.EIP155, + rpcTarget: getRpcServiceUrl(chain.rpcUri), + displayName: chain.chainName, + blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, + ticker: chain.nativeCurrency.symbol, + tickerName: chain.nativeCurrency.name, + } - useEffect(() => { - if (!chain || !onboard || !isSocialWalletOptions(SOCIAL_WALLET_OPTIONS)) { - return - } + const currentInstance = getStore() + let previousChainChangedListeners: Function[] = [] + if (currentInstance?.provider) { + // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId + const oldProvider = currentInstance.provider + previousChainChangedListeners = oldProvider.listeners('chainChanged') + } - const chainConfig = { - chainId: `0x${Number(chain.chainId).toString(16)}`, - chainNamespace: CHAIN_NAMESPACES.EIP155, - rpcTarget: getRpcServiceUrl(chain.rpcUri), - displayName: chain.chainName, - blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, - ticker: chain.nativeCurrency.symbol, - tickerName: chain.nativeCurrency.name, - } + const { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } = await import('@web3auth/mpc-core-kit') - const currentInstance = getStore() - let previousChainChangedListeners: Function[] = [] - if (currentInstance?.provider) { - // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId - const oldProvider = currentInstance.provider - previousChainChangedListeners = oldProvider.listeners('chainChanged') - } + const web3AuthCoreKit = new Web3AuthMPCCoreKit({ + web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, + // Available networks are "sapphire_devnet", "sapphire_mainnet" + web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, + baseUrl: `${window.location.origin}/`, + uxMode: 'popup', + enableLogging: !IS_PRODUCTION, + //@ts-ignore + chainConfig, + manualSync: true, + hashedFactorNonce: 'safe-global-sfa-nonce', + }) - const web3AuthCoreKit = new Web3AuthMPCCoreKit({ - web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, - // Available networks are "sapphire_devnet", "sapphire_mainnet" - web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, - baseUrl: `${window.location.origin}/`, - uxMode: 'popup', - enableLogging: !IS_PRODUCTION, - chainConfig, - manualSync: true, - hashedFactorNonce: 'safe-global-sfa-nonce', - }) + return web3AuthCoreKit + .init() + .then(() => { + setStore(web3AuthCoreKit) + // If rehydration was successful, connect to onboard + if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { + return web3AuthCoreKit + } - web3AuthCoreKit - .init() - .then(() => { - setStore(web3AuthCoreKit) - // If rehydration was successful, connect to onboard - if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { - return - } - const connectedWallet = getConnectedWallet(onboard.state.get().wallets) - if (!connectedWallet) { - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - } else { - const newProvider = web3AuthCoreKit.provider + const connectedWallet = getConnectedWallet(onboard.state.get().wallets) + if (!connectedWallet) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + } else { + const newProvider = web3AuthCoreKit.provider - // To propagate the changedChain we disconnect and connect - if (previousChainChangedListeners.length > 0 && newProvider) { - previousChainChangedListeners.forEach((previousListener) => - newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), - ) - newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) - } + // To propagate the changedChain we disconnect and connect + if (previousChainChangedListeners.length > 0 && newProvider) { + previousChainChangedListeners.forEach((previousListener) => + newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), + ) + newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) } - }) - .catch((error) => console.error(error)) - }, [chain, onboard]) + } + + return web3AuthCoreKit + }) + .catch((error) => console.error(error)) } export const _getMPCCoreKitInstance = getStore diff --git a/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts b/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts new file mode 100644 index 0000000000..799d7f2036 --- /dev/null +++ b/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts @@ -0,0 +1,68 @@ +import useAddressBook from '@/hooks/useAddressBook' +import useChainId from '@/hooks/useChainId' +import { useCurrentChain } from '@/hooks/useChains' +import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { useAppDispatch } from '@/store' +import { upsertAddressBookEntry } from '@/store/addressBookSlice' +import { type WalletState } from '@web3-onboard/core' +import { type UserInfo } from '@web3auth/mpc-core-kit' +import { useCallback, useEffect } from 'react' +import { checksumAddress } from '@/utils/addresses' + +const useRehydrateSocialWallet = () => { + const chain = useCurrentChain() + const onboard = useOnboard() + const currentChainId = useChainId() + const addressBook = useAddressBook() + const dispatch = useAppDispatch() + + const updateAddressBook = useCallback( + (userInfo: UserInfo | undefined, wallets: WalletState[] | undefined | void) => { + if (!userInfo || !wallets || !currentChainId || wallets.length === 0) return + + const address = wallets[0].accounts[0]?.address + if (address) { + const signerAddress = checksumAddress(address) + if (addressBook[signerAddress] === undefined) { + const email = userInfo.email + dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) + } + } + }, + [addressBook, currentChainId, dispatch], + ) + + useEffect(() => { + if (!chain || !onboard) return + + const rehydrate = async () => { + const { initMPC } = await import('./useMPC') + const { initSocialWallet } = await import('./useSocialWallet') + const mpcCoreKit = await initMPC(chain, onboard) + + if (!mpcCoreKit) return + + const socialWalletService = await initSocialWallet(mpcCoreKit) + + const onConnect = async () => { + const wallets = await connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + + // If the signer is not in the address book => add the user's email as name + const userInfo = socialWalletService?.getUserInfo() + updateAddressBook(userInfo, wallets) + } + + socialWalletService.setOnConnect(onConnect) + } + + void rehydrate() + }, [chain, onboard, updateAddressBook]) +} + +export default useRehydrateSocialWallet diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts index d98998ffdd..8db21735d6 100644 --- a/src/hooks/wallets/mpc/useSocialWallet.ts +++ b/src/hooks/wallets/mpc/useSocialWallet.ts @@ -1,59 +1,15 @@ -import useAddressBook from '@/hooks/useAddressBook' -import useChainId from '@/hooks/useChainId' import ExternalStore from '@/services/ExternalStore' import type { ISocialWalletService } from '@/services/mpc/interfaces' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import { useAppDispatch } from '@/store' -import { upsertAddressBookEntry } from '@/store/addressBookSlice' -import { checksumAddress } from '@/utils/addresses' -import { useCallback, useEffect } from 'react' -import useOnboard, { connectWallet } from '../useOnboard' -import useMpc from './useMPC' +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' const { getStore, setStore, useStore } = new ExternalStore() -export const useInitSocialWallet = () => { - const mpcCoreKit = useMpc() - const onboard = useOnboard() - const addressBook = useAddressBook() - const currentChainId = useChainId() - const dispatch = useAppDispatch() - const socialWalletService = useStore() +export const initSocialWallet = async (mpcCoreKit: Web3AuthMPCCoreKit) => { + const SocialWalletService = (await import('@/services/mpc/SocialWalletService')).default + const socialWalletService = new SocialWalletService(mpcCoreKit) + setStore(socialWalletService) - const onConnect = useCallback(async () => { - if (!onboard || !socialWalletService) return - - const wallets = await connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - - // If the signer is not in the address book => add the user's email as name - const userInfo = socialWalletService.getUserInfo() - if (userInfo && wallets && currentChainId && wallets.length > 0) { - const address = wallets[0].accounts[0]?.address - if (address) { - const signerAddress = checksumAddress(address) - if (addressBook[signerAddress] === undefined) { - const email = userInfo.email - dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) - } - } - } - }, [addressBook, currentChainId, dispatch, onboard, socialWalletService]) - - useEffect(() => { - socialWalletService?.setOnConnect(onConnect) - }, [onConnect, socialWalletService]) - - useEffect(() => { - if (mpcCoreKit) { - setStore(new SocialWalletService(mpcCoreKit)) - } - }, [mpcCoreKit]) + return socialWalletService } export const getSocialWalletService = getStore diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index e7b3d6d3d7..65dcbf5fe9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,3 +1,4 @@ +import useRehydrateSocialWallet from '@/hooks/wallets/mpc/useRehydrateSocialWallet' import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import Sentry from '@/services/sentry' // needs to be imported first import type { ReactNode } from 'react' @@ -38,7 +39,6 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' -import { useInitMPC } from '@/hooks/wallets/mpc/useMPC' import { WalletConnectProvider } from '@/services/walletconnect/WalletConnectContext' import useABTesting from '@/services/tracking/useAbTesting' import { AbTest } from '@/services/tracking/abTesting' @@ -65,7 +65,7 @@ const InitApp = (): null => { useTxTracking() useSafeMsgTracking() useBeamer() - useInitMPC() + useRehydrateSocialWallet() useABTesting(AbTest.HUMAN_DESCRIPTION) return null diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts index 6e34ce89d8..84052dfbed 100644 --- a/src/services/mpc/SocialLoginModule.ts +++ b/src/services/mpc/SocialLoginModule.ts @@ -1,14 +1,9 @@ -import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' -import { getSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' -import * as PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import { FEATURES, hasFeature } from '@/utils/chains' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' import { type EIP1193Provider } from '@web3-onboard/core' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' - -const getMPCProvider = () => _getMPCCoreKitInstance()?.provider +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' const assertDefined = (mpcProvider: T | undefined) => { if (!mpcProvider) { @@ -23,9 +18,9 @@ export const isSocialLoginWallet = (walletLabel: string | undefined) => { return walletLabel === ONBOARD_MPC_MODULE_LABEL } -const getConnectedAccounts = async () => { +const getConnectedAccounts = async (provider: typeof Web3AuthMPCCoreKit.prototype.provider | undefined) => { try { - const web3 = assertDefined(getMPCProvider()) + const web3 = assertDefined(provider) return web3.request({ method: 'eth_accounts' }) } catch (e) { throw new ProviderRpcError({ @@ -50,6 +45,13 @@ function MpcModule(chain: ChainInfo): WalletInit { label: ONBOARD_MPC_MODULE_LABEL, getIcon: async () => (await import('./icon')).default, getInterface: async () => { + const { _getMPCCoreKitInstance } = await import('@/hooks/wallets/mpc/useMPC') + const { getSocialWalletService } = await import('@/hooks/wallets/mpc/useSocialWallet') + const { COREKIT_STATUS } = await import('@web3auth/mpc-core-kit') + const { open } = await import('./PasswordRecoveryModal') + + const getMPCProvider = () => _getMPCCoreKitInstance()?.provider + const provider: EIP1193Provider = { on: (event, listener) => { const web3 = assertDefined(getMPCProvider()) @@ -85,11 +87,11 @@ function MpcModule(chain: ChainInfo): WalletInit { const status = await socialWalletService.loginAndCreate() if (status === COREKIT_STATUS.REQUIRED_SHARE) { - PasswordRecoveryModal.open(() => { - getConnectedAccounts().then(resolve).catch(reject) + open(() => { + getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) }) } else { - getConnectedAccounts().then(resolve).catch(reject) + getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) } } return From 3598c18c584ff0e24a4672c0193f4f787f8260d3 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:14:17 +0100 Subject: [PATCH 09/15] Tests: Cypress seedless google flow (#2812) * Add seedless google flow happy path tests --- cypress/e2e/pages/create_wallet.pages.js | 55 ++++++++++++++++++- cypress/e2e/pages/main.page.js | 4 ++ cypress/e2e/pages/navigation.page.js | 5 ++ cypress/e2e/pages/owners.pages.js | 6 ++ cypress/e2e/pages/safeapps.pages.js | 8 +++ .../e2e/safe-apps/browser_permissions.cy.js | 4 +- cypress/e2e/smoke/create_safe_google.cy.js | 50 +++++++++++++++++ src/components/common/SocialSigner/index.tsx | 2 + .../create/steps/ReviewStep/index.tsx | 21 +++++-- .../create/steps/StatusStep/StatusMessage.tsx | 2 +- .../create/steps/StatusStep/index.tsx | 2 +- .../safe-apps/SafeAppsInfoModal/index.tsx | 1 + .../sidebar/SidebarNavigation/config.tsx | 2 +- .../tx-flow/flows/SuccessScreen/index.tsx | 4 +- 14 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 cypress/e2e/pages/navigation.page.js create mode 100644 cypress/e2e/smoke/create_safe_google.cy.js diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ba4f9f8599..ed5fa6db89 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,4 +1,5 @@ import * as constants from '../../support/constants' +import * as main from '../pages/main.page' const welcomeLoginScreen = '[data-testid="welcome-login"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' @@ -11,12 +12,64 @@ export const removeOwnerBtn = 'button[aria-label="Remove owner"]' const connectingContainer = 'div[class*="connecting-container"]' const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' const connectWalletBtn = 'Connect wallet' - +const googleConnectBtn = '[data-testid="google-connect-btn"]' +const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' +const googleAccountInfoHeader = '[data-testid="open-account-center"]' +const reviewStepOwnerInfo = '[data-testid="review-step-owner-info"]' +const reviewStepNextBtn = '[data-testid="review-step-next-btn"]' +const safeCreationStatusInfo = '[data-testid="safe-status-info"]' +const startUsingSafeBtn = '[data-testid="start-using-safe-btn"]' +const sponsorIcon = '[data-testid="sponsor-icon"]' +const networkFeeSection = '[data-tetid="network-fee-section"]' + +const sponsorStr = 'Your account is sponsored by Goerli' +const safeCreationProcessing = 'Transaction is being executed' +const safeCreationComplete = 'Your Safe Account is being indexed' const changeNetworkWarningStr = 'Change your wallet network' const safeAccountSetupStr = 'Safe Account setup' const policy1_2 = '1/1 policy' export const walletName = 'test1-sepolia-safe' export const defaltSepoliaPlaceholder = 'Sepolia Safe' +const welcomeToSafeStr = 'Welcome to Safe' + +export function verifySafeIsBeingCreated() { + cy.get(safeCreationStatusInfo).should('have.text', safeCreationProcessing) +} + +export function verifySafeCreationIsComplete() { + cy.get(safeCreationStatusInfo).should('exist').and('have.text', safeCreationComplete) + cy.get(startUsingSafeBtn).should('exist').click() + cy.get(welcomeToSafeStr).should('exist') +} + +export function clickOnReviewStepNextBtn() { + cy.get(reviewStepNextBtn).click() +} +export function verifyOwnerInfoIsPresent() { + return cy.get(reviewStepOwnerInfo).shoul('exist') +} + +export function verifySponsorMessageIsPresent() { + main.verifyElementsExist([sponsorIcon, networkFeeSection]) + // Goerli is generated + cy.get(networkFeeSection).contains(sponsorStr).should('exist') +} + +export function verifyGoogleConnectBtnIsDisabled() { + cy.get(googleConnectBtn).should('be.disabled') +} + +export function verifyGoogleConnectBtnIsEnabled() { + cy.get(googleConnectBtn).should('not.be.disabled') +} + +export function verifyGoogleSignin() { + return cy.get(googleSignedinBtn).should('exist') +} + +export function verifyGoogleAccountInfoInHeader() { + return cy.get(googleAccountInfoHeader).should('exist') +} export function verifyPolicy1_1() { cy.contains(policy1_2).should('exist') diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 2c6df5a423..b3a4c2de03 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -1,8 +1,12 @@ import * as constants from '../../support/constants' const acceptSelection = 'Accept selection' +const executeStr = 'Execute' export const modalDialogCloseBtn = '[data-testid="modal-dialog-close-btn"]' +export function clickOnExecuteBtn() { + cy.get('button').contains(executeStr).click() +} export function clickOnSideMenuItem(item) { cy.get('p').contains(item).click() } diff --git a/cypress/e2e/pages/navigation.page.js b/cypress/e2e/pages/navigation.page.js new file mode 100644 index 0000000000..e5deb73c08 --- /dev/null +++ b/cypress/e2e/pages/navigation.page.js @@ -0,0 +1,5 @@ +export const sideNavSettingsIcon = '[data-testid="settings-nav-icon"]' + +export function clickOnSideNavigation(option) { + cy.get(option).should('exist').click() +} diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 6e2e30aef3..cb4c521b67 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -20,6 +20,7 @@ const thresholdOption = 'li[role="option"]' const existingOwnerAddressInput = (index) => `input[name="owners.${index}.address"]` const existingOwnerNameInput = (index) => `input[name="owners.${index}.name"]` const singleOwnerNameInput = 'input[name="name"]' +const finishTransactionBtn = '[data-testid="finish-transaction-btn"]' const disconnectBtnStr = 'Disconnect' const notConnectedStatus = 'Connect' @@ -31,11 +32,16 @@ const backbtnStr = 'Back' const removeOwnerStr = 'Remove owner' const selectedOwnerStr = 'Selected owner' const addNewOwnerStr = 'Add new owner' +const processedTransactionStr = 'Transaction was successful' export const safeAccountNonceStr = 'Safe Account nonce' export const nonOwnerErrorMsg = 'Your connected wallet is not an owner of this Safe Account' export const disconnectedUserErrorMsg = 'Please connect your wallet' +export function verifyOwnerTransactionComplted() { + cy.get(processedTransactionStr).should('exist') + cy.get(finishTransactionBtn).should('exist') +} export function verifyNumberOfOwners(count) { const indices = Array.from({ length: count }, (_, index) => index) const names = indices.map(existingOwnerNameInput) diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index fb7401609b..f349d2034e 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -7,6 +7,7 @@ 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 appModal = '[data-testid="app-info-modal"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -208,9 +209,16 @@ function verifyDisclaimerIsVisible() { } export function clickOnContinueBtn() { + cy.get(appModal).should('exist') return cy.findByRole('button', { name: continueBtnStr }).click().wait(1000) } +export function checkLocalStorage() { + clickOnContinueBtn().should(() => { + expect(window.localStorage.getItem(constants.BROWSER_PERMISSIONS_KEY)).to.eq(localStorageItem) + }) +} + export function verifyCameraCheckBoxExists() { cy.findByRole('checkbox', { name: cameraCheckBoxStr }).should('exist') } diff --git a/cypress/e2e/safe-apps/browser_permissions.cy.js b/cypress/e2e/safe-apps/browser_permissions.cy.js index 59cbceffed..97f816dc75 100644 --- a/cypress/e2e/safe-apps/browser_permissions.cy.js +++ b/cypress/e2e/safe-apps/browser_permissions.cy.js @@ -30,8 +30,6 @@ describe('Browser permissions tests', () => { safeapps.verifyWarningDefaultAppMsgIsDisplayed() safeapps.verifyCameraCheckBoxExists() safeapps.clickOnContinueBtn() - safeapps.clickOnContinueBtn().should(() => { - expect(window.localStorage.getItem(constants.BROWSER_PERMISSIONS_KEY)).to.eq(safeapps.localStorageItem) - }) + safeapps.checkLocalStorage() }) }) diff --git a/cypress/e2e/smoke/create_safe_google.cy.js b/cypress/e2e/smoke/create_safe_google.cy.js new file mode 100644 index 0000000000..f29ded27e1 --- /dev/null +++ b/cypress/e2e/smoke/create_safe_google.cy.js @@ -0,0 +1,50 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' +import * as navigation from '../pages/navigation.page' + +describe('Safe creation Google tests', () => { + beforeEach(() => { + cy.visit(constants.welcomeUrl + '?chain=gor') + cy.clearLocalStorage() + main.acceptCookies() + // TODO: Need credentials to finish API Google login + // createwallet.loginGoogleAPI() + }) + + it('Verify that "Connect with Google" option is disabled for the networks without Relay on the Welcome page', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + createwallet.selectNetwork(constants.networks.polygon) + createwallet.verifyGoogleConnectBtnIsDisabled() + }) + + it.skip('Verify a successful connection with google', () => { + createwallet.verifyGoogleSignin() + }) + + it.skip('Verify Google account info in the header after account connection', () => { + createwallet.verifyGoogleAccountInfoInHeader() + }) + + it.skip('Verify a successful safe creation with a Google account', { defaultCommandTimeout: 90000 }, () => { + createwallet.verifyGoogleSignin().click() + createwallet.verifyOwnerInfoIsPresent() + createwallet.clickOnReviewStepNextBtn() + createwallet.verifySafeIsBeingCreated() + createwallet.verifySafeCreationIsComplete() + }) + + it.skip('Verify a successful transaction creation with Google account', { defaultCommandTimeout: 90000 }, () => { + createwallet.verifyGoogleSignin().click() + createwallet.clickOnReviewStepNextBtn() + createwallet.verifySafeCreationIsComplete() + navigation.clickOnSideNavigation(navigation.sideNavSettingsIcon) + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + main.clickOnExecuteBtn() + owner.verifyOwnerTransactionComplted() + }) +}) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index bef9d919b6..0282008027 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -114,6 +114,7 @@ export const SocialSigner = ({ {isSocialLogin && userInfo ? ( - diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index f9b469476e..90300a14bc 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -64,7 +64,7 @@ const StatusMessage = ({ status, isError }: { status: SafeCreationStatus; isErro return ( <> - + {stepInfo.description} diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 642b2ccd05..30c1cba83f 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -105,7 +105,7 @@ export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRender - diff --git a/src/components/safe-apps/SafeAppsInfoModal/index.tsx b/src/components/safe-apps/SafeAppsInfoModal/index.tsx index 5cf4ba7f35..924c2597fc 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/index.tsx +++ b/src/components/safe-apps/SafeAppsInfoModal/index.tsx @@ -112,6 +112,7 @@ const SafeAppsInfoModal = ({ return ( ({ width: '450px', backgroundColor: palette.background.paper, diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 359ec63826..cb613e1f8c 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -44,7 +44,7 @@ export const navItems: NavItem[] = [ }, { label: 'Settings', - icon: , + icon: , href: AppRoutes.settings.setup, }, ] diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index c7556fc201..6ee5b332c6 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -71,13 +71,13 @@ export const SuccessScreen = ({ txId }: { txId: string }) => {
{txLink && ( - )} -
From 0b7d378a18baadab053431256ec49fb52c231be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Thu, 16 Nov 2023 22:45:48 +0100 Subject: [PATCH 10/15] Revert Dockerfile optimization (#2817) - As environment variables are picked during build phase, we need to revert the dockerfile until we find a solution (it's better that the image takes 30 minutes to run that it doesn't work) - Build docker image for ARM64. That should make Apple computers run the image faster --- .github/workflows/deploy-dockerhub.yml | 55 +++++++++++++++++++------- Dockerfile | 35 ++++++---------- scripts/github/deploy_docker.sh | 24 ----------- 3 files changed, 51 insertions(+), 63 deletions(-) delete mode 100644 scripts/github/deploy_docker.sh diff --git a/.github/workflows/deploy-dockerhub.yml b/.github/workflows/deploy-dockerhub.yml index db4c810127..a565ca8779 100644 --- a/.github/workflows/deploy-dockerhub.yml +++ b/.github/workflows/deploy-dockerhub.yml @@ -13,25 +13,50 @@ on: jobs: dockerhub-push: runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || (github.event_name == 'release' && github.event.action == 'released') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - uses: docker/setup-buildx-action@v3 - name: Dockerhub login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Deploy Dockerhub main + - name: Deploy Main if: github.ref == 'refs/heads/main' - run: bash scripts/github/deploy_docker.sh staging - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} - - name: Deploy Dockerhub dev + uses: docker/build-push-action@v5 + with: + push: true + tags: safeglobal/safe-wallet-web:staging + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Develop if: github.ref == 'refs/heads/dev' - run: bash scripts/github/deploy_docker.sh dev - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} - - name: Deploy Dockerhub tag - if: startsWith(github.ref, 'refs/tags/') - run: bash scripts/github/deploy_docker.sh ${GITHUB_REF##*/} - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} + uses: docker/build-push-action@v5 + with: + push: true + tags: safeglobal/safe-wallet-web:dev + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Tag + if: (github.event_name == 'release' && github.event.action == 'released') + uses: docker/build-push-action@v5 + with: + push: true + tags: | + safeglobal/safe-wallet-web:${{ github.event.release.tag_name }} + safeglobal/safe-wallet-web:latest + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 9e8cba5245..82391964d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,21 @@ -FROM node:18-alpine AS base -ENV NEXT_TELEMETRY_DISABLED 1 - -FROM base AS builder - +FROM node:18-alpine RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers WORKDIR /app - -# Install dependencies -COPY package.json yarn.lock* ./ -RUN yarn --frozen-lockfile COPY . . -RUN yarn run after-install -RUN yarn build - -# Production image -FROM base AS runner -WORKDIR /app +# install deps +RUN yarn install --frozen-lockfile +RUN yarn after-install ENV NODE_ENV production -ENV REVERSE_PROXY_UI_PORT 8080 -RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs -COPY --from=builder /app/out ./out - -# Set the correct permission for prerender cache -RUN mkdir .next && chown nextjs:nodejs .next +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED 1 -USER nextjs +EXPOSE 3000 -EXPOSE ${REVERSE_PROXY_UI_PORT} +ENV PORT 3000 -CMD npx -y serve out -p ${REVERSE_PROXY_UI_PORT} +CMD ["yarn", "static-serve"] diff --git a/scripts/github/deploy_docker.sh b/scripts/github/deploy_docker.sh deleted file mode 100644 index 832a4878e5..0000000000 --- a/scripts/github/deploy_docker.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -export DOCKER_BUILDKIT=1 - -if [ "$1" = "dev" -o "$1" = "main" ]; then - # If image does not exist, don't use cache - docker pull safeglobal/$DOCKERHUB_PROJECT:$1 && \ - docker build -t $DOCKERHUB_PROJECT . --cache-from safeglobal/$DOCKERHUB_PROJECT:$1 --build-arg BUILDKIT_INLINE_CACHE=1 || \ - docker build -t $DOCKERHUB_PROJECT . --build-arg BUILDKIT_INLINE_CACHE=1 -else - # Building tag version from staging image (vX.X.X) - docker pull safeglobal/$DOCKERHUB_PROJECT:staging && \ - docker build -t $DOCKERHUB_PROJECT . --cache-from safeglobal/$DOCKERHUB_PROJECT:staging --build-arg BUILDKIT_INLINE_CACHE=1 || \ - docker build -t $DOCKERHUB_PROJECT . --build-arg BUILDKIT_INLINE_CACHE=1 - # Only push latest on release - case $1 in v*) - docker tag $DOCKERHUB_PROJECT safeglobal/$DOCKERHUB_PROJECT:latest - docker push safeglobal/$DOCKERHUB_PROJECT:latest - esac -fi -docker tag $DOCKERHUB_PROJECT safeglobal/$DOCKERHUB_PROJECT:$1 -docker push safeglobal/$DOCKERHUB_PROJECT:$1 From 9727a410b70ebdd1fbc7a8c43b47682f77e6fde3 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:48:49 +0300 Subject: [PATCH 11/15] Chore: prod build on staging (main) (#2809) --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f33c09fb11..a17a1b1c72 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,6 +40,7 @@ jobs: - uses: ./.github/workflows/build with: secrets: ${{ toJSON(secrets) }} + prod: ${{ github.ref == 'refs/heads/main' }} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 From ac480ec7cc8b9a080d9f8935f2b9b946fcbb02ba Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:57:32 +0100 Subject: [PATCH 12/15] fix: Update to new recommended nonce endpoint (#2737) --- package.json | 2 +- src/components/tx/SignOrExecuteForm/hooks.ts | 6 +++--- src/services/tx/tx-sender/recommendedNonce.ts | 15 ++++++++------- yarn.lock | 7 ++++++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 8cc875803d..90ca35975a 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@safe-global/safe-core-sdk-utils": "^1.7.4", "@safe-global/safe-deployments": "1.25.0", "@safe-global/safe-ethers-lib": "^1.9.4", - "@safe-global/safe-gateway-typescript-sdk": "^3.12.0", + "@safe-global/safe-gateway-typescript-sdk": "^3.13.2", "@safe-global/safe-modules-deployments": "^1.0.0", "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.74.0", diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index ca40563468..acf79a1baa 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -15,7 +15,7 @@ import { import { useHasPendingTxs } from '@/hooks/usePendingTxs' import type { ConnectedWallet } from '@/services/onboard' import type { OnboardAPI } from '@web3-onboard/core' -import { getSafeTxGas, getRecommendedNonce } from '@/services/tx/tx-sender/recommendedNonce' +import { getSafeTxGas, getNonces } from '@/services/tx/tx-sender/recommendedNonce' import useAsync from '@/hooks/useAsync' import { useUpdateBatch } from '@/hooks/useDraftBatch' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' @@ -163,9 +163,9 @@ export const useRecommendedNonce = (): number | undefined => { async () => { if (!safe.chainId || !safeAddress) return - const recommendedNonce = await getRecommendedNonce(safe.chainId, safeAddress) + const nonces = await getNonces(safe.chainId, safeAddress) - return recommendedNonce !== undefined ? Math.max(safe.nonce, recommendedNonce) : undefined + return nonces?.recommendedNonce }, // eslint-disable-next-line react-hooks/exhaustive-deps [safeAddress, safe.chainId, safe.txQueuedTag], // update when tx queue changes diff --git a/src/services/tx/tx-sender/recommendedNonce.ts b/src/services/tx/tx-sender/recommendedNonce.ts index 9874a5f5b8..164d7179b2 100644 --- a/src/services/tx/tx-sender/recommendedNonce.ts +++ b/src/services/tx/tx-sender/recommendedNonce.ts @@ -1,9 +1,12 @@ -import type { SafeTransactionEstimation } from '@safe-global/safe-gateway-typescript-sdk' -import { Operation, postSafeGasEstimation } from '@safe-global/safe-gateway-typescript-sdk' +import { + Operation, + postSafeGasEstimation, + getNonces as fetchNonces, + type SafeTransactionEstimation, +} from '@safe-global/safe-gateway-typescript-sdk' import type { MetaTransactionData, SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types' import { isLegacyVersion } from '@/hooks/coreSDK/safeCoreSDK' import { Errors, logError } from '@/services/exceptions' -import { EMPTY_DATA } from '@safe-global/safe-core-sdk/dist/src/utils/constants' const fetchRecommendedParams = async ( chainId: string, @@ -37,11 +40,9 @@ export const getSafeTxGas = async ( } } -export const getRecommendedNonce = async (chainId: string, safeAddress: string): Promise => { - const blankTxParams = { data: EMPTY_DATA, to: safeAddress, value: '0' } +export const getNonces = async (chainId: string, safeAddress: string) => { try { - const estimation = await fetchRecommendedParams(chainId, safeAddress, blankTxParams) - return Number(estimation.recommendedNonce) + return fetchNonces(chainId, safeAddress) } catch (e) { logError(Errors._616, e) } diff --git a/yarn.lock b/yarn.lock index 04aa30539d..63af5d5650 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3876,7 +3876,12 @@ "@safe-global/safe-core-sdk-utils" "^1.7.4" ethers "5.7.2" -"@safe-global/safe-gateway-typescript-sdk@^3.12.0", "@safe-global/safe-gateway-typescript-sdk@^3.5.3": +"@safe-global/safe-gateway-typescript-sdk@^3.13.2": + version "3.13.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.13.2.tgz#f03884c7eb766f5508085d95ab96063a28e20920" + integrity sha512-kGlJecJHBzGrGTq/yhLANh56t+Zur6Ubpt+/w03ARX1poDb4TM8vKU3iV8tuYpk359PPWp+Qvjnqb9oW2YQcYw== + +"@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.12.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.12.0.tgz#aa767a32f4d10f4ec9a47ad7e32d547d3b51e94c" integrity sha512-hExCo62lScVC9/ztVqYEYL2pFxcqLTvB8fj0WtdP5FWrvbtEgD0pbVolchzD5bf85pbzvEwdAxSVS7EdCZxTNw== From 0b5939cb5e8e8184a6463fa5a54727e55499da71 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:00:32 +0300 Subject: [PATCH 13/15] Fix: WC verification wording (#2798) --- .../walletconnect/WcProposalForm/ProposalVerification.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx b/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx index cbd0e66582..48cad9ae5b 100644 --- a/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx +++ b/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx @@ -29,7 +29,7 @@ const Validation: { }, INVALID: { color: 'error', - desc: 'has been flagged as a high risk by WalletConnect.', + desc: 'has a domain that does not match the sender of this request. Approving it may result in a loss of funds.', Icon: CloseIcon, }, } From 4cd3f3ce364139ec5238ae11684f38106ce4548a Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:02:49 +0100 Subject: [PATCH 14/15] Tests: Cypress tests imrpovements (#2816) * Update tests structure * Increase containers for parallel execution * Update workflows * Add test ids --- .github/workflows/ondemand.yml | 45 +++++ .github/workflows/regression.yml | 48 +++++ .github/workflows/safe-apps-e2e.yml | 2 +- .github/workflows/{e2e.yml => smoke.yml} | 4 +- cypress.config.js | 12 +- cypress/e2e/create_safe.cy.js | 21 -- cypress/e2e/custom_apps.cy.js | 29 --- cypress/e2e/intercom.cy.js | 44 ----- cypress/e2e/non_owner_spending_limit.cy.js | 54 ----- cypress/e2e/pages/balances.pages.js | 2 +- cypress/e2e/pages/create_wallet.pages.js | 11 +- cypress/e2e/pages/main.page.js | 10 + cypress/e2e/pages/navigation.page.js | 2 + cypress/e2e/pages/owners.pages.js | 15 +- cypress/e2e/regression/add_owner.cy.js | 55 ++++++ cypress/e2e/regression/address_book.cy.js | 65 ++++++ cypress/e2e/regression/assets.cy.js | 185 ++++++++++++++++++ .../e2e/{smoke => regression}/balances.cy.js | 0 .../balances_pagination.cy.js | 0 cypress/e2e/regression/batch_tx.cy.js | 33 ++++ .../e2e/{smoke => regression}/beamer.cy.js | 0 .../create_safe_google.cy.js | 0 .../e2e/regression/create_safe_simple.cy.js | 123 ++++++++++++ .../import_export_data.cy.js | 3 +- cypress/e2e/regression/load_safe.cy.js | 81 ++++++++ cypress/e2e/regression/nfts.cy.js | 28 +++ .../pending_actions.cy.js | 0 cypress/e2e/regression/remove_owner.cy.js | 41 ++++ cypress/e2e/regression/replace_owner.cy.js | 71 +++++++ cypress/e2e/regression/tx_history.cy.js | 31 +++ cypress/e2e/smoke/add_owner.cy.js | 65 ++---- cypress/e2e/smoke/address_book.cy.js | 56 +----- cypress/e2e/smoke/assets.cy.js | 178 +---------------- cypress/e2e/smoke/batch_tx.cy.js | 24 +-- cypress/e2e/smoke/create_safe_simple.cy.js | 119 +---------- cypress/e2e/smoke/create_tx.cy.js | 8 +- cypress/e2e/smoke/dashboard.cy.js | 10 +- cypress/e2e/smoke/landing.cy.js | 4 +- cypress/e2e/smoke/load_safe.cy.js | 56 +----- cypress/e2e/smoke/nfts.cy.js | 21 +- cypress/e2e/smoke/remove_owner.cy.js | 41 +--- cypress/e2e/smoke/replace_owner.cy.js | 66 +------ cypress/e2e/smoke/tx_history.cy.js | 13 +- cypress/e2e/spending_limit.cy.js | 54 ----- cypress/e2e/tx_modal.cy.js | 164 ---------------- cypress/e2e/tx_simulation.cy.js | 62 ------ cypress/support/constants.js | 2 + .../create/steps/OwnerPolicyStep/index.tsx | 10 +- .../create/steps/ReviewStep/index.tsx | 2 +- .../create/steps/SetNameStep/index.tsx | 4 +- .../settings/owner/OwnerList/index.tsx | 1 + .../tx-flow/flows/AddOwner/ChooseOwner.tsx | 4 +- .../flows/RemoveOwner/SetThreshold.tsx | 2 +- src/pages/settings/setup.tsx | 2 +- 54 files changed, 936 insertions(+), 1047 deletions(-) create mode 100644 .github/workflows/ondemand.yml create mode 100644 .github/workflows/regression.yml rename .github/workflows/{e2e.yml => smoke.yml} (94%) delete mode 100644 cypress/e2e/create_safe.cy.js delete mode 100644 cypress/e2e/custom_apps.cy.js delete mode 100644 cypress/e2e/intercom.cy.js delete mode 100644 cypress/e2e/non_owner_spending_limit.cy.js create mode 100644 cypress/e2e/regression/add_owner.cy.js create mode 100644 cypress/e2e/regression/address_book.cy.js create mode 100644 cypress/e2e/regression/assets.cy.js rename cypress/e2e/{smoke => regression}/balances.cy.js (100%) rename cypress/e2e/{smoke => regression}/balances_pagination.cy.js (100%) create mode 100644 cypress/e2e/regression/batch_tx.cy.js rename cypress/e2e/{smoke => regression}/beamer.cy.js (100%) rename cypress/e2e/{smoke => regression}/create_safe_google.cy.js (100%) create mode 100644 cypress/e2e/regression/create_safe_simple.cy.js rename cypress/e2e/{smoke => regression}/import_export_data.cy.js (95%) create mode 100644 cypress/e2e/regression/load_safe.cy.js create mode 100644 cypress/e2e/regression/nfts.cy.js rename cypress/e2e/{smoke => regression}/pending_actions.cy.js (100%) create mode 100644 cypress/e2e/regression/remove_owner.cy.js create mode 100644 cypress/e2e/regression/replace_owner.cy.js create mode 100644 cypress/e2e/regression/tx_history.cy.js delete mode 100644 cypress/e2e/spending_limit.cy.js delete mode 100644 cypress/e2e/tx_modal.cy.js delete mode 100644 cypress/e2e/tx_simulation.cy.js diff --git a/.github/workflows/ondemand.yml b/.github/workflows/ondemand.yml new file mode 100644 index 0000000000..77fd680078 --- /dev/null +++ b/.github/workflows/ondemand.yml @@ -0,0 +1,45 @@ +name: Regression on demand tests + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-20.04 + name: Cypress Regression on demand tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/yarn + + - name: Install Cypress + run: | + ./node_modules/.bin/cypress install + + - uses: ./.github/workflows/build + with: + secrets: ${{ toJSON(secrets) }} + e2e_mnemonic: ${{ secrets.NEXT_PUBLIC_CYPRESS_MNEMONIC }} + + - name: Serve + run: yarn serve & + + - uses: cypress-io/github-action@v4 + with: + parallel: true + spec: cypress/e2e/**/*.cy.js + browser: chrome + record: true + config: baseUrl=http://localhost:8080 + group: 'Regression on demand tests' + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml new file mode 100644 index 0000000000..82719bbe95 --- /dev/null +++ b/.github/workflows/regression.yml @@ -0,0 +1,48 @@ +name: Regression tests + +on: + pull_request: + branches: + - 'release**' + - 'release/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-20.04 + name: Cypress Regression tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3] + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/yarn + + - name: Install Cypress + run: | + ./node_modules/.bin/cypress install + + - uses: ./.github/workflows/build + with: + secrets: ${{ toJSON(secrets) }} + e2e_mnemonic: ${{ secrets.NEXT_PUBLIC_CYPRESS_MNEMONIC }} + + - name: Serve + run: yarn serve & + + - uses: cypress-io/github-action@v4 + with: + parallel: true + spec: cypress/e2e/**/*.cy.js + browser: chrome + record: true + config: baseUrl=http://localhost:8080 + group: 'Regression tests' + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/safe-apps-e2e.yml b/.github/workflows/safe-apps-e2e.yml index 7053c80d4d..35c0add6ad 100644 --- a/.github/workflows/safe-apps-e2e.yml +++ b/.github/workflows/safe-apps-e2e.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - containers: [1, 2, 3] + containers: [1, 2] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/e2e.yml b/.github/workflows/smoke.yml similarity index 94% rename from .github/workflows/e2e.yml rename to .github/workflows/smoke.yml index fc2c709f25..d2d0ff5acc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/smoke.yml @@ -1,4 +1,4 @@ -name: e2e +name: Smoke tests on: pull_request: @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - containers: [1, 2, 3] + containers: [1, 2, 3, 4, 5] steps: - uses: actions/checkout@v3 diff --git a/cypress.config.js b/cypress.config.js index ff3bf4baf8..cb3c4e200d 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,4 +1,5 @@ import { defineConfig } from 'cypress' +import * as fs from 'fs' export default defineConfig({ projectId: 'exhdra', @@ -8,8 +9,17 @@ export default defineConfig({ runMode: 1, openMode: 0, }, - e2e: { + setupNodeEvents(on, config) { + on('after:spec', (spec, results) => { + if (results && results.video) { + const failures = results.tests.some((test) => test.attempts.some((attempt) => attempt.state === 'failed')) + if (!failures) { + fs.unlinkSync(results.video) + } + } + }) + }, baseUrl: 'http://localhost:3000', testIsolation: false, hideXHR: true, diff --git a/cypress/e2e/create_safe.cy.js b/cypress/e2e/create_safe.cy.js deleted file mode 100644 index 3906b002d2..0000000000 --- a/cypress/e2e/create_safe.cy.js +++ /dev/null @@ -1,21 +0,0 @@ -describe('Create Safe', () => { - it('should create a new Safe', () => { - cy.visit('/welcome') - - // Close cookie banner - cy.contains('button', 'Accept all').click() - - // Ensure wallet is connected to correct chain via header - cy.contains('E2E Wallet @ Goerli') - - cy.contains('Create new Safe').click() - - // Name - cy.wait(1000) // Wait for form default values to populate - cy.contains('button', 'Next').click() - - // Owners and confirmations - cy.wait(1000) // Wait for form default values to populate - cy.contains('button', 'Next').click() - }) -}) diff --git a/cypress/e2e/custom_apps.cy.js b/cypress/e2e/custom_apps.cy.js deleted file mode 100644 index 4c008309bc..0000000000 --- a/cypress/e2e/custom_apps.cy.js +++ /dev/null @@ -1,29 +0,0 @@ -const appUrl = 'https://safe-custom-app.com' - -describe('When visiting a custom Safe App', () => { - before(() => { - cy.fixture('safe-app').then((html) => { - cy.intercept('GET', `${appUrl}`, html) - cy.intercept('GET', `${appUrl}/manifest.json`, { - name: 'Cypress Test App', - description: 'Cypress Test App Description', - icons: [{ src: 'logo.svg', sizes: 'any', type: 'image/svg+xml' }], - }) - }) - - cy.visitSafeApp(`${appUrl}`) - cy.wait(1000) - cy.contains('button', 'Accept all').click() - }) - - it('should show the custom app warning', () => { - cy.findByRole('heading', { content: /warning/i }) - cy.findByText('https://safe-custom-app.com') - cy.findByText('Check the link you are using') - - cy.findByRole('checkbox').should('exist').click() - cy.findByRole('button', { name: /continue/i }).click() - cy.reload() - cy.findByRole('heading', { content: /warning/i }).should('not.exist') - }) -}) diff --git a/cypress/e2e/intercom.cy.js b/cypress/e2e/intercom.cy.js deleted file mode 100644 index 0067f6e812..0000000000 --- a/cypress/e2e/intercom.cy.js +++ /dev/null @@ -1,44 +0,0 @@ -const RINKEBY_TEST_SAFE = 'rin:0xFfDC1BcdeC18b1196e7FA04246295DE3A17972Ac' -const intercomIframe = 'iframe[id="intercom-frame"]' -const intercomButton = '[aria-label="Open Intercom Messenger"]' -const fakeIntercomButton = 'img[alt="Open Intercom"]' - -describe('Intercom and cookie prefs', () => { - it('should show the Intercom chat if cookies are enabled', () => { - cy.visit('/') - - // Don't accept Intercom cookies - cy.contains('a', 'Accept selection').click() - - // Click on the Intercom button - cy.get(fakeIntercomButton).click() - - // Cookie preferences appear - cy.contains( - 'You attempted to open the customer support chat. Please accept the community support & updates cookies', - ) - - cy.contains('a', 'Accept all').click() - - // Intercom is now active - cy.get(intercomIframe).should('exist') - cy.get(fakeIntercomButton).should('not.exist') - cy.get(intercomButton).click() - cy.get('#intercom-container').should('exist') - - // Intercom should be disabled on a Safe App page - cy.visit(`/${RINKEBY_TEST_SAFE}/apps?appUrl=https://safe-apps.dev.gnosisdev.com/drain-safe`) - cy.get(intercomButton).should('not.exist') - - // Go to Settings and change the cookie settings - cy.visit(`/${RINKEBY_TEST_SAFE}/settings`) - cy.get(intercomIframe).should('exist') - cy.contains('button', 'Preferences').click() - cy.contains('Community support & updates').click() - cy.contains('a', 'Accept selection').click() - - // Intercom should be now disabled - cy.get(intercomButton).should('not.exist') - cy.get(fakeIntercomButton).should('be.visible') - }) -}) diff --git a/cypress/e2e/non_owner_spending_limit.cy.js b/cypress/e2e/non_owner_spending_limit.cy.js deleted file mode 100644 index 581a9f426b..0000000000 --- a/cypress/e2e/non_owner_spending_limit.cy.js +++ /dev/null @@ -1,54 +0,0 @@ -const HW_WALLET = '0xff6E053fBf4E5895eDec09146Bc10f705E8c4b3D' -const SPENDING_LIMIT_SAFE = 'gor:0xBE3C5aFF7f66c23fe71c3047911f9Aa0026b281B' - -describe('Check non-owner spending limit beneficiary modal', () => { - before(() => { - cy.visit(`/${SPENDING_LIMIT_SAFE}/home`, { failOnStatusCode: false }) - - cy.contains('Accept selection').click() - }) - - it('should open the spending limit modal', () => { - // Assert that "New transaction" button is visible - cy.contains('New transaction', { - timeout: 60_000, // `lastWallet` takes a while initialize in CI - }).should('be.visible') - - // Open the new transaction modal - cy.contains('New transaction').click() - }) - - it('should draft a spending limit transaction', () => { - // Modal is open - cy.contains('h2', 'New transaction').should('be.visible') - - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(SPENDING_LIMIT_SAFE) - - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]').contains('Görli Ether').click() - - // Insert max amount - cy.contains('Spending Limit Transaction (100 GOR)').click() - - // Insert max amount - cy.contains('Max').click() - - cy.contains('Next').click() - }) - - it('should review the spending limit transaction', () => { - cy.contains( - 'Spending limit transactions only appear in the interface once they are successfully processed and indexed. Pending transactions can only be viewed in your signer wallet application or under your wallet address on a Blockchain Explorer.', - ) - - // Alias for New transaction modal - cy.contains('h2', 'Review transaction').parents('div').as('modal') - - // Estimation is loaded - cy.get('button[type="submit"]').should('not.be.disabled') - }) -}) diff --git a/cypress/e2e/pages/balances.pages.js b/cypress/e2e/pages/balances.pages.js index 0cb82caeb1..ebd08d0de9 100644 --- a/cypress/e2e/pages/balances.pages.js +++ b/cypress/e2e/pages/balances.pages.js @@ -22,7 +22,7 @@ const hiddenTokenIcon = 'svg[data-testid="VisibilityOffOutlinedIcon"]' const hideTokenDefaultString = 'Hide tokens' const assetNameSortBtnStr = 'Asset' const assetBalanceSortBtnStr = 'Balance' -const sendBtnStr = 'Send' +export const sendBtnStr = 'Send' const sendTokensStr = 'Send tokens' const pageRowsDefault = '25' diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ed5fa6db89..38ff86234e 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -14,13 +14,15 @@ const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' const connectWalletBtn = 'Connect wallet' const googleConnectBtn = '[data-testid="google-connect-btn"]' const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' -const googleAccountInfoHeader = '[data-testid="open-account-center"]' +export const accountInfoHeader = '[data-testid="open-account-center"]' const reviewStepOwnerInfo = '[data-testid="review-step-owner-info"]' const reviewStepNextBtn = '[data-testid="review-step-next-btn"]' const safeCreationStatusInfo = '[data-testid="safe-status-info"]' const startUsingSafeBtn = '[data-testid="start-using-safe-btn"]' const sponsorIcon = '[data-testid="sponsor-icon"]' const networkFeeSection = '[data-tetid="network-fee-section"]' +const nextBtn = '[data-testid="next-btn"]' +const backBtn = '[data-testid="back-btn"]' const sponsorStr = 'Your account is sponsored by Goerli' const safeCreationProcessing = 'Transaction is being executed' @@ -32,6 +34,9 @@ export const walletName = 'test1-sepolia-safe' export const defaltSepoliaPlaceholder = 'Sepolia Safe' const welcomeToSafeStr = 'Welcome to Safe' +export function clickOnBackBtn() { + cy.get(backBtn).should('be.enabled').click() +} export function verifySafeIsBeingCreated() { cy.get(safeCreationStatusInfo).should('have.text', safeCreationProcessing) } @@ -68,7 +73,7 @@ export function verifyGoogleSignin() { } export function verifyGoogleAccountInfoInHeader() { - return cy.get(googleAccountInfoHeader).should('exist') + return cy.get(accountInfoHeader).should('exist') } export function verifyPolicy1_1() { @@ -128,7 +133,7 @@ export function selectNetwork(network, regex = false) { } export function clickOnNextBtn() { - cy.contains('button', 'Next').click() + cy.get(nextBtn).should('be.enabled').click() } export function verifyOwnerName(name, index) { diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index b3a4c2de03..5cd87d4d05 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -11,6 +11,16 @@ export function clickOnSideMenuItem(item) { cy.get('p').contains(item).click() } +export function waitForTrnsactionHistoryToComplete() { + cy.intercept('GET', constants.transactionHistoryEndpoint).as('History') + cy.wait('@History') +} + +export function waitForSafeListRequestToComplete() { + cy.intercept('GET', constants.safeListEndpoint).as('Safes') + cy.wait('@Safes') +} + export function acceptCookies(index = 0) { cy.wait(1000) diff --git a/cypress/e2e/pages/navigation.page.js b/cypress/e2e/pages/navigation.page.js index e5deb73c08..8dd0ab339f 100644 --- a/cypress/e2e/pages/navigation.page.js +++ b/cypress/e2e/pages/navigation.page.js @@ -1,4 +1,6 @@ export const sideNavSettingsIcon = '[data-testid="settings-nav-icon"]' +export const setupSection = '[data-testid="setup-section"]' +export const modalBackBtn = '[data-testid="modal-back-btn"]' export function clickOnSideNavigation(option) { cy.get(option).should('exist').click() diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index cb4c521b67..f0276a325b 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -1,11 +1,12 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' +import * as createWallet from '../pages/create_wallet.pages' +import * as navigation from '../pages/navigation.page' const copyToClipboardBtn = 'button[aria-label="Copy to clipboard"]' const tooltipLabel = (label) => `span[aria-label="${label}"]` const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' const replaceOwnerBtn = 'span[data-track="settings: Replace owner"] > span > button' -const addOwnerBtn = 'span[data-track="settings: Add owner"] > button' const tooltip = 'div[role="tooltip"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' const sentinelStart = 'div[data-testid="sentinelStart"]' @@ -21,12 +22,13 @@ const existingOwnerAddressInput = (index) => `input[name="owners.${index}.addres const existingOwnerNameInput = (index) => `input[name="owners.${index}.name"]` const singleOwnerNameInput = 'input[name="name"]' const finishTransactionBtn = '[data-testid="finish-transaction-btn"]' +const addOwnerBtn = '[data-testid="add-owner-btn"]' +const addOwnerNextBtn = '[data-testid="add-owner-next-btn"]' const disconnectBtnStr = 'Disconnect' const notConnectedStatus = 'Connect' const e2eWalletStr = 'E2E Wallet' const max50charsLimitStr = 'Maximum 50 symbols' -const nextBtnStr = 'Next' const executeBtnStr = 'Execute' const backbtnStr = 'Back' const removeOwnerStr = 'Remove owner' @@ -160,12 +162,11 @@ export function clickOnConnectBtn() { } export function waitForConnectionStatus() { - cy.get('div').contains(e2eWalletStr) + cy.get(createWallet.accountInfoHeader).should('exist') } export function openAddOwnerWindow() { - cy.get('span').contains(addNewOwnerStr).click() - cy.wait(1000) + cy.get(addOwnerBtn).should('be.enabled').click() cy.get(newOwnerName).should('be.visible') cy.get(newOwnerAddress).should('be.visible') } @@ -203,11 +204,11 @@ export function verifyNewOwnerName(name) { } export function clickOnNextBtn() { - cy.get('button').contains(nextBtnStr).click() + cy.get(addOwnerNextBtn).should('be.enabled').click() } export function clickOnBackBtn() { - cy.get('button').contains(backbtnStr).click() + cy.get(navigation.modalBackBtn).should('be.enabled').click() } export function verifyConfirmTransactionWindowDisplayed() { diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js new file mode 100644 index 0000000000..2bb15fc757 --- /dev/null +++ b/cypress/e2e/regression/add_owner.cy.js @@ -0,0 +1,55 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as owner from '../pages/owners.pages' +import * as addressBook from '../pages/address_book.page' + +describe('Add Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + cy.clearLocalStorage() + main.acceptCookies() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.waitForConnectionStatus() + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyAddOwnerBtnIsDisabled() + }) + + it('Verify the Add New Owner Form can be opened', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + }) + + it('Verify error message displayed if character limit is exceeded in Name input', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify that the "Name" field is auto-filled with the relevant name from Address Book', () => { + cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + addressBook.clickOnCreateEntryBtn() + addressBook.typeInName(constants.addresBookContacts.user1.name) + addressBook.typeInAddress(constants.addresBookContacts.user1.address) + addressBook.clickOnSaveEntryBtn() + addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.addresBookContacts.user1.address) + owner.selectNewOwner(constants.addresBookContacts.user1.name) + owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) + }) + + it('Verify that Name field not mandatory', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + owner.verifyConfirmTransactionWindowDisplayed() + }) +}) diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js new file mode 100644 index 0000000000..bd7ac23a16 --- /dev/null +++ b/cypress/e2e/regression/address_book.cy.js @@ -0,0 +1,65 @@ +import 'cypress-file-upload' +const path = require('path') +import { format } from 'date-fns' +import * as constants from '../../support/constants' +import * as addressBook from '../../e2e/pages/address_book.page' +import * as main from '../../e2e/pages/main.page' + +const NAME = 'Owner1' +const EDITED_NAME = 'Edited Owner1' + +describe('Address book tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + main.acceptCookies() + }) + + //TODO: Use localstorage for setting up/deleting entries + it('Verify entered entry in Name input can be saved', () => { + addressBook.clickOnCreateEntryBtn() + addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) + addressBook.clickOnEditEntryBtn() + addressBook.typeInNameInput(EDITED_NAME) + addressBook.clickOnSaveButton() + addressBook.verifyNameWasChanged(NAME, EDITED_NAME) + }) + + //TODO: Rework to use Polygon. Replace Verify csv file can be imported (Goerli) with this test + it.skip('Verify that Sepolia and Polygon addresses can be imported', () => { + // Go to a Safe on Gnosis Chain + cy.get('header') + .contains(/^G(ö|oe)rli$/) + .click() + cy.contains('Gnosis Chain').click() + + // Navigate to the Address Book page + cy.visit(`/address-book?safe=${constants.GNO_TEST_SAFE}`) + + // Waits for the Address Book table to be in the page + cy.contains('p', 'Address book').should('be.visible') + + // Finds the imported Gnosis Chain address + cy.contains(constants.GNO_CSV_ENTRY.name).should('exist') + cy.contains(constants.GNO_CSV_ENTRY.address).should('exist') + }) + + // TODO: Rework with localstorage. Change title in Testrail. New title "...exported" + it('Verify the address book file can be downloaded', () => { + addressBook.clickOnImportFileBtn() + addressBook.importFile() + // Download the export file + const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) + const fileName = `safe-address-book-${date}.csv` //name that is given to the file automatically + + addressBook.clickOnExportFileBtn() + //This is the submit button for the Export modal. It requires an actuall class or testId to differentiate + //from the Export button at the top of the AB table + addressBook.confirmExport() + + const downloadsFolder = Cypress.config('downloadsFolder') + //File reading is failing in the CI. Can be tested locally + cy.readFile(path.join(downloadsFolder, fileName)).should('exist') + // TODO: Add verifications on address and chain from file, import and export files should be equal including all chains from origin + }) +}) diff --git a/cypress/e2e/regression/assets.cy.js b/cypress/e2e/regression/assets.cy.js new file mode 100644 index 0000000000..d50cb2be18 --- /dev/null +++ b/cypress/e2e/regression/assets.cy.js @@ -0,0 +1,185 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as balances from '../pages/balances.pages' +import * as owner from '../pages/owners.pages' + +const ASSET_NAME_COLUMN = 0 +const TOKEN_AMOUNT_COLUMN = 1 +const FIAT_AMOUNT_COLUMN = 2 + +describe('Assets tests', () => { + const fiatRegex = balances.fiatRegex + + beforeEach(() => { + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) + cy.clearLocalStorage() + main.acceptCookies() + }) + + it('Verify that non-native tokens are present and have balance', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyBalance(balances.currencyDaiCap, TOKEN_AMOUNT_COLUMN, balances.currencyDaiAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyDaiCap, + balances.currencyDaiFormat_2, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyAave, TOKEN_AMOUNT_COLUMN, balances.currencyAaveAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyAave, + balances.currentcyAaveFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyLink, TOKEN_AMOUNT_COLUMN, balances.currencyLinkAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyLink, + balances.currentcyLinkFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyTestTokenA, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenAAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyTestTokenA, + balances.currentcyTestTokenAFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyTestTokenB, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenBAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyTestTokenB, + balances.currentcyTestTokenBFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyUSDC, TOKEN_AMOUNT_COLUMN, balances.currencyTestUSDCAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyUSDC, + balances.currentcyTestUSDCFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + }) + + it('Verify that every token except the native token has a "go to blockexplorer link"', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + // Specifying true for Sepolia. Will delete the flag once completely migrate to Sepolia + balances.verifyAssetNameHasExplorerLink(balances.currencyUSDC, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenB, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenA, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyLink, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyAave, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyDaiCap, ASSET_NAME_COLUMN, true) + balances.verifyAssetExplorerLinkNotAvailable(constants.tokenNames.sepoliaEther, ASSET_NAME_COLUMN) + }) + + it('Verify the default Fiat currency and the effects after changing it', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyFirstRowDoesNotContainCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) + balances.verifyFirstRowContainsCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) + balances.clickOnCurrencyDropdown() + balances.selectCurrency(balances.currencyEUR) + balances.verifyFirstRowDoesNotContainCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) + balances.verifyFirstRowContainsCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) + }) + + it('Verify that a tool tip is shown pointing to "Token list" dropdown', () => { + //Spam warning message is removed in beforeEach hook + cy.reload() + }) + + it('Verify that checking the checkboxes increases the token selected counter', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.checkTokenCounter(1) + }) + + it('Verify that selecting tokens and saving hides them from the table', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.saveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink]) + }) + + it('Verify that Cancel closes the menu and does not change the table status', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.saveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.cancelSaveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) + }) + + it('Verify that Deselect All unchecks all tokens from the list', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.deselecAlltHiddenTokenSelection() + balances.verifyEachRowHasCheckbox(constants.checkboxStates.unchecked) + }) + + it('Verify the Hidden tokens counter works for spam tokens', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.saveHiddenTokenSelection() + balances.checkHiddenTokenBtnCounter(1) + }) + + it('Verify the Hidden tokens counter works for native tokens', () => { + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(constants.tokenNames.sepoliaEther) + balances.saveHiddenTokenSelection() + balances.checkHiddenTokenBtnCounter(1) + }) + + it('Verify you can hide tokens from the eye icon in the table rows', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.hideAsset(balances.currencyLink) + }) + + it('Verify the sorting of "Assets" and "Balance" in the table', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyTableRows(7) + balances.clickOnTokenNameSortBtn() + balances.verifyTokenNamesOrder() + balances.clickOnTokenNameSortBtn() + balances.verifyTokenNamesOrder('descending') + balances.clickOnTokenBalanceSortBtn() + balances.verifyTokenBalanceOrder() + balances.clickOnTokenBalanceSortBtn() + balances.verifyTokenBalanceOrder('descending') + }) + + // TODO: New title: "Verify that when owner is disconnected, Send button is disabled". + // Modify test accordingly. Include in smoke. + it('Verify that the Send button shows when hovering a row', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.showSendBtn(0) + owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) + // Removed the part that checks for a non owner error message in the tooltip + // because the safe has no assets, and we don't have a safe with assets where e2e wallet is not an owner + }) +}) diff --git a/cypress/e2e/smoke/balances.cy.js b/cypress/e2e/regression/balances.cy.js similarity index 100% rename from cypress/e2e/smoke/balances.cy.js rename to cypress/e2e/regression/balances.cy.js diff --git a/cypress/e2e/smoke/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js similarity index 100% rename from cypress/e2e/smoke/balances_pagination.cy.js rename to cypress/e2e/regression/balances_pagination.cy.js diff --git a/cypress/e2e/regression/batch_tx.cy.js b/cypress/e2e/regression/batch_tx.cy.js new file mode 100644 index 0000000000..7ae147fee7 --- /dev/null +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -0,0 +1,33 @@ +import * as batch from '../pages/batches.pages' +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as owner from '../../e2e/pages/owners.pages.js' + +const currentNonce = 3 +const funds_first_tx = '0.001' +const funds_second_tx = '0.002' + +describe('Batch transaction tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) + owner.waitForConnectionStatus() + main.acceptCookies() + }) + + // TODO: Check if localstorage can be used to add batches + // Rework test + it('Verify the Add batch button is present in a transaction form', () => { + //The "true" is to validate that the add to batch button is not visible if "Yes, execute" is selected + batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + }) + + it('Verify a second transaction can be added to the batch', () => { + batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + cy.wait(1000) + batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + batch.verifyBatchIconCount(2) + batch.clickOnBatchCounter() + batch.verifyAmountTransactionsInBatch(2) + }) +}) diff --git a/cypress/e2e/smoke/beamer.cy.js b/cypress/e2e/regression/beamer.cy.js similarity index 100% rename from cypress/e2e/smoke/beamer.cy.js rename to cypress/e2e/regression/beamer.cy.js diff --git a/cypress/e2e/smoke/create_safe_google.cy.js b/cypress/e2e/regression/create_safe_google.cy.js similarity index 100% rename from cypress/e2e/smoke/create_safe_google.cy.js rename to cypress/e2e/regression/create_safe_google.cy.js diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js new file mode 100644 index 0000000000..f8627e89d9 --- /dev/null +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -0,0 +1,123 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' + +describe('Safe creation tests', () => { + beforeEach(() => { + cy.visit(constants.welcomeUrl + '?chain=sep') + main.waitForSafeListRequestToComplete() + cy.clearLocalStorage() + main.acceptCookies() + }) + + it('Verify Next button is disabled until switching to network is done', () => { + owner.waitForConnectionStatus() + createwallet.selectNetwork(constants.networks.ethereum) + createwallet.clickOnCreateNewSafeBtn() + createwallet.checkNetworkChangeWarningMsg() + createwallet.verifyNextBtnIsDisabled() + createwallet.selectNetwork(constants.networks.sepolia) + createwallet.verifyNextBtnIsEnabled() + }) + + // TODO: Check unit tests + it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.typeWalletName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + createwallet.clearWalletName() + }) + + // TODO: Replace wallet with Safe + // TODO: Check unit tests + it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.typeWalletName(main.generateRandomString(50)) + owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify current connected account is shown as default owner', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) + }) + + // TODO: Check unit tests + it('Verify error message is displayed if owner name input exceeds 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + }) + + // TODO: Check unit tests + it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(50)) + owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify data persistence', () => { + const ownerName = 'David' + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.clickOnAddNewOwnerBtn() + createwallet.typeOwnerName(ownerName, 1) + createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) + createwallet.clickOnBackBtn() + createwallet.clearWalletName() + createwallet.typeWalletName(createwallet.walletName) + createwallet.clickOnNextBtn() + createwallet.clickOnNextBtn() + createwallet.verifySafeNameInSummaryStep(createwallet.walletName) + createwallet.verifyOwnerNameInSummaryStep(ownerName) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyThresholdStringInSummaryStep(1, 2) + createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) + createwallet.clickOnBackBtn() + createwallet.clickOnBackBtn() + cy.wait(1000) + createwallet.clickOnNextBtn() + createwallet.clickOnNextBtn() + createwallet.verifySafeNameInSummaryStep(createwallet.walletName) + createwallet.verifyOwnerNameInSummaryStep(ownerName) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyThresholdStringInSummaryStep(1, 2) + createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) + createwallet.verifyEstimatedFeeInSummaryStep() + }) + + it('Verify tip is displayed on right side for threshold 1/1', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.verifyPolicy1_1() + }) + + // TODO: Check unit tests + it('Verify address input validation rules', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.clickOnAddNewOwnerBtn() + createwallet.typeOwnerAddress(main.generateRandomString(10), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) + + createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownerAdded) + + createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + createwallet.typeOwnerAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.failedResolve) + }) +}) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/regression/import_export_data.cy.js similarity index 95% rename from cypress/e2e/smoke/import_export_data.cy.js rename to cypress/e2e/regression/import_export_data.cy.js index 2d99d5d9b7..379314b48a 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/regression/import_export_data.cy.js @@ -36,6 +36,7 @@ describe('Import Export Data tests', () => { file.verifyAppsAreVisible(appNames) }) + // TODO: Revisit logic it('Verify imported data in settings', () => { const unchecked = [file.prependChainPrefixStr, file.copyAddressStr] const checked = [file.darkModeStr] @@ -45,7 +46,7 @@ describe('Import Export Data tests', () => { file.verifyCheckboxes(checked, true) }) - it('Verifies data for export in Data tab', () => { + it('Verify data for export in Data tab', () => { file.clickOnShowMoreTabsBtn() file.verifDataTabBtnIsVisible() file.clickOnDataTab() diff --git a/cypress/e2e/regression/load_safe.cy.js b/cypress/e2e/regression/load_safe.cy.js new file mode 100644 index 0000000000..3099d130d6 --- /dev/null +++ b/cypress/e2e/regression/load_safe.cy.js @@ -0,0 +1,81 @@ +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_ADDRESS_ERROR_MSG = 'Address given is not a valid Safe address' + +// TODO +const OWNER_ENS_DEFAULT_NAME = 'test20.eth' +const OWNER_ADDRESS = constants.EOA + +describe('Load Safe tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.loadNewSafeSepoliaUrl) + main.acceptCookies() + cy.wait(2000) + }) + + it('Verify a network can be selected in the Safe', () => { + safe.clickNetworkSelector(constants.networks.sepolia) + safe.selectPolygon() + cy.wait(2000) + safe.clickNetworkSelector(constants.networks.polygon) + safe.selectSepolia() + }) + + // TODO: Devide by positive and negative tests + it('Verify only valid Safe name can be accepted', () => { + // alias the address input label + cy.get('input[name="address"]').parent().prev('label').as('addressLabel') + + createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) + safe.verifyIncorrectAddressErrorMessage() + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + + // Type an invalid address + // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) + // cy.get('@addressLabel').contains(INVALID_ADDRESS_ERROR_MSG) + + // Type a ENS name + // TODO: register a goerli ENS name for the test Safe + // cy.get('input[name="address"]').clear().type(SAFE_ENS_NAME) + // giving time to the ENS name to be translated + // cy.get('input[name="address"]', { timeout: 10000 }).should('have.value', `rin:${SAFE_ENS_NAME_TRANSLATED}`) + + // Uploading a QR code + // TODO: fix this + // cy.findByTestId('QrCodeIcon').click() + // cy.contains('Upload an image').click() + // cy.get('[type="file"]').attachFile('../fixtures/goerli_safe_QR.png') + + safe.verifyAddressInputValue() + safe.clickOnNextBtn() + }) + + it('Verify custom name in the first owner can be set', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + }) + + it('Verify Safe and owner names are displayed in the Review step', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + safe.verifyDataInReviewSection(testSafeName, testOwnerName) + safe.clickOnAddBtn() + }) +}) diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js new file mode 100644 index 0000000000..74f7791d24 --- /dev/null +++ b/cypress/e2e/regression/nfts.cy.js @@ -0,0 +1,28 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as nfts from '../pages/nfts.pages' + +const nftsName = 'CatFactory' +const nftsAddress = '0x373B...866c' +const nftsTokenID = 'CF' + +describe('NFTs tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.balanceNftsUrl + constants.SEPOLIA_TEST_SAFE_5) + main.acceptCookies() + nfts.waitForNftItems(2) + }) + + // TODO: Add Sign action + it('Verify multipls NFTs can be selected and reviewed', () => { + nfts.verifyInitialNFTData() + nfts.selectNFTs(3) + nfts.deselectNFTs([2], 3) + nfts.sendNFT() + nfts.verifyNFTModalData() + nfts.typeRecipientAddress(constants.SEPOLIA_TEST_SAFE_4) + nfts.clikOnNextBtn() + nfts.verifyReviewModalData(2) + }) +}) diff --git a/cypress/e2e/smoke/pending_actions.cy.js b/cypress/e2e/regression/pending_actions.cy.js similarity index 100% rename from cypress/e2e/smoke/pending_actions.cy.js rename to cypress/e2e/regression/pending_actions.cy.js diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js new file mode 100644 index 0000000000..84758dc45a --- /dev/null +++ b/cypress/e2e/regression/remove_owner.cy.js @@ -0,0 +1,41 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as owner from '../pages/owners.pages' + +describe('Remove Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) + main.waitForTrnsactionHistoryToComplete() + cy.clearLocalStorage() + main.acceptCookies() + owner.waitForConnectionStatus() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify that "Remove" icon is visible', () => { + owner.verifyRemoveBtnIsEnabled().should('have.length', 2) + }) + + it('Verify Tooltip displays correct message for Non-Owner', () => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_4) + main.waitForTrnsactionHistoryToComplete() + owner.waitForConnectionStatus() + owner.verifyRemoveBtnIsDisabled() + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyRemoveBtnIsDisabled() + }) + + it('Verify owner removal form can be opened', () => { + owner.openRemoveOwnerWindow(1) + }) + + it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { + owner.openRemoveOwnerWindow(1) + owner.verifyThresholdLimit(1, 1) + owner.getThresholdOptions().should('have.length', 1) + }) +}) diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js new file mode 100644 index 0000000000..4d3465983c --- /dev/null +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -0,0 +1,71 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as owner from '../pages/owners.pages' +import * as addressBook from '../pages/address_book.page' + +describe('Replace Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + cy.clearLocalStorage() + main.acceptCookies() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.waitForConnectionStatus() + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyReplaceBtnIsDisabled() + }) + + // TODO: Check unit tests + it('Verify max characters in name field', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + }) + + // TODO: Rework with localstorage + it('Verify that Address input auto-fills with related value', () => { + cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + addressBook.clickOnCreateEntryBtn() + addressBook.typeInName(constants.addresBookContacts.user1.name) + addressBook.typeInAddress(constants.addresBookContacts.user1.address) + addressBook.clickOnSaveEntryBtn() + addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(constants.addresBookContacts.user1.address) + owner.selectNewOwner(constants.addresBookContacts.user1.name) + owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) + }) + + it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + owner.verifyConfirmTransactionWindowDisplayed() + }) + + it('Verify relevant error messages are displayed in Address input', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(main.generateRandomString(10)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) + + owner.typeOwnerAddress(constants.addresBookContacts.user1.address.toUpperCase()) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafe) + + owner.typeOwnerAddress(constants.addresBookContacts.user1.address.replace('F', 'f')) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) + }) +}) diff --git a/cypress/e2e/regression/tx_history.cy.js b/cypress/e2e/regression/tx_history.cy.js new file mode 100644 index 0000000000..ea29756d19 --- /dev/null +++ b/cypress/e2e/regression/tx_history.cy.js @@ -0,0 +1,31 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createTx from '../pages/create_tx.pages' + +const OUTGOING = 'Sent' + +const str1 = 'Received' +const str2 = 'Executed' +const str3 = 'Transaction hash' + +describe('Transaction history tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + // Go to the test Safe transaction history + cy.visit(constants.transactionsHistoryUrl + constants.SEPOLIA_TEST_SAFE_5) + + // So that tests that rely on this feature don't randomly fail + cy.window().then((win) => win.localStorage.setItem('SAFE_v2__AB_human-readable', true)) + + main.acceptCookies() + }) + + it('Verify transaction can be expanded/collapsed', () => { + createTx.clickOnTransactionExpandableItem('Oct 20, 2023', () => { + createTx.verifyTransactionStrExists(str1) + createTx.verifyTransactionStrExists(str2) + createTx.verifyTransactionStrExists(str3) + createTx.clickOnExpandIcon() + }) + }) +}) diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index 2397881b65..a4588e261c 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -1,68 +1,29 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' -import * as addressBook from '../pages/address_book.page' +import * as navigation from '../pages/navigation.page' -describe('Add Owners tests', () => { +describe('[SMOKE] Add Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() + main.waitForTrnsactionHistoryToComplete() main.acceptCookies() - cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + main.verifyElementsExist([navigation.setupSection]) }) - it('Verify the presence of "Add Owner" button', () => { + it('[SMOKE] Verify the presence of "Add Owner" button', () => { owner.verifyAddOwnerBtnIsEnabled() }) - it('Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { + it('[SMOKE] Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_2) + main.waitForTrnsactionHistoryToComplete() owner.verifyAddOwnerBtnIsDisabled() }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyAddOwnerBtnIsDisabled() - }) - - it('Verify the Add New Owner Form can be opened', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - }) - - it('Verify error message displayed if character limit is exceeded in Name input', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify that the "Name" field is auto-filled with the relevant name from Address Book', () => { - cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - addressBook.clickOnCreateEntryBtn() - addressBook.typeInName(constants.addresBookContacts.user1.name) - addressBook.typeInAddress(constants.addresBookContacts.user1.address) - addressBook.clickOnSaveEntryBtn() - addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.addresBookContacts.user1.address) - owner.selectNewOwner(constants.addresBookContacts.user1.name) - owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) - }) - - it('Verify that Name field not mandatory', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - owner.verifyConfirmTransactionWindowDisplayed() - }) - - it('Verify relevant error messages are displayed in Address input', () => { + // TODO: Check if this test is covered with unit tests + it('[SMOKE] Verify relevant error messages are displayed in Address input', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) @@ -81,22 +42,20 @@ describe('Add Owners tests', () => { owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) - it('Verify default threshold value. Verify correct threshold calculation', () => { + it('[SMOKE] Verify default threshold value. Verify correct threshold calculation', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) owner.verifyThreshold(1, 2) }) - it('Verify valid Address validation', () => { + it('[SMOKE] Verify valid Address validation', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() - cy.reload() - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() + owner.clickOnBackBtn() owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_2) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index b0af66eecd..27093faa55 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -1,6 +1,4 @@ import 'cypress-file-upload' -const path = require('path') -import { format } from 'date-fns' import * as constants from '../../support/constants' import * as addressBook from '../../e2e/pages/address_book.page' import * as main from '../../e2e/pages/main.page' @@ -8,28 +6,21 @@ import * as main from '../../e2e/pages/main.page' const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' -describe('Address book tests', () => { +describe('[SMOKE] Address book tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + main.waitForTrnsactionHistoryToComplete() main.acceptCookies() }) - it('Verify entry can be added', () => { + it('[SMOKE] Verify entry can be added', () => { addressBook.clickOnCreateEntryBtn() addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) }) - it('Verify entered entry in Name input can be saved', () => { - addressBook.clickOnCreateEntryBtn() - addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) - addressBook.clickOnEditEntryBtn() - addressBook.typeInNameInput(EDITED_NAME) - addressBook.clickOnSaveButton() - addressBook.verifyNameWasChanged(NAME, EDITED_NAME) - }) - - it('Verify entry can be deleted', () => { + //TODO: Use localstorage for setting up/deleting entries + it('[SMOKE] Verify entry can be deleted', () => { addressBook.clickOnCreateEntryBtn() addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) // Click the delete button in the first entry @@ -38,45 +29,10 @@ describe('Address book tests', () => { addressBook.verifyEditedNameNotExists(EDITED_NAME) }) - it('Verify csv file can be imported (Goerli)', () => { + 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) }) - - it.skip('Verify Gnosis Chain imported address can be found', () => { - // Go to a Safe on Gnosis Chain - cy.get('header') - .contains(/^G(ö|oe)rli$/) - .click() - cy.contains('Gnosis Chain').click() - - // Navigate to the Address Book page - cy.visit(`/address-book?safe=${constants.GNO_TEST_SAFE}`) - - // Waits for the Address Book table to be in the page - cy.contains('p', 'Address book').should('be.visible') - - // Finds the imported Gnosis Chain address - cy.contains(constants.GNO_CSV_ENTRY.name).should('exist') - cy.contains(constants.GNO_CSV_ENTRY.address).should('exist') - }) - - it('Verify the address book file can be downloaded', () => { - addressBook.clickOnImportFileBtn() - addressBook.importFile() - // Download the export file - const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) - const fileName = `safe-address-book-${date}.csv` //name that is given to the file automatically - - addressBook.clickOnExportFileBtn() - //This is the submit button for the Export modal. It requires an actuall class or testId to differentiate - //from the Export button at the top of the AB table - addressBook.confirmExport() - - 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/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index ebdf8cad3a..7eb9e311f2 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -1,13 +1,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as balances from '../pages/balances.pages' -import * as owner from '../pages/owners.pages' const ASSET_NAME_COLUMN = 0 const TOKEN_AMOUNT_COLUMN = 1 const FIAT_AMOUNT_COLUMN = 2 -describe('Assets tests', () => { +describe('[SMOKE] Assets tests', () => { const fiatRegex = balances.fiatRegex beforeEach(() => { @@ -16,99 +15,15 @@ describe('Assets tests', () => { main.acceptCookies() }) - it('Verify that the token tab is selected by default and the table is visible', () => { + it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { balances.verifyTokensTabIsSelected('true') }) - it('Verify that the native token is visible', () => { + it('[SMOKE] Verify that the native token is visible', () => { balances.verifyTokenIsPresent(constants.tokenNames.sepoliaEther) }) - it('Verify that non-native tokens are present and have balance', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyBalance(balances.currencyDaiCap, TOKEN_AMOUNT_COLUMN, balances.currencyDaiAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyDaiCap, - balances.currencyDaiFormat_2, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyAave, TOKEN_AMOUNT_COLUMN, balances.currencyAaveAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyAave, - balances.currentcyAaveFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyLink, TOKEN_AMOUNT_COLUMN, balances.currencyLinkAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyLink, - balances.currentcyLinkFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyTestTokenA, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenAAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyTestTokenA, - balances.currentcyTestTokenAFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyTestTokenB, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenBAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyTestTokenB, - balances.currentcyTestTokenBFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyUSDC, TOKEN_AMOUNT_COLUMN, balances.currencyTestUSDCAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyUSDC, - balances.currentcyTestUSDCFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - }) - - it('Verify that every token except the native token has a "go to blockexplorer link"', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - // Specifying true for Sepolia. Will delete the flag once completely migrate to Sepolia - balances.verifyAssetNameHasExplorerLink(balances.currencyUSDC, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenB, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenA, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyLink, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyAave, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyDaiCap, ASSET_NAME_COLUMN, true) - balances.verifyAssetExplorerLinkNotAvailable(constants.tokenNames.sepoliaEther, ASSET_NAME_COLUMN) - }) - - it('Verify the default Fiat currency and the effects after changing it', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyFirstRowDoesNotContainCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) - balances.verifyFirstRowContainsCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) - balances.clickOnCurrencyDropdown() - balances.selectCurrency(balances.currencyEUR) - balances.verifyFirstRowDoesNotContainCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) - balances.verifyFirstRowContainsCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) - }) - - it('Verify that a tool tip is shown pointing to "Token list" dropdown', () => { - //Spam warning message is removed in beforeEach hook - cy.reload() - }) - - it('Verify that Token list dropdown down options show/hide spam tokens', () => { + it('[SMOKE] Verify that Token list dropdown down options show/hide spam tokens', () => { let spamTokens = [ balances.currencyAave, balances.currencyTestTokenA, @@ -124,95 +39,14 @@ describe('Assets tests', () => { main.verifyValuesExist(balances.tokenListTable, spamTokens) }) - it('Verify that "Hide token" button is present and opens the "Hide tokens menu"', () => { + it('[SMOKE] Verify that "Hide token" button is present and opens the "Hide tokens menu"', () => { balances.selectTokenList(balances.tokenListOptions.allTokens) balances.openHideTokenMenu() balances.verifyEachRowHasCheckbox() }) - it('Verify that checking the checkboxes increases the token selected counter', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.checkTokenCounter(1) - }) - - it('Verify that selecting tokens and saving hides them from the table', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.saveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink]) - }) - - it('Verify that Cancel closes the menu and does not change the table status', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.saveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.cancelSaveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) - }) - - it('Verify that Deselect All unchecks all tokens from the list', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.deselecAlltHiddenTokenSelection() - balances.verifyEachRowHasCheckbox(constants.checkboxStates.unchecked) - }) - - it('Verify the Hidden tokens counter works for spam tokens', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.saveHiddenTokenSelection() - balances.checkHiddenTokenBtnCounter(1) - }) - - it('Verify the Hidden tokens counter works for native tokens', () => { - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(constants.tokenNames.sepoliaEther) - balances.saveHiddenTokenSelection() - balances.checkHiddenTokenBtnCounter(1) - }) - - it('Verify you can hide tokens from the eye icon in the table rows', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.hideAsset(balances.currencyLink) - }) - - it('Verify the sorting of "Assets" and "Balance" in the table', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyTableRows(7) - balances.clickOnTokenNameSortBtn() - balances.verifyTokenNamesOrder() - balances.clickOnTokenNameSortBtn() - balances.verifyTokenNamesOrder('descending') - balances.clickOnTokenBalanceSortBtn() - balances.verifyTokenBalanceOrder() - balances.clickOnTokenBalanceSortBtn() - balances.verifyTokenBalanceOrder('descending') - }) - - it('Verify that clicking the button with an owner opens the Send funds form', () => { + it('[SMOKE] Verify that clicking the button with an owner opens the Send funds form', () => { balances.selectTokenList(balances.tokenListOptions.allTokens) balances.clickOnSendBtn(0) }) - - it('Verify that the Send button shows when hovering a row', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.showSendBtn(0) - owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) - // Removed the part that checks for a non owner error message in the tooltip - // because the safe has no assets, and we don't have a safe with assets where e2e wallet is not an owner - }) }) diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index c3e93d08fc..df4caff475 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -7,7 +7,7 @@ const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' -describe('Batch transaction tests', () => { +describe('[SMOKE] Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) @@ -15,17 +15,12 @@ describe('Batch transaction tests', () => { main.acceptCookies() }) - it('Verify empty batch list can be opened', () => { + it('[SMOKE] Verify empty batch list can be opened', () => { batch.openBatchtransactionsModal() batch.openNewTransactionModal() }) - it('Verify the Add batch button is present in a transaction form', () => { - //The "true" is to validate that the add to batch button is not visible if "Yes, execute" is selected - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) - }) - - it('Verify a transaction can be added to the batch', () => { + it('[SMOKE] Verify a transaction can be added to the batch', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.contains(batch.transactionAddedToBatchStr).should('be.visible') //The batch button in the header shows the transaction count @@ -34,16 +29,7 @@ describe('Batch transaction tests', () => { batch.verifyAmountTransactionsInBatch(1) }) - it('Verify a second transaction can be added to the batch', () => { - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) - cy.wait(1000) - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) - batch.verifyBatchIconCount(2) - batch.clickOnBatchCounter() - batch.verifyAmountTransactionsInBatch(2) - }) - - it('Verify the batch can be confirmed and related transactions exist in the form', () => { + it('[SMOKE] Verify the batch can be confirmed and related transactions exist in the form', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.wait(1000) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) @@ -58,7 +44,7 @@ describe('Batch transaction tests', () => { cy.get('@TransactionList').find('li').eq(1).find('span').eq(0).contains(funds_first_tx) }) - it('Verify a transaction can be removed from the batch', () => { + it('[SMOKE] Verify a transaction can be removed from the batch', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.wait(1000) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 1b924cd743..3578417fce 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -3,13 +3,14 @@ import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' -describe('Safe creation tests', () => { +describe('[SMOKE] Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + main.waitForSafeListRequestToComplete() cy.clearLocalStorage() main.acceptCookies() }) - it('Verify a Wallet can be connected', () => { + it('[SMOKE] Verify a Wallet can be connected', () => { createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() @@ -17,62 +18,16 @@ describe('Safe creation tests', () => { createwallet.connectWallet() }) - it('Verify Next button is disabled until switching to network is done', () => { - owner.waitForConnectionStatus() - createwallet.selectNetwork(constants.networks.ethereum) - createwallet.clickOnCreateNewSafeBtn() - createwallet.checkNetworkChangeWarningMsg() - createwallet.verifyNextBtnIsDisabled() - createwallet.selectNetwork(constants.networks.sepolia) - createwallet.verifyNextBtnIsEnabled() - }) - - it('Verify that a new Wallet has default name related to the selected network', () => { + it('[SMOKE] Verify that a new Wallet has default name related to the selected network', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) }) - it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { + it('[SMOKE] Verify Add and Remove Owner Row works as expected', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() - createwallet.typeWalletName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - createwallet.clearWalletName() - }) - - it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - createwallet.typeWalletName(main.generateRandomString(50)) - owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify current connected account is shown as default owner', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) - }) - - it('Verify error message is displayed if owner name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.typeExistingOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.typeExistingOwnerName(main.generateRandomString(50)) - owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify Add and Remove Owner Row works as expected', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(2) owner.verifyExistingOwnerAddress(1, '') @@ -83,10 +38,10 @@ describe('Safe creation tests', () => { owner.verifyNumberOfOwners(2) }) - it('Verify Threshold Setup', () => { + it('[SMOKE] Verify Threshold Setup', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(3) @@ -100,62 +55,4 @@ describe('Safe creation tests', () => { owner.verifyThresholdLimit(1, 2) createwallet.updateThreshold(1) }) - - it('Verify data persistence', () => { - const ownerName = 'David' - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() - createwallet.typeOwnerName(ownerName, 1) - createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) - owner.clickOnBackBtn() - createwallet.clearWalletName() - createwallet.typeWalletName(createwallet.walletName) - owner.clickOnNextBtn() - owner.clickOnNextBtn() - createwallet.verifySafeNameInSummaryStep(createwallet.walletName) - createwallet.verifyOwnerNameInSummaryStep(ownerName) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyThresholdStringInSummaryStep(1, 2) - createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) - owner.clickOnBackBtn() - owner.clickOnBackBtn() - cy.wait(1000) - owner.clickOnNextBtn() - owner.clickOnNextBtn() - createwallet.verifySafeNameInSummaryStep(createwallet.walletName) - createwallet.verifyOwnerNameInSummaryStep(ownerName) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyThresholdStringInSummaryStep(1, 2) - createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) - createwallet.verifyEstimatedFeeInSummaryStep() - }) - - it('Verify tip is displayed on right side for threshold 1/1', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.verifyPolicy1_1() - }) - - it('Verify address input validation rules', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() - createwallet.typeOwnerAddress(main.generateRandomString(10), 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - - createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownerAdded) - - createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - createwallet.typeOwnerAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.failedResolve) - }) }) diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index 74718d6980..75014058c3 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -5,14 +5,14 @@ import * as createtx from '../../e2e/pages/create_tx.pages' const sendValue = 0.00002 const currentNonce = 11 -describe('Create transactions tests', () => { +describe('[SMOKE] Create transactions tests', () => { before(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) main.acceptCookies() }) - it('Verify a new send token transaction can be created', () => { + it('[SMOKE] Verify a new send token transaction can be created', () => { createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() createtx.typeRecipientAddress(constants.EOA) @@ -23,7 +23,7 @@ describe('Create transactions tests', () => { createtx.clickOnNextBtn() }) - it('Verify a transaction can be reviewed, edited and submitted', () => { + it('[SMOKE] Verify a transaction can be reviewed, edited and submitted', () => { createtx.verifySubmitBtnIsEnabled() cy.wait(1000) createtx.verifyNativeTokenTransfer() @@ -36,7 +36,7 @@ describe('Create transactions tests', () => { createtx.clickOnSignTransactionBtn() }) - it('Verify that clicking on notification shows the transaction in queue', () => { + it('[SMOKE] Verify that clicking on notification shows the transaction in queue', () => { createtx.waitForProposeRequest() createtx.clickViewTransaction() createtx.verifySingleTxPage() diff --git a/cypress/e2e/smoke/dashboard.cy.js b/cypress/e2e/smoke/dashboard.cy.js index d3712a5b07..03b239c733 100644 --- a/cypress/e2e/smoke/dashboard.cy.js +++ b/cypress/e2e/smoke/dashboard.cy.js @@ -2,7 +2,7 @@ import * as constants from '../../support/constants' import * as dashboard from '../pages/dashboard.pages' import * as main from '../pages/main.page' -describe('Dashboard tests', () => { +describe('[SMOKE] Dashboard tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) @@ -11,19 +11,19 @@ describe('Dashboard tests', () => { dashboard.verifyConnectTransactStrIsVisible() }) - it('Verify the overview widget is displayed', () => { + it('[SMOKE] Verify the overview widget is displayed', () => { dashboard.verifyOverviewWidgetData() }) - it('Verify the transaction queue widget is displayed', () => { + it('[SMOKE] Verify the transaction queue widget is displayed', () => { dashboard.verifyTxQueueWidget() }) - it('Verify the featured Safe Apps are displayed', () => { + it('[SMOKE] Verify the featured Safe Apps are displayed', () => { dashboard.verifyFeaturedAppsSection() }) - it('Verify the Safe Apps Section is displayed', () => { + it('[SMOKE] Verify the Safe Apps Section is displayed', () => { dashboard.verifySafeAppsSection() }) }) diff --git a/cypress/e2e/smoke/landing.cy.js b/cypress/e2e/smoke/landing.cy.js index 206732446f..f56883364a 100644 --- a/cypress/e2e/smoke/landing.cy.js +++ b/cypress/e2e/smoke/landing.cy.js @@ -1,6 +1,6 @@ import * as constants from '../../support/constants' -describe('Landing page tests', () => { - it('Verify a user will be redirected to welcome page', () => { +describe('[SMOKE] Landing page tests', () => { + it('[SMOKE] Verify a user will be redirected to welcome page', () => { cy.clearLocalStorage() cy.visit('/') 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 edce4b7f8b..1d9122eb47 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -18,7 +18,7 @@ const INVALID_ADDRESS_ERROR_MSG = 'Address given is not a valid Safe address' const OWNER_ENS_DEFAULT_NAME = 'test20.eth' const OWNER_ADDRESS = constants.EOA -describe('Load Safe tests', () => { +describe('[SMOKE] Load Safe tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) @@ -26,59 +26,7 @@ describe('Load Safe tests', () => { cy.wait(2000) }) - it('Verify a network can be selected in the Safe', () => { - safe.clickNetworkSelector(constants.networks.sepolia) - safe.selectPolygon() - cy.wait(2000) - safe.clickNetworkSelector(constants.networks.polygon) - safe.selectSepolia() - }) - - it('Verify only valid Safe name can be accepted', () => { - // alias the address input label - cy.get('input[name="address"]').parent().prev('label').as('addressLabel') - - createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) - safe.verifyIncorrectAddressErrorMessage() - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - - // Type an invalid address - // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) - // cy.get('@addressLabel').contains(INVALID_ADDRESS_ERROR_MSG) - - // Type a ENS name - // TODO: register a goerli ENS name for the test Safe - // cy.get('input[name="address"]').clear().type(SAFE_ENS_NAME) - // giving time to the ENS name to be translated - // cy.get('input[name="address"]', { timeout: 10000 }).should('have.value', `rin:${SAFE_ENS_NAME_TRANSLATED}`) - - // Uploading a QR code - // TODO: fix this - // cy.findByTestId('QrCodeIcon').click() - // cy.contains('Upload an image').click() - // cy.get('[type="file"]').attachFile('../fixtures/goerli_safe_QR.png') - - safe.verifyAddressInputValue() - safe.clickOnNextBtn() - }) - - it('Verify custom name in the first owner an be set', () => { - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - safe.clickOnNextBtn() - createwallet.typeOwnerName(testOwnerName, 0) - safe.clickOnNextBtn() - }) - - it('Verify Safe and owner names are displayed in the Review step', () => { - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - safe.clickOnNextBtn() - createwallet.typeOwnerName(testOwnerName, 0) - safe.clickOnNextBtn() - safe.verifyDataInReviewSection(testSafeName, testOwnerName) - safe.clickOnAddBtn() - }) - - it('Verify the custom Safe name is successfully loaded', () => { + it('[SMOKE] Verify the custom Safe name is successfully loaded', () => { safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_2) safe.clickOnNextBtn() createwallet.typeOwnerName(testOwnerName, 0) diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index 4a39e1a545..11cd2217d2 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -6,7 +6,7 @@ const nftsName = 'CatFactory' const nftsAddress = '0x373B...866c' const nftsTokenID = 'CF' -describe('NFTs tests', () => { +describe('[SMOKE] NFTs tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + constants.SEPOLIA_TEST_SAFE_5) @@ -14,34 +14,23 @@ describe('NFTs tests', () => { nfts.waitForNftItems(2) }) - it('Verify that NFTs exist in the table', () => { + it('[SMOKE] Verify that NFTs exist in the table', () => { nfts.verifyNFTNumber(10) }) - it('Verify NFT row contains data', () => { + it('[SMOKE] Verify NFT row contains data', () => { nfts.verifyDataInTable(nftsName, nftsAddress, nftsTokenID) }) - it('Verify NFT preview window can be opened', () => { + it('[SMOKE] Verify NFT preview window can be opened', () => { nfts.openActiveNFT(0) nfts.verifyNameInNFTModal(nftsTokenID) nfts.verifySelectedNetwrokSepolia() nfts.closeNFTModal() }) - it('Verify NFT open does not open if no NFT exits', () => { + it('[SMOKE] Verify NFT open does not open if no NFT exits', () => { nfts.clickOnInactiveNFT() nfts.verifyNFTModalDoesNotExist() }) - - it('Verify multipls NFTs can be selected and reviewed', () => { - nfts.verifyInitialNFTData() - nfts.selectNFTs(3) - nfts.deselectNFTs([2], 3) - nfts.sendNFT() - nfts.verifyNFTModalData() - nfts.typeRecipientAddress(constants.SEPOLIA_TEST_SAFE_4) - nfts.clikOnNextBtn() - nfts.verifyReviewModalData(2) - }) }) diff --git a/cypress/e2e/smoke/remove_owner.cy.js b/cypress/e2e/smoke/remove_owner.cy.js index 4643b91e1a..d6127941ce 100644 --- a/cypress/e2e/smoke/remove_owner.cy.js +++ b/cypress/e2e/smoke/remove_owner.cy.js @@ -1,8 +1,9 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' +import * as createwallet from '../pages/create_wallet.pages' -describe('Remove Owners tests', () => { +describe('[SMOKE] Remove Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() @@ -10,44 +11,12 @@ describe('Remove Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify that "Remove" icon is visible', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.verifyRemoveBtnIsEnabled().should('have.length', 2) - }) - - it('Verify Tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_4) - owner.waitForConnectionStatus() - owner.verifyRemoveBtnIsDisabled() - }) - - it('Verify Tooltip displays correct message for disconnected user', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyRemoveBtnIsDisabled() - }) - - it('Verify owner removal form can be opened', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.openRemoveOwnerWindow(1) - }) - - it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.openRemoveOwnerWindow(1) - owner.verifyThresholdLimit(1, 1) - owner.getThresholdOptions().should('have.length', 1) - }) - - it('Verify owner deletion confirmation is displayed', () => { + // TODO: Add Sign action. Check there is no error before sign action on UI when nonce not loaded + it('[SMOKE] Verify owner deletion confirmation is displayed', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) owner.waitForConnectionStatus() owner.openRemoveOwnerWindow(1) - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() owner.verifyOwnerDeletionWindowDisplayed() }) }) diff --git a/cypress/e2e/smoke/replace_owner.cy.js b/cypress/e2e/smoke/replace_owner.cy.js index ed48056206..6bccf10e28 100644 --- a/cypress/e2e/smoke/replace_owner.cy.js +++ b/cypress/e2e/smoke/replace_owner.cy.js @@ -1,9 +1,8 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' -import * as addressBook from '../pages/address_book.page' -describe('Replace Owners tests', () => { +describe('[SMOKE] Replace Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() @@ -11,74 +10,19 @@ describe('Replace Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify that "Replace" icon is visible', () => { + it('[SMOKE] Verify that "Replace" icon is visible', () => { owner.verifyReplaceBtnIsEnabled() }) - it('Verify Tooltip displays correct message for Non-Owner', () => { + // TODO: Remove "tooltip" from title + it('[SMOKE] Verify Tooltip displays correct message for Non-Owner', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_2) owner.waitForConnectionStatus() owner.verifyReplaceBtnIsDisabled() }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyReplaceBtnIsDisabled() - }) - - it('Verify that the owner replacement form is opened', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - }) - - it('Verify max characters in name field', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify that Address input auto-fills with related value', () => { - cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - addressBook.clickOnCreateEntryBtn() - addressBook.typeInName(constants.addresBookContacts.user1.name) - addressBook.typeInAddress(constants.addresBookContacts.user1.address) - addressBook.clickOnSaveEntryBtn() - addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + it('[SMOKE] Verify that the owner replacement form is opened', () => { owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(constants.addresBookContacts.user1.address) - owner.selectNewOwner(constants.addresBookContacts.user1.name) - owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) - }) - - it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - owner.verifyConfirmTransactionWindowDisplayed() - }) - - it('Verify relevant error messages are displayed in Address input', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(main.generateRandomString(10)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.toUpperCase()) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafe) - - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.replace('F', 'f')) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) }) diff --git a/cypress/e2e/smoke/tx_history.cy.js b/cypress/e2e/smoke/tx_history.cy.js index a9b8309c3f..af41ef7d45 100644 --- a/cypress/e2e/smoke/tx_history.cy.js +++ b/cypress/e2e/smoke/tx_history.cy.js @@ -8,7 +8,7 @@ const str1 = 'Received' const str2 = 'Executed' const str3 = 'Transaction hash' -describe('Transaction history tests', () => { +describe('[SMOKE] Transaction history tests', () => { beforeEach(() => { cy.clearLocalStorage() // Go to the test Safe transaction history @@ -20,7 +20,7 @@ describe('Transaction history tests', () => { main.acceptCookies() }) - it('Verify October 29th transactions are displayed', () => { + it('[SMOKE] Verify October 29th transactions are displayed', () => { const DATE = 'Oct 29, 2023' const NEXT_DATE_LABEL = 'Oct 20, 2023' const amount = '0.00001 ETH' @@ -44,13 +44,4 @@ describe('Transaction history tests', () => { createTx.verifyTransactionStrExists(success) }) }) - - it('Verify transaction can be expanded/collapsed', () => { - createTx.clickOnTransactionExpandableItem('Oct 20, 2023', () => { - createTx.verifyTransactionStrExists(str1) - createTx.verifyTransactionStrExists(str2) - createTx.verifyTransactionStrExists(str3) - createTx.clickOnExpandIcon() - }) - }) }) diff --git a/cypress/e2e/spending_limit.cy.js b/cypress/e2e/spending_limit.cy.js deleted file mode 100644 index 403d1b93ce..0000000000 --- a/cypress/e2e/spending_limit.cy.js +++ /dev/null @@ -1,54 +0,0 @@ -const HW_WALLET = '0xff6E053fBf4E5895eDec09146Bc10f705E8c4b3D' -const SPENDING_LIMIT_SAFE = 'gor:0x28F95E682D1dd632b54Dc61740575f49DB39Eb7F' - -describe('Check spending limit modal', () => { - before(() => { - cy.visit(`/${SPENDING_LIMIT_SAFE}/home`, { failOnStatusCode: false }) - - cy.contains('Accept selection').click() - }) - - it('should open the spending limit modal', () => { - // Assert that "New transaction" button is visible - cy.contains('New transaction', { - timeout: 60_000, // `lastWallet` takes a while initialize in CI - }).should('be.visible') - - // Open the new transaction modal - cy.contains('New transaction').click() - }) - - it('should draft a spending limit transaction', () => { - // Modal is open - cy.contains('h2', 'New transaction').should('be.visible') - - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(SPENDING_LIMIT_SAFE) - - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]').contains('Görli Ether').click() - - // Insert max amount - cy.contains('Spending Limit Transaction (0.1 GOR)').click() - - // Insert max amount - cy.contains('Max').click() - - cy.contains('Next').click() - }) - - it('should review the spending limit transaction', () => { - cy.contains( - 'Spending limit transactions only appear in the interface once they are successfully processed and indexed. Pending transactions can only be viewed in your signer wallet application or under your wallet address on a Blockchain Explorer.', - ) - - // Alias for New transaction modal - cy.contains('h2', 'Review transaction').parents('div').as('modal') - - // Estimation is loaded - cy.get('button[type="submit"]').should('not.be.disabled') - }) -}) diff --git a/cypress/e2e/tx_modal.cy.js b/cypress/e2e/tx_modal.cy.js deleted file mode 100644 index f416144b47..0000000000 --- a/cypress/e2e/tx_modal.cy.js +++ /dev/null @@ -1,164 +0,0 @@ -import * as constants from '../support/constants' - -const TEST_SAFE = 'rin:0x11Df0fa87b30080d59eba632570f620e37f2a8f7' -const RECIPIENT_ENS = 'diogo.eth' -const SAFE_NONCE = '6' - -describe('Tx Modal', () => { - before(() => { - // Open the Safe used for testing - cy.visit(`/${TEST_SAFE}`) - cy.contains('a', 'Accept selection').click() - }) - - describe('Send funds modal', () => { - describe('Send Funds form', () => { - before(() => { - // Open Send Funds Modal - cy.contains('New Transaction').click() - cy.contains('Send funds').click() - }) - - it('should display Send Funds modal with all the form elements', () => { - // Modal header - cy.contains('Send funds') - .should('be.visible') - .next() - .contains('Step 1 of 2') - .should('be.visible') - .next() - // Current network is same as Safe - .contains('Rinkeby') - .should('be.visible') - - // It contains the form elements - cy.get('form').within(() => { - // Sending from the current Safe address - const [chainPrefix, safeAddress] = TEST_SAFE.split(':') - cy.contains(chainPrefix) - cy.contains(safeAddress) - - // Recipient field - cy.get('#address-book-input').should('be.visible') - - // Token selector - cy.contains('Select an asset*').should('be.visible') - - // Amount field - cy.contains('Amount').should('be.visible') - }) - - // Review button is disabled - cy.get('button[type="submit"]').should('be.disabled') - }) - - it('should resolve the ENS name', () => { - // Fills recipient with ens - cy.get('label[for="address-book-input"]').next().type(RECIPIENT_ENS) - - // Waits for resolving the ENS - cy.contains(constants.RECIPIENT_ADDRESS).should('be.visible') - }) - - it('should have all tokens available in the token selector', () => { - // Click on the Token selector - cy.contains('Select an asset*').click() - - const ownedTokens = ['Dai', 'Wrapped Ether', 'Ether', 'Uniswap', 'Gnosis', '0x', 'USD Coin'] - ownedTokens.forEach((token) => { - cy.get('ul[role="listbox"]').contains(token) - }) - }) - - it('should validate token amount', () => { - // Select a token - cy.get('ul[role="listbox"]').contains('Gnosis').click() - - // Insert an incorrect amount - cy.get('input[placeholder="Amount*"]').type('0.4') - - // Selecting more than the balance is not allowed - cy.contains('Maximum value is 0.000004') - - // Form field contains an error class - cy.get('input[placeholder="Amount*"]') - // Parent div is MuiInputBase-root - .parent('div') - .should(($div) => { - // Turn the classList into an array - const classList = Array.from($div[0].classList) - expect(classList).to.include('MuiInputBase-root').and.to.include('Mui-error') - }) - - // Insert a correct amount - cy.get('input[placeholder="Amount*"]').clear().type('0.000002') - - // Form field does not contain an error class - cy.get('input[placeholder="Amount*"]') - // Parent div is MuiInputBase-root - .parent('div') - .should(($div) => { - // Turn the classList into an array - const classList = Array.from($div[0].classList) - // Check if it contains the error class - expect(classList).to.include('MuiInputBase-root').and.not.to.include('Mui-error') - }) - - // Click Send max fills the input with token total amount - cy.contains('Send max').click() - cy.get('input[placeholder="Amount*"]').should('have.value', '0.000004') - }) - - it('should advance to the Review step', () => { - // Clicks Review - cy.contains('Review').click() - - // Modal step 2 - cy.contains('Step 2 of 2').should('be.visible') - }) - }) - - describe('Review modal', () => { - before(() => { - // Wait max 10s for estimate to finish - cy.contains('Submit', { timeout: 10000 }) - }) - - it('should have the same parameters as the previous step', () => { - // Sender - cy.contains('Sending from').parent().next().contains(TEST_SAFE) - // Recipient - cy.contains('Recipient').parent().next().contains(constants.RECIPIENT_ADDRESS) - - // Token value - cy.contains('0.000004 GNO') - }) - - it('should contain a correctly estimated gas limit value', () => { - const GAS_LIMIT = '79804' // gas limit is deterministic - - // Estimated gas price is loaded - cy.contains('Estimated fee price').next().should('not.have.text', '> 0.001 ETH') - - // Click Advanced parameters - cy.contains('Estimated fee price').click() - - // Find Gas limit - cy.contains('Gas limit').next().contains(GAS_LIMIT).should('be.visible') - - // Close info again - cy.contains('Estimated fee price').click() - }) - - it('should contain the Safe nonce upon clicking Advanced parameters', () => { - // Click Advanced parameters - cy.contains('Advanced parameters').click() - // Find Safe nonce - cy.contains('Safe nonce').next().contains(SAFE_NONCE).should('be.visible') - - // Close dialog again - cy.contains('Advanced parameters').click() - }) - }) - }) -}) diff --git a/cypress/e2e/tx_simulation.cy.js b/cypress/e2e/tx_simulation.cy.js deleted file mode 100644 index 4f219d41c3..0000000000 --- a/cypress/e2e/tx_simulation.cy.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as constants from '../support/constants' - -const TEST_SAFE = 'rin:0x11Df0fa87b30080d59eba632570f620e37f2a8f7' - -describe('Tx Simulation', () => { - before(() => { - // Open the Safe used for testing - cy.visit(`/${TEST_SAFE}/home`, { failOnStatusCode: false }) - cy.contains('button', 'Accept selection').click() - - // Open Send Funds Modal - cy.contains('New transaction').click() - cy.contains('Send tokens').click() - - // Choose recipient - cy.get('input[name="recipient"]').should('be.visible') - cy.get('input[name="recipient"]').type(constants.RECIPIENT_ADDRESS, { force: true }) - - // Select asset and amount - cy.get('input[name="tokenAddress"]').parent().click() - cy.get('ul[role="listbox"]').contains('Gnosis').click() - cy.contains('Max').click() - - // go to review step - cy.contains('Next').click() - }) - it('should initially have a successful simulation', () => { - // Simulate - cy.contains('Simulate').click() - - // result exists after max 10 seconds - cy.contains('The transaction was successfully simulated', { timeout: 10000 }) - }) - - it('should show unexpected error for a very low gas limit', () => { - // Set Gas Limit to too low - cy.contains('Estimated fee').click() - cy.contains('Edit').click() - cy.get('input[name="gasLimit"]').clear().type('21000') - cy.contains('Confirm').click() - - // Simulate - cy.contains('Simulate').click() - - // error exists after max 10 seconds - cy.contains('An unexpected error occurred during simulation:', { timeout: 10000 }) - }) - - it('should simulate with failed transaction for a slightly too low gas limit', () => { - // Set Gas Limit to too low - cy.contains('Estimated fee').click() - cy.contains('Edit').click() - cy.get('input[name="gasLimit"]').clear().type('75000') - cy.contains('Confirm').click() - - // Simulate - cy.contains('Simulate').click() - - // failed tx exists after max 10 seconds - cy.contains('out of gas', { timeout: 10000 }) - }) -}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index e9dce33964..a518c1c918 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -61,6 +61,8 @@ export const validAppUrl = 'https://my-valid-custom-app.com' export const proposeEndpoint = '/**/propose' export const appsEndpoint = '/**/safe-apps' +export const transactionHistoryEndpoint = '**/history' +export const safeListEndpoint = '**/safes' export const mainSideMenuOptions = { home: 'Home', diff --git a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx index 81ee517a65..f791ff5ffd 100644 --- a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx +++ b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx @@ -170,10 +170,16 @@ const OwnerPolicyStep = ({ - - diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index febd0bbdbd..e9b8afc372 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -250,7 +250,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - diff --git a/src/components/settings/owner/OwnerList/index.tsx b/src/components/settings/owner/OwnerList/index.tsx index 7c484cc7be..49b6149404 100644 --- a/src/components/settings/owner/OwnerList/index.tsx +++ b/src/components/settings/owner/OwnerList/index.tsx @@ -115,6 +115,7 @@ export const OwnerList = () => { {(isOk) => ( diff --git a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx index 942fbdc0ae..71b125efef 100644 --- a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx @@ -82,7 +82,7 @@ export const SetThreshold = ({ - diff --git a/src/pages/settings/setup.tsx b/src/pages/settings/setup.tsx index a551c4a177..3229762803 100644 --- a/src/pages/settings/setup.tsx +++ b/src/pages/settings/setup.tsx @@ -24,7 +24,7 @@ const Setup: NextPage = () => {
- + From 4a78e00405483b0141ca522f31f700b9a916820e Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:43:32 +0300 Subject: [PATCH 15/15] Fix: separate WalletConnect storage (#2793) --- src/services/walletconnect/WalletConnectWallet.ts | 3 ++- src/services/walletconnect/constants.ts | 4 ++-- yarn.lock | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/walletconnect/WalletConnectWallet.ts b/src/services/walletconnect/WalletConnectWallet.ts index 88b28f45fc..b2f8b3079b 100644 --- a/src/services/walletconnect/WalletConnectWallet.ts +++ b/src/services/walletconnect/WalletConnectWallet.ts @@ -7,7 +7,7 @@ import type { SessionTypes } from '@walletconnect/types' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import uniq from 'lodash/uniq' -import { IS_PRODUCTION, WC_PROJECT_ID } from '@/config/constants' +import { IS_PRODUCTION, LS_NAMESPACE, WC_PROJECT_ID } from '@/config/constants' import { EIP155, SAFE_COMPATIBLE_METHODS, SAFE_WALLET_METADATA } from './constants' import { invariant } from '@/utils/helpers' import { getEip155ChainId, stripEip155Prefix } from './utils' @@ -36,6 +36,7 @@ class WalletConnectWallet { const core = new Core({ projectId: WC_PROJECT_ID, logger: IS_PRODUCTION ? undefined : 'debug', + customStoragePrefix: LS_NAMESPACE, }) const web3wallet = await Web3Wallet.init({ diff --git a/src/services/walletconnect/constants.ts b/src/services/walletconnect/constants.ts index 2206a0069c..50656e9072 100644 --- a/src/services/walletconnect/constants.ts +++ b/src/services/walletconnect/constants.ts @@ -26,9 +26,9 @@ export const SAFE_COMPATIBLE_METHODS = [ export const SAFE_WALLET_METADATA = { name: 'Safe{Wallet}', - description: 'The most trusted platform to manage digital assets on Ethereum', url: 'https://app.safe.global', - icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], + description: 'Smart contract wallet for Ethereum', + icons: ['https://app.safe.global/images/logo-round.svg'], } export const EIP155 = 'eip155' as const diff --git a/yarn.lock b/yarn.lock index 63af5d5650..86e2841099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16728,4 +16728,4 @@ yocto-queue@^0.1.0: zksync-web3@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/zksync-web3/-/zksync-web3-0.14.3.tgz#64ac2a16d597464c3fc4ae07447a8007631c57c9" - integrity sha512-hT72th4AnqyLW1d5Jlv8N2B/qhEnl2NePK2A3org7tAa24niem/UAaHMkEvmWI3SF9waYUPtqAtjpf+yvQ9zvQ== + integrity sha512-hT72th4AnqyLW1d5Jlv8N2B/qhEnl2NePK2A3org7tAa24niem/UAaHMkEvmWI3SF9waYUPtqAtjpf+yvQ9zvQ== \ No newline at end of file