Skip to content

Commit ace5c7c

Browse files
authored
test: Migrate page objects to the new framework (perf/Predict) (#27642)
<!-- 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** > Migrates several E2E page objects (Predict flows and `TransactionPayConfirmation`) from Detox-only elements/gestures to the new unified `encapsulated` element model with `UnifiedGestures`, adding Appium/Playwright locators and framework-conditional actions via `encapsulatedAction`. > > Extends selectors by introducing `PredictBalanceSelectorsText.AVAILABLE_BALANCE` (sourced from `enContent`) and updates Predict market list/details interactions to use category-label mapping, outcome-button ancestor/XPath matching, and additional visibility/assertion helpers. Also adds a `TabBarComponent.tapBrowser()` helper and tightens `tapWallet()` validation to assert `WalletView.totalBalance` visibility. > ## **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: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **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] > **Medium Risk** > Moderate risk because it refactors E2E test page objects and gesture plumbing to a new cross-framework abstraction; failures will surface as flaky/broken tests across Detox/Appium rather than runtime app issues. > > **Overview** > Migrates several E2E page objects (Predict market list/details and `TransactionPayConfirmation`) from Detox-only elements/gestures to the unified `encapsulated` element model, adding Appium/Playwright locators and using `UnifiedGestures` plus `encapsulatedAction` where Detox-specific delays/scrolling are needed. > > Extends Predict selectors with `PredictBalanceSelectorsText.AVAILABLE_BALANCE` and updates Predict interactions to use category label mapping, ancestor/XPath outcome targeting, and new visibility/assertion helpers (e.g., tab content checks, volume label). > > Enhances the gesture abstraction by allowing `UnifiedGestureOptions` to pass Detox-only `scrollToElement` parameters (`direction`, `scrollAmount`), and adds a `TabBarComponent.tapBrowser()` convenience method. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cb77792. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d3b6348 commit ace5c7c

6 files changed

Lines changed: 705 additions & 173 deletions

File tree

app/components/UI/Predict/Predict.testIds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ export const PredictBalanceSelectorsIDs = {
242242
BALANCE_CARD: 'predict-balance-card',
243243
} as const;
244244

245+
export const PredictBalanceSelectorsText = {
246+
AVAILABLE_BALANCE: enContent.predict.available_balance,
247+
} as const;
248+
245249
// ========================================
246250
// PREDICT ADD FUNDS SELECTORS
247251
// ========================================

tests/framework/GestureStrategy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export interface UnifiedGestureOptions {
2222
speed?: 'fast' | 'slow';
2323
/** Swipe percentage (0–1) — Detox only; Appium ignores this */
2424
percentage?: number;
25+
/** Scroll direction — Detox only; used by scrollToElement */
26+
direction?: 'up' | 'down' | 'left' | 'right';
27+
/** Scroll amount in px — Detox only; used by scrollToElement */
28+
scrollAmount?: number;
2529
}
2630

2731
/**
@@ -220,6 +224,8 @@ export class DetoxGestureStrategy implements GestureStrategy {
220224
{
221225
timeout: opts?.timeout,
222226
elemDescription: opts?.description,
227+
direction: opts?.direction,
228+
scrollAmount: opts?.scrollAmount,
223229
},
224230
);
225231
}

tests/page-objects/Confirmation/TransactionPayConfirmation.ts

Lines changed: 239 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,260 @@ import {
22
ConfirmationRowComponentIDs,
33
TransactionPayComponentIDs,
44
} from '../../../app/components/Views/confirmations/ConfirmationView.testIds';
5-
import Matchers from '../../framework/Matchers';
6-
import { Assertions, Gestures } from '../../framework';
5+
import {
6+
Assertions,
7+
FrameworkDetector,
8+
Matchers,
9+
PlaywrightMatchers,
10+
UnifiedGestures,
11+
asDetoxElement,
12+
asPlaywrightElement,
13+
encapsulated,
14+
encapsulatedAction,
15+
type EncapsulatedElementType,
16+
} from '../../framework';
717

818
class TransactionPayConfirmation {
9-
get bridgeTime(): DetoxElement {
10-
return Matchers.getElementByID(ConfirmationRowComponentIDs.BRIDGE_TIME);
19+
get bridgeTime(): EncapsulatedElementType {
20+
return encapsulated({
21+
detox: () =>
22+
Matchers.getElementByID(ConfirmationRowComponentIDs.BRIDGE_TIME),
23+
appium: () =>
24+
PlaywrightMatchers.getElementById(
25+
ConfirmationRowComponentIDs.BRIDGE_TIME,
26+
{
27+
exact: true,
28+
},
29+
),
30+
});
1131
}
1232

13-
get payWithRow(): DetoxElement {
14-
return Matchers.getElementByID(ConfirmationRowComponentIDs.PAY_WITH);
33+
get payWithRow(): EncapsulatedElementType {
34+
return encapsulated({
35+
detox: () =>
36+
Matchers.getElementByID(ConfirmationRowComponentIDs.PAY_WITH),
37+
appium: () =>
38+
PlaywrightMatchers.getElementById(
39+
ConfirmationRowComponentIDs.PAY_WITH,
40+
{
41+
exact: true,
42+
},
43+
),
44+
});
1545
}
1646

17-
get payWithSymbol(): DetoxElement {
18-
return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_SYMBOL);
47+
get payWithSymbol(): EncapsulatedElementType {
48+
return encapsulated({
49+
detox: () =>
50+
Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_SYMBOL),
51+
appium: () =>
52+
PlaywrightMatchers.getElementById(
53+
TransactionPayComponentIDs.PAY_WITH_SYMBOL,
54+
{
55+
exact: true,
56+
},
57+
),
58+
});
1959
}
2060

21-
get payWithFiat(): DetoxElement {
22-
return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_FIAT);
61+
get payWithFiat(): EncapsulatedElementType {
62+
return encapsulated({
63+
detox: () =>
64+
Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_FIAT),
65+
appium: () =>
66+
PlaywrightMatchers.getElementById(
67+
TransactionPayComponentIDs.PAY_WITH_FIAT,
68+
{
69+
exact: true,
70+
},
71+
),
72+
});
2373
}
2474

25-
get payWithBalance(): DetoxElement {
26-
return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_BALANCE);
75+
get payWithBalance(): EncapsulatedElementType {
76+
return encapsulated({
77+
detox: () =>
78+
Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_BALANCE),
79+
appium: () =>
80+
PlaywrightMatchers.getElementById(
81+
TransactionPayComponentIDs.PAY_WITH_BALANCE,
82+
{
83+
exact: true,
84+
},
85+
),
86+
});
2787
}
2888

29-
get keyboardContinueButton(): DetoxElement {
30-
return Matchers.getElementByID(
31-
TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON,
32-
);
89+
get keyboardContinueButton(): EncapsulatedElementType {
90+
return encapsulated({
91+
detox: () =>
92+
Matchers.getElementByID(
93+
TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON,
94+
),
95+
appium: () =>
96+
PlaywrightMatchers.getElementById(
97+
TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON,
98+
{
99+
exact: true,
100+
},
101+
),
102+
});
33103
}
34104

35-
get total(): DetoxElement {
36-
return Matchers.getElementByID(ConfirmationRowComponentIDs.TOTAL);
105+
get amount(): EncapsulatedElementType {
106+
return encapsulated({
107+
detox: () => Matchers.getElementByID(ConfirmationRowComponentIDs.AMOUNT),
108+
appium: () =>
109+
PlaywrightMatchers.getElementById(ConfirmationRowComponentIDs.AMOUNT, {
110+
exact: true,
111+
}),
112+
});
37113
}
38114

39-
get transactionFee(): DetoxElement {
40-
return Matchers.getElementByID(ConfirmationRowComponentIDs.TRANSACTION_FEE);
115+
get total(): EncapsulatedElementType {
116+
return encapsulated({
117+
detox: () => Matchers.getElementByID(ConfirmationRowComponentIDs.TOTAL),
118+
appium: () =>
119+
PlaywrightMatchers.getElementById(ConfirmationRowComponentIDs.TOTAL, {
120+
exact: true,
121+
}),
122+
});
41123
}
42124

43-
async tapPayWithRow(): Promise<void> {
44-
await Gestures.waitAndTap(this.payWithRow, {
45-
elemDescription: 'Pay With Row',
125+
get transactionFee(): EncapsulatedElementType {
126+
return encapsulated({
127+
detox: () =>
128+
Matchers.getElementByID(ConfirmationRowComponentIDs.TRANSACTION_FEE),
129+
appium: () =>
130+
PlaywrightMatchers.getElementById(
131+
ConfirmationRowComponentIDs.TRANSACTION_FEE,
132+
{
133+
exact: true,
134+
},
135+
),
46136
});
47137
}
48138

49-
get tokenListScrollView(): Promise<Detox.NativeMatcher> {
139+
get payWithTokenList(): EncapsulatedElementType {
140+
return encapsulated({
141+
detox: () =>
142+
Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST),
143+
appium: () =>
144+
PlaywrightMatchers.getElementById(
145+
TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST,
146+
{
147+
exact: true,
148+
},
149+
),
150+
});
151+
}
152+
153+
get tokenListScrollViewIdentifier(): Promise<Detox.NativeMatcher> {
50154
return Matchers.getIdentifier(
51155
TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST,
52156
);
53157
}
54158

55-
async tapPayWithToken(tokenSymbol: string): Promise<void> {
56-
const tokenElement = Matchers.getElementByText(
57-
tokenSymbol,
58-
) as unknown as DetoxElement;
59-
await Gestures.scrollToElement(tokenElement, this.tokenListScrollView, {
60-
direction: 'down',
61-
scrollAmount: 200,
62-
elemDescription: `Pay With Token ${tokenSymbol}`,
159+
private getTokenOptionAt(
160+
tokenSymbol: string,
161+
index: number,
162+
): EncapsulatedElementType {
163+
return encapsulated({
164+
detox: () => Matchers.getElementByText(tokenSymbol, index),
165+
appium: async () => {
166+
const elements =
167+
await PlaywrightMatchers.getAllElementsByText(tokenSymbol);
168+
if (elements.length === 0) {
169+
throw new Error(
170+
`No pay with token option found for "${tokenSymbol}"`,
171+
);
172+
}
173+
if (index >= elements.length) {
174+
throw new Error(
175+
`Token index ${index} out of bounds (${elements.length} elements)`,
176+
);
177+
}
178+
return elements[index];
179+
},
180+
});
181+
}
182+
183+
private getKeypadButton(key: string): EncapsulatedElementType {
184+
return encapsulated({
185+
detox: () => Matchers.getElementByText(key),
186+
appium: () => PlaywrightMatchers.getElementByText(key),
63187
});
64-
await Gestures.waitAndTap(Matchers.getElementByText(tokenSymbol), {
65-
elemDescription: `Pay With Token ${tokenSymbol}`,
188+
}
189+
190+
private async expectText(
191+
elem: EncapsulatedElementType,
192+
text: string,
193+
description: string,
194+
): Promise<void> {
195+
await encapsulatedAction({
196+
detox: async () => {
197+
await Assertions.expectElementToHaveText(asDetoxElement(elem), text, {
198+
description,
199+
});
200+
},
201+
appium: async () => {
202+
const resolved = await asPlaywrightElement(elem);
203+
const actualText = (await resolved.textContent())
204+
.replace(/\s+/gu, ' ')
205+
.trim();
206+
if (!actualText.includes(text)) {
207+
throw new Error(
208+
`${description}: expected text containing "${text}" but got "${actualText}"`,
209+
);
210+
}
211+
},
66212
});
67213
}
68214

215+
async tapPayWithRow(): Promise<void> {
216+
await UnifiedGestures.waitAndTap(this.payWithRow, {
217+
description: 'Pay With Row',
218+
});
219+
}
220+
221+
async isAmountEntryVisible(): Promise<void> {
222+
await Assertions.expectElementToBeVisible(this.keyboardContinueButton, {
223+
description: 'Transaction pay keyboard continue button',
224+
timeout: 15000,
225+
});
226+
}
227+
228+
async isPayWithTokenListVisible(): Promise<void> {
229+
await Assertions.expectElementToBeVisible(this.payWithTokenList, {
230+
description: 'Pay with token list',
231+
timeout: 15000,
232+
});
233+
}
234+
235+
async tapPayWithToken(tokenSymbol: string, index = 0): Promise<void> {
236+
const tokenElement = this.getTokenOptionAt(tokenSymbol, index);
237+
const opts = { description: `Pay With Token ${tokenSymbol}` };
238+
239+
if (FrameworkDetector.isDetox()) {
240+
await UnifiedGestures.scrollToElement(
241+
tokenElement,
242+
this.tokenListScrollViewIdentifier,
243+
{ ...opts, direction: 'down', scrollAmount: 200 },
244+
);
245+
}
246+
await UnifiedGestures.waitAndTap(tokenElement, opts);
247+
}
248+
69249
async tapKeyboardContinueButton(): Promise<void> {
70-
await Gestures.waitAndTap(this.keyboardContinueButton, {
71-
elemDescription: 'Keyboard Continue Button',
250+
await UnifiedGestures.waitAndTap(this.keyboardContinueButton, {
251+
description: 'Keyboard Continue Button',
72252
});
73253
}
74254

75255
async tapKeyboardAmount(amount: string): Promise<void> {
76256
for (const char of amount) {
77-
await Gestures.waitAndTap(Matchers.getElementByText(char), {
78-
elemDescription: `Keyboard Key ${char}`,
257+
await UnifiedGestures.waitAndTap(this.getKeypadButton(char), {
258+
description: `Keyboard Key ${char}`,
79259
});
80260
}
81261
}
@@ -86,20 +266,33 @@ class TransactionPayConfirmation {
86266
}
87267

88268
async verifyBridgeTime(time: string): Promise<void> {
89-
await Assertions.expectElementToHaveText(this.bridgeTime, time, {
90-
description: 'Bridge time should be correct',
91-
});
269+
await this.expectText(
270+
this.bridgeTime,
271+
time,
272+
'Bridge time should be correct',
273+
);
274+
}
275+
276+
async verifyAmount(amount: string): Promise<void> {
277+
await this.expectText(this.amount, amount, 'Amount should be correct');
92278
}
93279

94280
async verifyTotal(total: string): Promise<void> {
95-
await Assertions.expectElementToHaveText(this.total, total, {
96-
description: 'Total should be correct',
97-
});
281+
await this.expectText(this.total, total, 'Total should be correct');
98282
}
99283

100284
async verifyTransactionFee(fee: string): Promise<void> {
101-
await Assertions.expectElementToHaveText(this.transactionFee, fee, {
102-
description: 'Transaction fee should be correct',
285+
await this.expectText(
286+
this.transactionFee,
287+
fee,
288+
'Transaction fee should be correct',
289+
);
290+
}
291+
292+
async verifyTransactionFeeVisible(): Promise<void> {
293+
await Assertions.expectElementToBeVisible(this.transactionFee, {
294+
description: 'Transaction fee row should be visible',
295+
timeout: 15000,
103296
});
104297
}
105298
}

0 commit comments

Comments
 (0)