Skip to content

Commit 48275cf

Browse files
ffmcgee725jiexiadonesky1
authored
test: react native app e2e appwright test coverage (legacy evm) (#26566)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR establishes E2E test coverage for legacy evm MetaMask Connect flows within the React Native Playground app, ensuring the MetaMask Connect correctly manages simultaneous connections across multiple networks and that permissions, request routing, and session lifecycle all function end-to-end. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/WAPI-1052 ## **Manual testing steps** ~~1. Pull https://github.com/MetaMask/connect-monorepo 2. `yarn && yarn build` 3. Run appropriate test dapp locally `integrations/wagmi` or `playground/legacy-evm-react-vite-playground` using `yarn dev --host`~~ 4. In the mobile repo, update `appwright/appwright.config.ts` for the `mm-connect-android-local` entry 5. You will need a prefined SRP android build. You can find one **[here](https://app.bitrise.io/build/2f2254fc-34bf-4291-bbb5-d525aa01d717?tab=artifacts)**. 6. Add entry for `E2E_PASSWORD` in `.js.env` and source it with `source .js.env`. You can get the password from someone in slack. 7. Determine which `appwright/tests/mm-connect/connection-*.spec.js` you want to run ~~8. Update the dapp url constant to use `10.0.2.2` for the host~~ 9. Ensure the other tests in the suite are marked `.skip` 10. Follow instructions in `tests/performance/mm-connect/README.md` (L#51 - L#105) to create APK for react native playground to be used in e2e test 11. `yarn appwright test --project mm-connect-android-local --config appwright/appwright.config.ts` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Test-only changes; main risk is increased CI runtime/flakiness due to added end-to-end app automation and new selectors. > > **Overview** > Adds a new RN-playground Appwright spec, `legacy-evm-rn-connect.spec.js`, that exercises the **Legacy EVM** MetaMask Connect flow end-to-end (connect, `personal_sign`, cancelled `eth_sendTransaction`, and `wallet_switchEthereumChain` verification). > > Extends `RNPlaygroundDapp` with Legacy EVM-specific selectors/actions/assertions (connect-legacy button, card fields, response text, and action buttons), and updates the performance/MM Connect READMEs to document the new test and RN playground APK requirements/commands. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e6b529e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jiexi Luan <jiexiluan@gmail.com> Co-authored-by: Alex Donesky <adonesky@gmail.com>
1 parent 26d36a5 commit 48275cf

4 files changed

Lines changed: 297 additions & 10 deletions

File tree

tests/performance/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,13 @@ Integration tests for MetaMask Connect:
276276
- `connection-multichain.spec.js` - Multichain connection performance
277277
- `connection-wagmi.spec.js` - Wagmi integration performance
278278
- `multichain-rn-connect.spec.js` - Multichain + Solana via the React Native Playground APK
279+
- `legacy-evm-rn-connect.spec.js` - Legacy EVM connection via the React Native Playground APK
279280
280-
> The RN playground test requires a separate APK to be built and installed on the
281-
> emulator before running. See [`tests/performance/mm-connect/README.md`](mm-connect/README.md)
282-
> for full setup instructions.
281+
> The RN playground tests require a separate APK built from the
282+
> [`playground/react-native-playground`](https://github.com/MetaMask/connect-monorepo/tree/main/playground/react-native-playground)
283+
> directory of the [connect-monorepo](https://github.com/MetaMask/connect-monorepo).
284+
> The APK must be installed on the emulator before running.
285+
> See [`tests/performance/mm-connect/README.md`](mm-connect/README.md) for full setup instructions.
283286
284287
## Performance Tracking System
285288

tests/performance/mm-connect/README.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ This directory contains Appwright-based E2E tests for MetaMask Connect flows.
44

55
## Test Files
66

7-
| File | Description |
8-
| ------------------------------- | ----------------------------------------------------------- |
9-
| `connection-evm.spec.js` | Legacy EVM connection via Browser Playground in Chrome |
10-
| `connection-multichain.spec.js` | Multichain API connection via Browser Playground in Chrome |
11-
| `connection-wagmi.spec.js` | Wagmi connector via Browser Playground in Chrome |
12-
| `multichain-rn-connect.spec.js` | **Multichain + Solana via the React Native Playground APK** |
7+
| File | Description |
8+
| ------------------------------- | ------------------------------------------------------------- |
9+
| `connection-evm.spec.js` | Legacy EVM connection via Browser Playground in Chrome |
10+
| `connection-multichain.spec.js` | Multichain API connection via Browser Playground in Chrome |
11+
| `connection-wagmi.spec.js` | Wagmi connector via Browser Playground in Chrome |
12+
| `multichain-rn-connect.spec.js` | **Multichain + Solana via the React Native Playground APK** |
13+
| `legacy-evm-rn-connect.spec.js` | **Legacy EVM connection via the React Native Playground APK** |
1314

1415
## React Native Playground Setup
1516

@@ -156,12 +157,18 @@ Then run:
156157
yarn run-appwright:mm-connect-android-local
157158
```
158159

159-
Or run only the RN playground spec:
160+
Or run individual RN playground specs:
160161

161162
```bash
163+
# Multichain + Solana test
162164
npx appwright test tests/performance/mm-connect/multichain-rn-connect.spec.js \
163165
--project mm-connect-android-local \
164166
--config tests/appwright.config.ts
167+
168+
# Legacy EVM test
169+
npx appwright test tests/performance/mm-connect/legacy-evm-rn-connect.spec.js \
170+
--project mm-connect-android-local \
171+
--config tests/appwright.config.ts
165172
```
166173

167174
### What the Test Covers
@@ -184,6 +191,25 @@ MetaMask Connect flow:
184191
(`sdk.disconnect()`, the equivalent of `wallet_revokeSession`) and verifies
185192
the session is fully terminated on both the dApp and wallet sides.
186193

194+
The `legacy-evm-rn-connect` test validates the Legacy EVM connection flow:
195+
196+
1. **accountsChanged propagation** — connects via the Legacy EVM SDK and
197+
verifies the dapp receives accounts and displays the active account.
198+
199+
2. **personal_sign** — sends a personal_sign request, approves it in the
200+
wallet, and verifies the signature is returned to the dapp.
201+
202+
3. **eth_sendTransaction (cancel)** — sends a transaction request, cancels it
203+
in the wallet (to avoid spending funds), and verifies the dapp handles the
204+
rejection.
205+
206+
4. **Chain switching from the dapp** — calls `wallet_switchEthereumChain` to
207+
switch to Polygon from the dapp side and verifies the chain ID updates.
208+
209+
5. **Chain switching from the wallet** — switches to Ethereum Mainnet in the
210+
wallet's network picker and verifies the dapp receives the `chainChanged`
211+
event.
212+
187213
### How It Works
188214

189215
Unlike the browser-based tests (which serve a web dApp in Chrome and use
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { test, expect } from '../../framework/fixtures/performance';
2+
3+
import { login } from '../../framework/utils/Flows.js';
4+
import RNPlaygroundDapp from '../../../wdio/screen-objects/RNPlaygroundDapp.js';
5+
import DappConnectionModal from '../../../wdio/screen-objects/Modals/DappConnectionModal.js';
6+
import SignModal from '../../../wdio/screen-objects/Modals/SignModal.js';
7+
import {
8+
unlockIfLockScreenVisible,
9+
ensurePlaygroundInstalled,
10+
} from './utils.js';
11+
import AppwrightGestures from '../../framework/AppwrightGestures.ts';
12+
13+
const DEFAULT_SCROLL_PARAMS = {
14+
scrollParams: { percent: 0.2 },
15+
};
16+
17+
/**
18+
* After a MetaMask action (approve / sign / cancel), wait for the callback
19+
* deeplink to return to the playground. Falls back to activateApp if the
20+
* automatic return does not happen within a short window.
21+
*/
22+
async function returnToPlayground() {
23+
await AppwrightGestures.wait(2000);
24+
await RNPlaygroundDapp.ensureInPlayground();
25+
}
26+
27+
test.beforeAll(() => {
28+
ensurePlaygroundInstalled();
29+
});
30+
31+
test('@metamask/connect-legacy-evm-rn - Connect via Legacy EVM, sign, send transaction, and switch chains', async ({
32+
device,
33+
}) => {
34+
RNPlaygroundDapp.device = device;
35+
DappConnectionModal.device = device;
36+
SignModal.device = device;
37+
38+
await device.webDriverClient.updateSettings({
39+
waitForIdleTimeout: 100,
40+
waitForSelectorTimeout: 0,
41+
shouldWaitForQuiescence: false,
42+
});
43+
44+
//
45+
// 1. Login to MetaMask wallet
46+
//
47+
48+
await login(device);
49+
50+
//
51+
// 2. Switch to the RN playground and connect via Legacy EVM
52+
//
53+
54+
await RNPlaygroundDapp.switchToPlayground();
55+
await RNPlaygroundDapp.waitForPlaygroundReady();
56+
57+
await RNPlaygroundDapp.tapConnectLegacy();
58+
await AppwrightGestures.wait(3000);
59+
60+
await unlockIfLockScreenVisible(device);
61+
await AppwrightGestures.wait(5000);
62+
await DappConnectionModal.tapConnectButton();
63+
64+
//
65+
// 3. Verify accountsChanged — Legacy EVM card visible with accounts
66+
//
67+
68+
await returnToPlayground();
69+
await AppwrightGestures.wait(2000);
70+
71+
await RNPlaygroundDapp.scrollToElement(RNPlaygroundDapp.appTitle, {
72+
scrollParams: { direction: 'down' },
73+
});
74+
await RNPlaygroundDapp.scrollToElement(
75+
RNPlaygroundDapp.legacyEvmCard,
76+
DEFAULT_SCROLL_PARAMS,
77+
);
78+
await RNPlaygroundDapp.assertLegacyEvmConnected();
79+
await RNPlaygroundDapp.assertLegacyEvmHasAccounts();
80+
await RNPlaygroundDapp.assertLegacyEvmActiveAccount();
81+
82+
const initialChainId = await RNPlaygroundDapp.getLegacyEvmChainId();
83+
console.log(`Initial chain ID: ${initialChainId}`);
84+
85+
//
86+
// 4. personal_sign — request, approve, verify result
87+
//
88+
89+
await RNPlaygroundDapp.scrollToElement(
90+
RNPlaygroundDapp.legacyEvmBtnPersonalSign,
91+
DEFAULT_SCROLL_PARAMS,
92+
);
93+
await RNPlaygroundDapp.tapLegacyEvmButton(
94+
RNPlaygroundDapp.legacyEvmBtnPersonalSign,
95+
);
96+
await AppwrightGestures.wait(3000);
97+
98+
await unlockIfLockScreenVisible(device);
99+
await AppwrightGestures.wait(1000);
100+
await SignModal.tapConfirmButton();
101+
102+
await returnToPlayground();
103+
await AppwrightGestures.wait(1000);
104+
105+
// Verify signature was returned (hex string starting with 0x)
106+
await RNPlaygroundDapp.scrollToElement(
107+
RNPlaygroundDapp.legacyEvmResponseText,
108+
DEFAULT_SCROLL_PARAMS,
109+
);
110+
const signResponse = await RNPlaygroundDapp.getLegacyEvmResponseText();
111+
console.log(`personal_sign response: ${signResponse}`);
112+
expect(signResponse).toContain('0x');
113+
114+
//
115+
// 5. eth_sendTransaction — request, cancel (to avoid spending funds)
116+
//
117+
118+
await RNPlaygroundDapp.scrollToElement(
119+
RNPlaygroundDapp.legacyEvmBtnSendTransaction,
120+
DEFAULT_SCROLL_PARAMS,
121+
);
122+
await RNPlaygroundDapp.tapLegacyEvmButton(
123+
RNPlaygroundDapp.legacyEvmBtnSendTransaction,
124+
);
125+
await AppwrightGestures.wait(3000);
126+
127+
await unlockIfLockScreenVisible(device);
128+
await AppwrightGestures.wait(1000);
129+
130+
// Cancel the transaction to avoid spending real funds
131+
await SignModal.tapCancelButton();
132+
133+
await returnToPlayground();
134+
await AppwrightGestures.wait(1000);
135+
136+
// The dapp should show an error (user rejected) in the response
137+
await RNPlaygroundDapp.scrollToElement(
138+
RNPlaygroundDapp.legacyEvmResponseText,
139+
DEFAULT_SCROLL_PARAMS,
140+
);
141+
const txResponse = await RNPlaygroundDapp.getLegacyEvmResponseText();
142+
console.log(`eth_sendTransaction (cancelled) response: ${txResponse}`);
143+
expect(txResponse.toLowerCase()).toContain('denied');
144+
145+
//
146+
// 6. Chain switching from the dapp — wallet_switchEthereumChain
147+
// Switch to Polygon from the dapp, verify the chain ID updates.
148+
//
149+
150+
await RNPlaygroundDapp.scrollToElement(
151+
RNPlaygroundDapp.legacyEvmBtnSwitchPolygon,
152+
DEFAULT_SCROLL_PARAMS,
153+
);
154+
await RNPlaygroundDapp.tapLegacyEvmButton(
155+
RNPlaygroundDapp.legacyEvmBtnSwitchPolygon,
156+
);
157+
await AppwrightGestures.wait(3000);
158+
159+
// The switch opens MetaMask with a network approval dialog.
160+
// The SwitchChainApproval dialog uses "connect-button" as its confirm testID.
161+
await unlockIfLockScreenVisible(device);
162+
await AppwrightGestures.wait(1000);
163+
await DappConnectionModal.tapConnectButton();
164+
165+
await returnToPlayground();
166+
await AppwrightGestures.wait(2000);
167+
168+
// Verify chain ID updated to Polygon (0x89)
169+
await RNPlaygroundDapp.scrollToElement(
170+
RNPlaygroundDapp.legacyEvmChainIdValue,
171+
{ scrollParams: { direction: 'down' } },
172+
);
173+
const polygonChainId = await RNPlaygroundDapp.getLegacyEvmChainId();
174+
console.log(`Chain ID after dapp switch to Polygon: ${polygonChainId}`);
175+
expect(polygonChainId).toContain('0x89');
176+
});

wdio/screen-objects/RNPlaygroundDapp.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,46 @@ class RNPlaygroundDapp {
6969
return this._getByTestId('app-section-error');
7070
}
7171

72+
get connectLegacyButton() {
73+
return this._getByTestId('app-btn-connect-legacy');
74+
}
75+
76+
// ============================================================
77+
// LEGACY EVM CARD SELECTORS
78+
// ============================================================
79+
80+
get legacyEvmCard() {
81+
return this._getByTestId('legacy-evm-card');
82+
}
83+
84+
get legacyEvmChainIdValue() {
85+
return this._getByTestId('legacy-evm-chain-id-value');
86+
}
87+
88+
get legacyEvmAccountsValue() {
89+
return this._getByTestId('legacy-evm-accounts-value');
90+
}
91+
92+
get legacyEvmActiveAccount() {
93+
return this._getByTestId('legacy-evm-active-account');
94+
}
95+
96+
get legacyEvmResponseText() {
97+
return this._getByTestId('legacy-evm-response-text');
98+
}
99+
100+
get legacyEvmBtnPersonalSign() {
101+
return this._getByTestId('legacy-evm-btn-personal-sign');
102+
}
103+
104+
get legacyEvmBtnSendTransaction() {
105+
return this._getByTestId('legacy-evm-btn-send-transaction');
106+
}
107+
108+
get legacyEvmBtnSwitchPolygon() {
109+
return this._getByTestId('legacy-evm-btn-switch-polygon');
110+
}
111+
72112
// ============================================================
73113
// NETWORK CHECKBOX SELECTORS
74114
// ============================================================
@@ -172,6 +212,18 @@ class RNPlaygroundDapp {
172212
await AppwrightGestures.tap(elem);
173213
}
174214

215+
async tapConnectLegacy() {
216+
if (!this._device) return;
217+
const elem = await this.connectLegacyButton;
218+
await AppwrightGestures.tap(elem);
219+
}
220+
221+
async tapLegacyEvmButton(buttonPromise) {
222+
if (!this._device) return;
223+
const elem = await buttonPromise;
224+
await AppwrightGestures.tap(elem);
225+
}
226+
175227
async tapDisconnect() {
176228
if (!this._device) return;
177229
const elem = await this.disconnectButton;
@@ -275,6 +327,36 @@ class RNPlaygroundDapp {
275327
await expect(code).toBeVisible({ timeout: timeoutMs });
276328
}
277329

330+
async assertLegacyEvmConnected(timeoutMs = 15000) {
331+
if (!this._device) return;
332+
const card = await this.legacyEvmCard;
333+
await expect(card).toBeVisible({ timeout: timeoutMs });
334+
}
335+
336+
async assertLegacyEvmHasAccounts(timeoutMs = 10000) {
337+
if (!this._device) return;
338+
const accounts = await this.legacyEvmAccountsValue;
339+
await expect(accounts).toBeVisible({ timeout: timeoutMs });
340+
}
341+
342+
async assertLegacyEvmActiveAccount(timeoutMs = 10000) {
343+
if (!this._device) return;
344+
const activeAcct = await this.legacyEvmActiveAccount;
345+
await expect(activeAcct).toBeVisible({ timeout: timeoutMs });
346+
}
347+
348+
async getLegacyEvmChainId() {
349+
if (!this._device) return null;
350+
const chainIdElem = await this.legacyEvmChainIdValue;
351+
return await chainIdElem.getText();
352+
}
353+
354+
async getLegacyEvmResponseText() {
355+
if (!this._device) return null;
356+
const resp = await this.legacyEvmResponseText;
357+
return await resp.getText();
358+
}
359+
278360
/**
279361
* Assert the result code text contains the expected substring.
280362
*/

0 commit comments

Comments
 (0)