mirror of
https://github.com/luxfi/safe-wallet.git
synced 2026-07-26 22:53:37 +00:00
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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user