From 495d3cc054687b4e5c9f4c35d98ef215ffc5c1c3 Mon Sep 17 00:00:00 2001 From: Tim <4171783+tmjssz@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:35:30 +0200 Subject: [PATCH] tests(mobile): e2e coverage for WalletConnectGate flows (#7760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests(mobile): add e2e coverage for WalletConnectGate flows Adds Maestro tests for the two non-signing gate states on the review-and-execute screen: - wc-gate-reconnect.yml: expired session renders "Reconnect wallet to continue"; tapping restores the session and reveals Execute transaction. - wc-gate-switch-network.yml: wrong-network state renders "Switch network to continue"; tapping clears the flag and reveals Execute transaction. Wires up the e2e control surface: - WalletConnectContext.e2e.tsx: mock reconnect() and switchNetworkIfNeeded() now flip walletConnectE2eState so the gate transitions correctly. - TestCtrls: e2eWcGateReconnect and e2eWcGateWrongNetwork buttons. - connectSignerSetup: setupWcGateReconnect / setupWcGateWrongNetwork seed a pending-tx safe with a WC signer in the desired gate state. Pin isows@1.0.7 in package.json resolutions. Part of WA-1858. * tests(mobile): extract shared review-and-execute nav for wc-gate tests Both wc-gate YAML tests duplicated ~30 lines navigating from app start to the Review-and-Execute screen of the first pending tx. Move that navigation into apps/mobile/e2e/utils/setup/open-review-and-execute.yml, parameterised by SETUP_BUTTON_ID, and have both gate tests `runFlow` it. The seeding button id is now the only piece of per-test configuration. * tests(mobile): assert wc-gate dismissal by id, not by neighbouring text The gate-dismissed assertion used a regex matching either "Execute transaction" or "Insufficient funds". Those are distinct semantic states (funded vs unfunded signer); coupling them in one assertion lets a funds-error regression silently pass the gate test. Replace the text regex with `extendedWaitUntil: notVisible` on the gate button id. That waits for re-render and asserts the gate is gone in one step, decoupling the test from the underlying execute-button copy. * tests(mobile): drop unread fields from WC e2e state overrides WalletConnectGate and useWalletConnectStatus only read `address`, `isWrongNetwork`, and `isWalletConnectSigner`. The setup helpers and the `reconnect` mock were also setting `hasProvider` and `walletInfo` — unobservable from any production code path, so they only added noise. - setupWcGateReconnect: drop the override entirely; post-reset defaults already model an expired session. - setupWcGateWrongNetwork: drop hasProvider and walletInfo. - WalletConnectContext.e2e.tsx reconnect mock: drop hasProvider. * tests(mobile): add `wc-gate` tag to connect-signer suite The two child flows (wc-gate-reconnect.yml, wc-gate-switch-network.yml) declare a `wc-gate` tag, but the parent __suite__.yml didn't surface it. Tag-based runs (`maestro test --include-tags wc-gate`) now also pick up the aggregated suite, matching repo convention. * tests(mobile): annotate wcSigner literal with explicit Signer type The literal relied on inference plus `as const` on `type`. With an explicit Signer annotation, future widening or narrowing of the discriminated union in signersSlice will surface here at compile time instead of failing silently in the e2e flow. The `as const` workaround is no longer needed. * tests(mobile): reuse setupPendingTxSafe from WC gate setup setupWcGateBase duplicated the pending-tx safe scaffolding already in setupHelpers.ts (base config, signer registration, active safe/signer). The only delta was the signer's `type: 'walletconnect'` and the extra setExecutionMethod dispatch. Extend setupPendingTxSafe with an optional `options.signer` override. Existing five private-key callers are unchanged. setupWcGateBase now constructs the WC Signer literal and delegates everything else to the shared helper. * docs(mobile): document `e2e` exception for TestCtrls triggers The repo-wide testID convention is kebab-case, but every TestCtrls scenario-trigger button uses `e2eConnectSignerOwner`-style camelCase. That divergence was undocumented, so new contributors would either churn the convention or get pushed to match it without context. Codify the exception: product testIDs stay kebab-case; the hidden e2e-build-only TestCtrls triggers use `e2e`. * docs: document `isows@1.0.7` resolution and add resolutions log The root package.json `resolutions` block had 12 entries with zero recorded rationale, so each new reviewer has to investigate why a pin exists. Add `docs/resolutions.md` as the canonical home for that rationale and seed it with the `isows@1.0.7` entry — a benign dedup (no CVE) added to align the transitive version under viem. Future contributors are asked to update the log alongside any new resolutions entry. --- apps/mobile/docs/e2e-tests-guidelines.md | 6 ++ .../e2e/tests/connect-signer/__suite__.yml | 20 ++++++ .../connect-signer/wc-gate-reconnect.yml | 34 ++++++++++ .../connect-signer/wc-gate-switch-network.yml | 34 ++++++++++ .../utils/setup/open-review-and-execute.yml | 49 ++++++++++++++ .../context/WalletConnectContext.e2e.tsx | 11 +++- .../e2e-maestro/components/TestCtrls.e2e.tsx | 22 ++++++- .../e2e-maestro/setup/connectSignerSetup.ts | 66 ++++++++++++++++++- .../tests/e2e-maestro/setup/setupHelpers.ts | 17 ++++- docs/resolutions.md | 15 +++++ package.json | 1 + yarn.lock | 8 +-- 12 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 apps/mobile/e2e/tests/connect-signer/__suite__.yml create mode 100644 apps/mobile/e2e/tests/connect-signer/wc-gate-reconnect.yml create mode 100644 apps/mobile/e2e/tests/connect-signer/wc-gate-switch-network.yml create mode 100644 apps/mobile/e2e/utils/setup/open-review-and-execute.yml create mode 100644 docs/resolutions.md diff --git a/apps/mobile/docs/e2e-tests-guidelines.md b/apps/mobile/docs/e2e-tests-guidelines.md index 6a15bfb20..0636d7be9 100644 --- a/apps/mobile/docs/e2e-tests-guidelines.md +++ b/apps/mobile/docs/e2e-tests-guidelines.md @@ -14,6 +14,12 @@ This document outlines best practices for writing and maintaining end-to-end tes - **Screen:** `settings-screen` - **Uniqueness:** It's virtually impossible to have unique testIDs across the app. We should make sure that every screen has unique testIDs. +- **Exception — TestCtrls triggers:** The hidden buttons in + [`TestCtrls.e2e.tsx`](../src/tests/e2e-maestro/components/TestCtrls.e2e.tsx) + that seed Redux state for a scenario use the `e2e` form + (e.g. `e2eConnectSignerOwner`, `e2eWcGateReconnect`). These IDs only + exist in e2e builds and are scoped to test setup, so they are kept + visually distinct from product testIDs. --- diff --git a/apps/mobile/e2e/tests/connect-signer/__suite__.yml b/apps/mobile/e2e/tests/connect-signer/__suite__.yml new file mode 100644 index 000000000..39169900b --- /dev/null +++ b/apps/mobile/e2e/tests/connect-signer/__suite__.yml @@ -0,0 +1,20 @@ +appId: ${APP_ID} +tags: + - connect-signer + - wc-gate + - suite +--- +# Connect Signer Test Suite +# Runs all connect signer and WalletConnect gate tests + +- runFlow: + file: ./connect-signer-success.yml + +- runFlow: + file: ./connect-signer-not-owner.yml + +- runFlow: + file: ./wc-gate-reconnect.yml + +- runFlow: + file: ./wc-gate-switch-network.yml diff --git a/apps/mobile/e2e/tests/connect-signer/wc-gate-reconnect.yml b/apps/mobile/e2e/tests/connect-signer/wc-gate-reconnect.yml new file mode 100644 index 000000000..8a7b118fc --- /dev/null +++ b/apps/mobile/e2e/tests/connect-signer/wc-gate-reconnect.yml @@ -0,0 +1,34 @@ +appId: ${APP_ID} +tags: + - connect-signer + - wc-gate +--- +# WalletConnect Gate: Session expired → "Reconnect wallet to continue" +# Uses pendingTxSafe1 with a WC signer whose session is NOT active. + +- runFlow: + file: ../../utils/setup/app-start.yml + +- runFlow: + file: ../../utils/setup/open-review-and-execute.yml + env: + SETUP_BUTTON_ID: 'e2eWcGateReconnect' + +# Verify the WalletConnect gate shows "Reconnect wallet to continue" +# instead of the normal execute/confirm button +- extendedWaitUntil: + visible: + id: 'reconnect-wallet-button' + timeout: 10000 +- assertVisible: 'Reconnect wallet to continue' + +# Tap "Reconnect wallet to continue" — the e2e mock marks the session active +- tapOn: + id: 'reconnect-wallet-button' + +# Verify the gate is dismissed (the underlying execute/insufficient-funds +# button is intentionally not asserted — those are distinct semantic states) +- extendedWaitUntil: + notVisible: + id: 'reconnect-wallet-button' + timeout: 10000 diff --git a/apps/mobile/e2e/tests/connect-signer/wc-gate-switch-network.yml b/apps/mobile/e2e/tests/connect-signer/wc-gate-switch-network.yml new file mode 100644 index 000000000..70a779b64 --- /dev/null +++ b/apps/mobile/e2e/tests/connect-signer/wc-gate-switch-network.yml @@ -0,0 +1,34 @@ +appId: ${APP_ID} +tags: + - connect-signer + - wc-gate +--- +# WalletConnect Gate: Wrong network → "Switch network to continue" +# Uses pendingTxSafe1 with a WC signer connected on the wrong chain. + +- runFlow: + file: ../../utils/setup/app-start.yml + +- runFlow: + file: ../../utils/setup/open-review-and-execute.yml + env: + SETUP_BUTTON_ID: 'e2eWcGateWrongNetwork' + +# Verify the WalletConnect gate shows "Switch network to continue" +# instead of the normal execute/confirm button +- extendedWaitUntil: + visible: + id: 'switch-network-button' + timeout: 10000 +- assertVisible: 'Switch network to continue' + +# Tap "Switch network to continue" — the e2e mock clears isWrongNetwork +- tapOn: + id: 'switch-network-button' + +# Verify the gate is dismissed (the underlying execute/insufficient-funds +# button is intentionally not asserted — those are distinct semantic states) +- extendedWaitUntil: + notVisible: + id: 'switch-network-button' + timeout: 10000 diff --git a/apps/mobile/e2e/utils/setup/open-review-and-execute.yml b/apps/mobile/e2e/utils/setup/open-review-and-execute.yml new file mode 100644 index 000000000..71c52eace --- /dev/null +++ b/apps/mobile/e2e/utils/setup/open-review-and-execute.yml @@ -0,0 +1,49 @@ +appId: ${APP_ID} +tags: + - util +--- +# =========================================== +# Open Review and Execute Flow +# =========================================== +# Shared flow for navigating from app start to the Review and Execute screen +# of the first pending transaction. Use after a TestCtrls button has seeded +# Redux with a pending-tx safe and any required signer/session state. +# +# Environment variables: +# SETUP_BUTTON_ID: testID of the TestCtrls button that seeds the scenario +# Example: 'e2eWcGateReconnect' + +# Seed Redux state for the scenario +- tapOn: + id: '${SETUP_BUTTON_ID}' + +# Wait for pending transactions list to load +- extendedWaitUntil: + visible: + id: 'pending-tx-list' + timeout: 10000 + +# Tap on the first pending transaction (Send) +- extendedWaitUntil: + visible: + text: 'Send' + timeout: 10000 +- tapOn: + text: 'Send' + index: 0 + +# Wait for the Confirm transaction screen +- extendedWaitUntil: + visible: + text: 'Confirm transaction' + timeout: 10000 + +# Tap Continue to navigate to the review screen +- assertVisible: + text: 'Continue' +- tapOn: + text: 'Continue' + +# Wait for the review screen to load +- waitForAnimationToEnd: + timeout: 2000 diff --git a/apps/mobile/src/features/WalletConnect/context/WalletConnectContext.e2e.tsx b/apps/mobile/src/features/WalletConnect/context/WalletConnectContext.e2e.tsx index 5b8c97681..56805e859 100644 --- a/apps/mobile/src/features/WalletConnect/context/WalletConnectContext.e2e.tsx +++ b/apps/mobile/src/features/WalletConnect/context/WalletConnectContext.e2e.tsx @@ -105,8 +105,12 @@ export function WalletConnectProvider({ children }: WalletConnectProviderProps) } }, []) - const reconnect = useCallback(async (_signerAddress: string) => { - Logger.info('[E2E] reconnect called') + const reconnect = useCallback(async (signerAddress: string) => { + Logger.info('[E2E] reconnect called — marking session active for', signerAddress) + walletConnectE2eState.set({ + isConnected: true, + address: getAddress(signerAddress), + }) }, []) const switchNetwork = useCallback(async (_chainId: string) => { @@ -114,7 +118,8 @@ export function WalletConnectProvider({ children }: WalletConnectProviderProps) }, []) const switchNetworkIfNeeded = useCallback(async () => { - Logger.info('[E2E] switchNetworkIfNeeded called') + Logger.info('[E2E] switchNetworkIfNeeded called — clearing isWrongNetwork') + walletConnectE2eState.set({ isWrongNetwork: false }) }, []) const sign = useCallback(async (_params: unknown): Promise => { diff --git a/apps/mobile/src/tests/e2e-maestro/components/TestCtrls.e2e.tsx b/apps/mobile/src/tests/e2e-maestro/components/TestCtrls.e2e.tsx index eaa4687a3..da64902ef 100644 --- a/apps/mobile/src/tests/e2e-maestro/components/TestCtrls.e2e.tsx +++ b/apps/mobile/src/tests/e2e-maestro/components/TestCtrls.e2e.tsx @@ -4,7 +4,13 @@ import { useDispatch } from 'react-redux' import { useRouter } from 'expo-router' import { useState } from 'react' import { setupOnboardedAccount, setupTestOnboarding, setupSeedPhraseImportAccount } from '../setup/onboardingSetup' -import { setupConnectSignerOwner, setupConnectSignerNonOwner, switchToOwnerState } from '../setup/connectSignerSetup' +import { + setupConnectSignerOwner, + setupConnectSignerNonOwner, + switchToOwnerState, + setupWcGateReconnect, + setupWcGateWrongNetwork, +} from '../setup/connectSignerSetup' import { setupOnboardedAccountForAssets } from '../setup/assetsSetup' import { setupPositionsTestSafe } from '../setup/positionsSetup' import { @@ -232,6 +238,20 @@ export function TestCtrls() { style={BTN} /> + {/* WalletConnect Gate Scenarios */} + setupWcGateReconnect(dispatch, router)} + accessibilityRole="button" + style={BTN} + /> + setupWcGateWrongNetwork(dispatch, router)} + accessibilityRole="button" + style={BTN} + /> + {/* Clipboard Verification Trigger */} setIsClipboardVisible(true)} /> diff --git a/apps/mobile/src/tests/e2e-maestro/setup/connectSignerSetup.ts b/apps/mobile/src/tests/e2e-maestro/setup/connectSignerSetup.ts index c168e1c9b..27141b577 100644 --- a/apps/mobile/src/tests/e2e-maestro/setup/connectSignerSetup.ts +++ b/apps/mobile/src/tests/e2e-maestro/setup/connectSignerSetup.ts @@ -3,8 +3,21 @@ import type { Router } from 'expo-router' import { addSafe } from '@/src/store/safesSlice' import { setActiveSafe } from '@/src/store/activeSafeSlice' import { updatePromptAttempts } from '@/src/store/notificationsSlice' -import { walletConnectE2eState } from '@/src/features/WalletConnect/context/walletConnectE2eState' -import { mockedActiveAccount, mockedActiveSafeInfo } from './mockData' +import { Signer } from '@/src/store/signersSlice' +import { setExecutionMethod } from '@/src/store/executionMethodSlice' +import { ExecutionMethod } from '@/src/features/HowToExecuteSheet/types' +import { + walletConnectE2eState, + WalletConnectE2eState, +} from '@/src/features/WalletConnect/context/walletConnectE2eState' +import { + mockedActiveAccount, + mockedActiveSafeInfo, + pendingTxSafe1, + pendingTxSafeInfo1, + mockedPendingTxSignerAddress, +} from './mockData' +import { setupPendingTxSafe } from './setupHelpers' import { Address } from '@/src/types/address' /** First owner from the mocked Safe — used for the happy path. */ @@ -61,3 +74,52 @@ export const setupConnectSignerNonOwner = (dispatch: Dispatch, router: Router) = setWcState(NON_OWNER_ADDRESS, false) onboardAndNavigate(dispatch, router) } + +// --------------------------------------------------------------------------- +// WalletConnectGate E2E tests +// --------------------------------------------------------------------------- + +/** + * Shared setup for WalletConnectGate tests. + * Creates a pending-tx safe with a WC signer and configures the gate state. + */ +const setupWcGateBase = (dispatch: Dispatch, router: Router, wcOverrides: Partial) => { + walletConnectE2eState.reset() + + const wcSigner: Signer = { + value: mockedPendingTxSignerAddress, + name: 'WC Gate Signer', + logoUri: null, + type: 'walletconnect', + walletName: WALLET_NAME, + walletIcon: WALLET_ICON, + } + + setupPendingTxSafe(dispatch, pendingTxSafe1, pendingTxSafeInfo1, 'WC Gate Test Safe', mockedPendingTxSignerAddress, { + signer: wcSigner, + }) + + // Force execution method to WITH_WC so relay doesn't override the gate + dispatch(setExecutionMethod(ExecutionMethod.WITH_WC)) + + // Configure the WC session / network state + walletConnectE2eState.set(wcOverrides) + + router.replace('/pending-transactions') +} + +/** Setup: WC signer with expired session → gate shows "Reconnect wallet to continue". */ +export const setupWcGateReconnect = (dispatch: Dispatch, router: Router) => { + // Post-reset defaults already model an expired WC session + // (isConnected=false, address=undefined). No overrides needed. + setupWcGateBase(dispatch, router, {}) +} + +/** Setup: WC signer connected on wrong network → gate shows "Switch network to continue". */ +export const setupWcGateWrongNetwork = (dispatch: Dispatch, router: Router) => { + setupWcGateBase(dispatch, router, { + isConnected: true, + address: mockedPendingTxSignerAddress, + isWrongNetwork: true, + }) +} diff --git a/apps/mobile/src/tests/e2e-maestro/setup/setupHelpers.ts b/apps/mobile/src/tests/e2e-maestro/setup/setupHelpers.ts index 8b146446d..d5a8b0810 100644 --- a/apps/mobile/src/tests/e2e-maestro/setup/setupHelpers.ts +++ b/apps/mobile/src/tests/e2e-maestro/setup/setupHelpers.ts @@ -5,7 +5,7 @@ import { updateSettings } from '@/src/store/settingsSlice' import { updatePromptAttempts } from '@/src/store/notificationsSlice' import { addSafe } from '@/src/store/safesSlice' import { addContact } from '@/src/store/addressBookSlice' -import { addSigner } from '@/src/store/signersSlice' +import { addSigner, Signer } from '@/src/store/signersSlice' import { setActiveSigner } from '@/src/store/activeSignerSlice' import { setActiveSafe } from '@/src/store/activeSafeSlice' @@ -55,6 +55,10 @@ export const setupSigner = (dispatch: Dispatch, signerAddress: string) => { * - Signer setup * - Safe setup * - Active signer and safe + * + * By default registers a private-key signer for `signerAddress`. + * Pass `options.signer` to register a pre-built signer (e.g. a WalletConnect + * signer) instead. */ export const setupPendingTxSafe = ( dispatch: Dispatch, @@ -62,12 +66,19 @@ export const setupPendingTxSafe = ( info: SafeOverview, name: string, signerAddress: string, + options: { signer?: Signer } = {}, ) => { setupBaseConfig(dispatch) - const mockedSigner = setupSigner(dispatch, signerAddress) + let signer: Signer + if (options.signer) { + signer = options.signer + dispatch(addSigner(signer)) + } else { + signer = setupSigner(dispatch, signerAddress) + } setupSafe(dispatch, account, info, name) - dispatch(setActiveSigner({ safeAddress: account.address, signer: mockedSigner })) + dispatch(setActiveSigner({ safeAddress: account.address, signer })) dispatch(setActiveSafe(account)) } diff --git a/docs/resolutions.md b/docs/resolutions.md new file mode 100644 index 000000000..79f4993e5 --- /dev/null +++ b/docs/resolutions.md @@ -0,0 +1,15 @@ +# Yarn `resolutions` rationale + +The root [`package.json`](../package.json) `resolutions` block forces specific +versions of transitive dependencies. Each entry has a reason — without one +it's impossible to tell, months later, whether a pin is still load-bearing +or safe to drop. Add a short note here whenever a new entry is added. + +## Entries + +- **`isows: 1.0.7`** — dedup the transitive version that ships under viem + (1.0.6 → 1.0.7 is a maintenance bump, no CVE; the pin avoids two copies + in the bundle when other packages resolve isows differently). + +> When you add an entry to `resolutions`, add the matching line here in the +> same commit. diff --git a/package.json b/package.json index 49b1818ab..c48d84876 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@ethersproject/signing-key/elliptic": "^6.6.1", "stylus": "0.64.0", "viem": "2.21.55", + "isows": "1.0.7", "webpack": "5.97.1", "react-native-quick-base64": "2.2.2", "@tamagui/image@npm:2.0.0-rc.26": "patch:@tamagui/image@npm%3A2.0.0-rc.26#~/.yarn/patches/@tamagui-image-npm-2.0.0-rc.26-04087a216c.patch" diff --git a/yarn.lock b/yarn.lock index 5f53e9a5e..6e11c8f71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30989,12 +30989,12 @@ __metadata: languageName: node linkType: hard -"isows@npm:1.0.6": - version: 1.0.6 - resolution: "isows@npm:1.0.6" +"isows@npm:1.0.7": + version: 1.0.7 + resolution: "isows@npm:1.0.7" peerDependencies: ws: "*" - checksum: 10/ab9e85b50bcc3d70aa5ec875aa2746c5daf9321cb376ed4e5434d3c2643c5d62b1f466d93a05cd2ad0ead5297224922748c31707cb4fbd68f5d05d0479dce99c + checksum: 10/044b949b369872882af07b60b613b5801ae01b01a23b5b72b78af80c8103bbeed38352c3e8ceff13a7834bc91fd2eb41cf91ec01d59a041d8705680e6b0ec546 languageName: node linkType: hard