diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 553c77861c..49dd8f99eb 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -31,7 +31,7 @@ const spamTokenWarningIcon = '[data-testid="warning"]' const untrustedTokenWarningModal = '[data-testid="untrusted-token-warning"]' const sendTokensBtn = '[data-testid="send-tokens-btn"]' export const replacementNewSigner = '[data-testid="new-owner"]' -const messageItem = '[data-testid="message-item"]' +export const messageItem = '[data-testid="message-item"]' const viewTransactionBtn = 'View transaction' const transactionDetailsTitle = 'Transaction details' diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 9494cb9a8c..b8547b1318 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -300,3 +300,7 @@ export function verifyTextVisibility(stringsArray) { cy.contains(string).should('be.visible') }) } + +export function getIframeBody(iframe) { + return cy.get(iframe).its('0.contentDocument.body').should('not.be.empty').then(cy.wrap) +} diff --git a/cypress/e2e/pages/messages.pages.js b/cypress/e2e/pages/messages.pages.js new file mode 100644 index 0000000000..bc6052b32d --- /dev/null +++ b/cypress/e2e/pages/messages.pages.js @@ -0,0 +1,14 @@ +import { messageItem } from './create_tx.pages' +const onchainMsgInput = 'input[placeholder*="Message"]' + +export function enterOnchainMessage(msg) { + cy.get(onchainMsgInput).type(msg) +} + +export function clickOnMessageSignBtn(index) { + cy.get(messageItem) + .eq(index) + .within(() => { + cy.get('button').contains('Sign').click() + }) +} diff --git a/cypress/e2e/pages/modals.page.js b/cypress/e2e/pages/modals.page.js index 1e8063405a..d2b99a68e2 100644 --- a/cypress/e2e/pages/modals.page.js +++ b/cypress/e2e/pages/modals.page.js @@ -5,6 +5,8 @@ export const modalTitiles = { editEntry: 'Edit entry', deleteEntry: 'Delete entry', dataImport: 'Data import', + confirmTx: 'Confirm transaction', + confirmMsg: 'Confirm message', } export function verifyModalTitle(title) { diff --git a/cypress/e2e/pages/modals/message_confirmation.pages.js b/cypress/e2e/pages/modals/message_confirmation.pages.js new file mode 100644 index 0000000000..67d78c4c56 --- /dev/null +++ b/cypress/e2e/pages/modals/message_confirmation.pages.js @@ -0,0 +1,46 @@ +import * as modal from '../modals.page' +import * as checkers from '../../../support/utils/checkers' +import * as main from '../main.page' + +const messageHash = '[data-testid="message-hash"]' +const messageDetails = '[data-testid="message-details"]' +const messageInfobox = '[data-testid="message-infobox"]' + +const messageInfoBoxData = [ + 'Collect all the confirmations', + 'Confirmations (1 of 2)', + 'The signature will be submitted to the Safe App when the message is fully signed', +] + +export function verifyConfirmationWindowTitle(title) { + cy.get(modal.modalTitle).should('contain', title) +} + +export function verifyMessagePresent(msg) { + cy.get('textarea').should('contain', msg) +} + +export function verifySafeAppInPopupWindow(safeApp) { + cy.contains(safeApp) +} + +export function verifyOffchainMessageHash(index) { + cy.get(messageHash) + .eq(index) + .invoke('text') + .then((text) => { + if (!checkers.startsWith0x(text)) { + throw new Error(`Message at index ${index} does not start with '0x': ${text}`) + } + }) +} + +export function checkMessageInfobox() { + cy.get(messageInfobox).within(() => { + main.verifyTextVisibility(messageInfoBoxData) + }) +} + +export function clickOnMessageDetails() { + cy.get(messageDetails).click() +} diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index b7d1238f5d..9c9c3e84d4 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -89,6 +89,8 @@ export const transferStr = 'Transfer' export const successStr = 'Success' export const failedStr = 'Failed' +export const dummyTxStr = 'Trigger dummy tx (safe.txs.send)' +export const signOnchainMsgStr = 'Sign message (on-chain)' export const pinWalletConnectStr = /pin walletconnect/i export const transactionBuilderStr = 'Transaction Builder' export const testAddressValueStr = 'testAddressValue' @@ -126,6 +128,14 @@ export const permissionCheckboxNames = { fullscreen: 'Fullscreen', } +export function triggetOffChainTx() { + cy.contains(dummyTxStr).click() +} + +export function triggetOnChainTx() { + cy.contains(signOnchainMsgStr).click() +} + export function verifyWarningDefaultAppMsgIsDisplayed() { cy.get('p').contains(warningDefaultAppStr).should('be.visible') cy.wait(1000) @@ -145,6 +155,7 @@ export function verifyLinkName(name) { export function clickOnApp(app) { cy.contains(app).click() + cy.wait(2000) } export function verifyNoAppsTextPresent() { diff --git a/cypress/e2e/regression/messages_offchain.cy.js b/cypress/e2e/regression/messages_offchain.cy.js index 62077ced58..28bfacda76 100644 --- a/cypress/e2e/regression/messages_offchain.cy.js +++ b/cypress/e2e/regression/messages_offchain.cy.js @@ -3,8 +3,12 @@ import * as main from '../pages/main.page.js' import * as createTx from '../pages/create_tx.pages.js' import * as msg_data from '../../fixtures/txmessages_data.json' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as modal from '../pages/modals.page' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' let staticSafes = [] +const offchainMessage = 'Test message 2 off-chain' const typeMessagesGeneral = msg_data.type.general const typeMessagesOffchain = msg_data.type.offChain @@ -46,7 +50,7 @@ describe('Offchain Messages tests', () => { ) }) - it('Verify exapanded details for simple off-chain message', () => { + it('Verify exapanded details for EIP 191 off-chain message', () => { createTx.clickOnTransactionItemByIndex(2) cy.contains(typeMessagesOffchain.message2).should('be.visible') }) @@ -71,4 +75,14 @@ describe('Offchain Messages tests', () => { main.verifyTextVisibility(values) }) + + it('Verify confirmation window is displayed for unsigned message', () => { + messages.clickOnMessageSignBtn(2) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifyMessagePresent(offchainMessage) + msg_confirmation_modal.clickOnMessageDetails() + msg_confirmation_modal.verifyOffchainMessageHash(0) + msg_confirmation_modal.verifyOffchainMessageHash(1) + msg_confirmation_modal.checkMessageInfobox() + }) }) diff --git a/cypress/e2e/regression/messages_popup.cy.js b/cypress/e2e/regression/messages_popup.cy.js new file mode 100644 index 0000000000..399d41dc29 --- /dev/null +++ b/cypress/e2e/regression/messages_popup.cy.js @@ -0,0 +1,72 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as modal from '../pages/modals.page.js' +import * as apps from '../pages/safeapps.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' + +let staticSafes = [] +const safeApp = 'Safe Test App' +const onchainMessage = 'Message 1' +let iframeSelector + +describe('Messages popup window tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.appsCustomUrl + staticSafes.SEP_STATIC_SAFE_10) + main.acceptCookies() + iframeSelector = `iframe[id="iframe-${constants.safeTestAppurl}"]` + }) + + it('Verify off-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + main.getIframeBody(iframeSelector).within(() => { + apps.triggetOffChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmTx) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + }) + + it('Verify on-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + main.getIframeBody(iframeSelector).within(() => { + messages.enterOnchainMessage(onchainMessage) + apps.triggetOnChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + msg_confirmation_modal.verifyMessagePresent(onchainMessage) + }) +}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index dc60748283..1b9fec42e6 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -38,11 +38,13 @@ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ +export const safeTestAppurl = 'https://safe-apps-test-app.pages.dev' export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' export const drainAccount_url = 'https://safe-apps.dev.5afe.dev/drain-safe' export const testAppUrl = 'https://safe-test-app.com' export const addressBookUrl = '/address-book?safe=' export const appsUrlGeneral = '/apps?=safe=' +export const appsCustomUrl = 'apps/custom?safe=' export const BALANCE_URL = '/balances?safe=' export const balanceNftsUrl = '/balances/nfts?safe=' export const transactionQueueUrl = '/transactions/queue?safe=' @@ -230,6 +232,9 @@ export const localStorageKeys = { SAFE_v2__safeApps: 'SAFE_v2__safeApps', SAFE_v2__cookies: 'SAFE_v2__cookies', SAFE_v2__tokenlist_onboarding: 'SAFE_v2__tokenlist_onboarding', + SAFE_v2__customSafeApps_11155111: 'SAFE_v2__customSafeApps-11155111', + SAFE_v2__SafeApps__browserPermissions: 'SAFE_v2__SafeApps__browserPermissions', + SAFE_v2__SafeApps__infoModal: 'SAFE_v2__SafeApps__infoModal', } export const connectWalletNames = { diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 6eb629a971..7972fa2db0 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -1,3 +1,4 @@ +/* eslint-disable */ export const batchData = { entry0: { 11155111: { @@ -631,6 +632,26 @@ export const pinnedApps = { transactionBuilder: { 11155111: { pinned: [24], opened: [] } }, } +export const customApps = (url) => ({ + safeTestApp: [{ url: url }], + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, +}) + +export const appPermissions = (url) => ({ + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, + infoModalAccepted: { 11155111: { consentsAccepted: true, warningCheckedCustomApps: [] } }, +}) + export const cookies = { acceptedCookies: { necessary: true, updates: true, analytics: true }, acceptedTokenListOnboarding: true, diff --git a/cypress/support/utils/checkers.js b/cypress/support/utils/checkers.js new file mode 100644 index 0000000000..112e11b096 --- /dev/null +++ b/cypress/support/utils/checkers.js @@ -0,0 +1,4 @@ +export function startsWith0x(str) { + const pattern = /^0x/ + return pattern.test(str) +} diff --git a/src/components/safe-messages/InfoBox/index.tsx b/src/components/safe-messages/InfoBox/index.tsx index 21a22a5c6c..cc7b3d5fa0 100644 --- a/src/components/safe-messages/InfoBox/index.tsx +++ b/src/components/safe-messages/InfoBox/index.tsx @@ -16,7 +16,7 @@ const InfoBox = ({ className?: string }): ReactElement => { return ( -