Skip to content

Commit 38817db

Browse files
test: migrates the WalletConnect performance test (#29785)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** This PR migrates the WC performance test to the new framework. `skip-e2e` has been applied since all changes are related to Appium. ### Framework changes: - Allows for lazy fetch on `getElementByNameiOS` iOS Run: https://app-automate.browserstack.com/dashboard/v2/builds/8c27168975bc74376449c6f9fa82158c549c8f41/sessions/195066d41131f7143a0b1989f5e0b710d39999f4 (Quality-Gates failed) Android Run: https://app-automate.browserstack.com/dashboard/v2/builds/91d934900ddd178f4d255db708e8a14ac3153aed/sessions/05e33619e97368cfd712ec14a41c19033b01ce45 <!-- 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: ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMQA-1725 ## **Manual testing steps** N/A ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A <!-- [screenshots/recordings] --> ### **After** N/A <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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] > **Medium Risk** > Moderate risk because it rewires a cross-context (native/web) performance test and adds new iOS/Android selectors and retry logic, which can introduce flaky behavior if locators or timing differ across devices. > > **Overview** > Migrates the Uniswap WalletConnect performance interaction test from the legacy JS/WebdriverIO setup to the new Playwright-based fixture (`uniswap-interaction.spec.js` replaced with `uniswap-interaction.spec.ts`), updating flows to use `loginToAppPlaywright`, `native-browser.flow`, and `PlaywrightContextHelpers`. > > Adds a new `UniswapDapp` page object to encapsulate Uniswap connect + WalletConnect + MetaMask deep link interactions and “Uniswap displayed” checks with platform-specific selectors and retry/wait helpers. > > Extends test/framework selectors by adding an optional `lazy` mode to `PlaywrightMatchers.getElementByNameiOS`, and updates `DappConnectionModal` selectors/behavior for iOS (edit accounts) plus a small tap/refactor in network selection. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit cdc8842. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7f3f590 commit 38817db

5 files changed

Lines changed: 363 additions & 113 deletions

File tree

tests/framework/PlaywrightMatchers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,19 @@ export default class PlaywrightMatchers {
229229
/**
230230
* Get element by name on iOS
231231
* @param name - The name to search for
232+
* @param lazy - Whether to get a lazy element. Lazy elements are not required to be present in the DOM. This is useful for negative assertions where the element may never have been rendered (e.g. waitForDisplayed({ reverse: true })).
232233
* @returns The wrapped element
233234
*/
234-
static async getElementByNameiOS(name: string): Promise<PlaywrightElement> {
235+
static async getElementByNameiOS(
236+
name: string,
237+
lazy = false,
238+
): Promise<PlaywrightElement> {
235239
const isIOS = await PlatformDetector.isIOS();
236240
if (!isIOS) throw new Error('This function is only valid for iOS');
237241
const xpath = `//*[contains(@name,'${name}')]`;
242+
if (lazy) {
243+
return await this.getLazyElementByXPath(xpath);
244+
}
238245
return await this.getElementByXPath(xpath);
239246
}
240247

tests/page-objects/MMConnect/DappConnectionModal.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import PlaywrightMatchers from '../../framework/PlaywrightMatchers';
88
import UnifiedGestures from '../../framework/UnifiedGestures';
99
import { getDriver } from '../../framework/PlaywrightUtilities';
1010
import { ConnectAccountBottomSheetSelectorsIDs } from '../../../app/components/Views/AccountConnect/ConnectAccountBottomSheet.testIds';
11+
import { ConnectedAccountsSelectorsIDs } from '../../../app/components/Views/AccountConnect/ConnectedAccountModal.testIds';
1112
import { AccountCellIds } from '../../../app/component-library/components-temp/MultichainAccounts/AccountCell/AccountCell.testIds';
1213
import { CellComponentSelectorsIDs } from '../../../app/component-library/components/Cells/Cell/CellComponent.testIds';
1314
import { sleep } from '../../framework';
@@ -33,10 +34,16 @@ class DappConnectionModal {
3334

3435
get editAccountsButton(): EncapsulatedElementType {
3536
return encapsulated({
36-
appium: () =>
37-
PlaywrightMatchers.getElementByXPath(
38-
'//android.view.ViewGroup[@content-desc="Edit accounts"]',
39-
),
37+
appium: {
38+
android: () =>
39+
PlaywrightMatchers.getElementByXPath(
40+
'//android.view.ViewGroup[@content-desc="Edit accounts"]',
41+
),
42+
ios: () =>
43+
PlaywrightMatchers.getElementById(
44+
ConnectedAccountsSelectorsIDs.ACCOUNT_LIST_BOTTOM_SHEET,
45+
),
46+
},
4047
});
4148
}
4249

@@ -130,10 +137,10 @@ class DappConnectionModal {
130137
direction: 'down',
131138
percent: 1.0,
132139
});
133-
const element = await asPlaywrightElement(
140+
const networkButton = await asPlaywrightElement(
134141
this.getNetworkButton(networkName),
135142
);
136-
await element.click();
143+
await networkButton.click();
137144
},
138145
});
139146
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import {
2+
asPlaywrightElement,
3+
encapsulated,
4+
encapsulatedAction,
5+
EncapsulatedElementType,
6+
PlatformDetector,
7+
PlaywrightAssertions,
8+
PlaywrightGestures,
9+
PlaywrightMatchers,
10+
sleep,
11+
UnifiedGestures,
12+
} from '../../framework';
13+
14+
class UniswapDapp {
15+
private getByXPath(xpath: string): EncapsulatedElementType {
16+
return encapsulated({
17+
appium: () => PlaywrightMatchers.getLazyElementByXPath(xpath),
18+
});
19+
}
20+
21+
get connectButton(): EncapsulatedElementType {
22+
return encapsulated({
23+
appium: {
24+
android: () =>
25+
PlaywrightMatchers.getLazyElementByXPath(
26+
'//*[@data-testid="navbar-connect-wallet"]',
27+
),
28+
ios: () =>
29+
PlaywrightMatchers.getElementById('Connect', { exact: true }),
30+
},
31+
});
32+
}
33+
34+
get walletConnect(): EncapsulatedElementType {
35+
return encapsulated({
36+
appium: {
37+
android: () =>
38+
PlaywrightMatchers.getElementByXPath(
39+
'//*[contains(normalize-space(.), "WalletConnect")]',
40+
),
41+
ios: () =>
42+
PlaywrightMatchers.getElementByXPath(
43+
'//XCUIElementTypeStaticText[@name="WalletConnect"]',
44+
),
45+
},
46+
});
47+
}
48+
49+
get metaMaskWalletOption(): EncapsulatedElementType {
50+
return encapsulated({
51+
appium: {
52+
android: () =>
53+
PlaywrightMatchers.getLazyElementByXPath(
54+
'//android.widget.Button[@text="MetaMask MetaMask"]',
55+
),
56+
ios: () =>
57+
PlaywrightMatchers.getElementById('MetaMask MetaMask', {
58+
exact: true,
59+
}),
60+
},
61+
});
62+
}
63+
64+
get metaMaskDeeplinkButton(): EncapsulatedElementType {
65+
return encapsulated({
66+
appium: {
67+
android: () =>
68+
PlaywrightMatchers.getLazyElementByXPath(
69+
'//android.widget.TextView[@text="MetaMask"]',
70+
),
71+
ios: () =>
72+
PlaywrightMatchers.getLazyElementByXPath(
73+
'//XCUIElementTypeOther[@name="textfield"]',
74+
),
75+
},
76+
});
77+
}
78+
79+
get uniswapDialog(): EncapsulatedElementType {
80+
return this.getByXPath('//android.app.AlertDialog');
81+
}
82+
83+
get uniswapIcon(): EncapsulatedElementType {
84+
return encapsulated({
85+
appium: () => PlaywrightMatchers.getElementById('account-icon'),
86+
});
87+
}
88+
89+
get solanaPopup(): EncapsulatedElementType {
90+
return encapsulated({
91+
appium: () =>
92+
PlaywrightMatchers.getElementByText('Use Solana on Uniswap'),
93+
});
94+
}
95+
96+
get SolanaPopup(): EncapsulatedElementType {
97+
return this.solanaPopup;
98+
}
99+
100+
async waitForConnectButtonVisible(timeoutMs = 20000): Promise<void> {
101+
await this.waitForElementVisible(
102+
this.connectButton,
103+
timeoutMs,
104+
'UniswapDapp: connect button not visible',
105+
);
106+
}
107+
108+
async waitForWalletConnectVisible(timeoutMs = 15000): Promise<void> {
109+
await this.waitForElementVisible(
110+
this.walletConnect,
111+
timeoutMs,
112+
'UniswapDapp: WalletConnect option not visible',
113+
);
114+
}
115+
116+
async tapConnect(): Promise<void> {
117+
await PlaywrightGestures.waitAndTap(
118+
await asPlaywrightElement(this.connectButton),
119+
{
120+
delay: 3000, // 3 seconds - DOM might not be ready yet
121+
},
122+
);
123+
}
124+
125+
async tapOnWalletConnect(): Promise<void> {
126+
await PlaywrightGestures.waitAndTap(
127+
await asPlaywrightElement(this.walletConnect),
128+
{
129+
delay: 3000, // 3 seconds - DOM might not be ready yet
130+
},
131+
);
132+
}
133+
134+
async connectWithMetaMask(): Promise<void> {
135+
await this.waitForConnectButtonVisible();
136+
await this.tapConnect();
137+
await this.waitForWalletConnectVisible();
138+
await this.tapOnWalletConnect();
139+
}
140+
141+
async connectIOS(timeoutMs = 20000): Promise<void> {
142+
await this.waitForConnectButtonVisible(timeoutMs);
143+
await this.tapConnect();
144+
}
145+
146+
async selectWalletConnectOption(): Promise<void> {
147+
await this.tapOnWalletConnect();
148+
}
149+
150+
async tapOnMetaMaskWalletOption(): Promise<void> {
151+
await UnifiedGestures.waitAndTap(this.metaMaskWalletOption, {
152+
description: 'tap MetaMask wallet option',
153+
});
154+
}
155+
156+
async tapOnMetaMaskDeeplinkButton(): Promise<void> {
157+
await encapsulatedAction({
158+
appium: async () => {
159+
await sleep(2000);
160+
await PlaywrightGestures.waitAndTap(
161+
await asPlaywrightElement(this.metaMaskDeeplinkButton),
162+
);
163+
},
164+
});
165+
}
166+
167+
async tapOnMetaMaskWalletOptionAndOpenDeeplink(): Promise<void> {
168+
await this.tapOnMetaMaskWalletOption();
169+
if (PlatformDetector.isAndroid()) {
170+
await this.tapOnMetaMaskDeeplinkButton();
171+
}
172+
}
173+
174+
async isUniswapDisplayed(timeoutMs = 30000): Promise<void> {
175+
await encapsulatedAction({
176+
appium: async () => {
177+
if (PlatformDetector.isAndroid()) {
178+
const dialogVisible = await this.isElementVisible(
179+
this.uniswapDialog,
180+
timeoutMs,
181+
);
182+
183+
if (dialogVisible) {
184+
return;
185+
}
186+
187+
const iconVisible = await this.isElementVisible(
188+
this.uniswapIcon,
189+
timeoutMs,
190+
);
191+
192+
if (!iconVisible) {
193+
throw new Error(
194+
'Neither Uniswap dialog nor account icon is visible in Android context',
195+
);
196+
}
197+
198+
return;
199+
}
200+
201+
await this.waitForElementVisible(
202+
this.solanaPopup,
203+
timeoutMs,
204+
'UniswapDapp: Solana popup not visible',
205+
);
206+
},
207+
});
208+
}
209+
210+
private async waitForElementVisible(
211+
targetElement: EncapsulatedElementType,
212+
timeoutMs: number,
213+
timeoutMsg: string,
214+
): Promise<void> {
215+
await encapsulatedAction({
216+
appium: async () => {
217+
await PlaywrightAssertions.expectConditionWithRetry(
218+
async () => {
219+
const resolvedElement = await asPlaywrightElement(targetElement);
220+
await resolvedElement.waitForDisplayed({
221+
timeout: timeoutMs,
222+
timeoutMsg,
223+
});
224+
},
225+
{
226+
maxRetries: 5,
227+
description: timeoutMsg,
228+
},
229+
);
230+
},
231+
});
232+
}
233+
234+
private async isElementVisible(
235+
targetElement: EncapsulatedElementType,
236+
timeoutMs: number,
237+
): Promise<boolean> {
238+
try {
239+
const resolvedElement = await asPlaywrightElement(targetElement);
240+
await resolvedElement.waitForDisplayed({ timeout: timeoutMs });
241+
return true;
242+
} catch {
243+
return false;
244+
}
245+
}
246+
}
247+
248+
export default new UniswapDapp();

0 commit comments

Comments
 (0)