tests(mobile): e2e coverage for WalletConnectGate flows (#7760)

* 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<PascalCase>` 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<PascalCase>`.

* 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.
This commit is contained in:
Tim
2026-04-28 10:35:30 +02:00
committed by GitHub
parent 51cf336a92
commit 495d3cc054
12 changed files with 270 additions and 13 deletions
+6
View File
@@ -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<PascalCase>` 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.
---
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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<unknown> => {
@@ -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 */}
<Pressable
testID="e2eWcGateReconnect"
onPress={() => setupWcGateReconnect(dispatch, router)}
accessibilityRole="button"
style={BTN}
/>
<Pressable
testID="e2eWcGateWrongNetwork"
onPress={() => setupWcGateWrongNetwork(dispatch, router)}
accessibilityRole="button"
style={BTN}
/>
{/* Clipboard Verification Trigger */}
<ClipboardVerificationTrigger onPress={() => setIsClipboardVisible(true)} />
</View>
@@ -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>) => {
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,
})
}
@@ -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))
}
+15
View File
@@ -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.
+1
View File
@@ -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"
+4 -4
View File
@@ -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