From aa4338fc63d8161e79e11430f39b6a7c6298627b Mon Sep 17 00:00:00 2001 From: Curtis David Date: Wed, 18 Mar 2026 14:09:14 -0400 Subject: [PATCH 1/5] test: Migrate page objects to the new framework (perf/Predict) --- app/components/UI/Predict/Predict.testIds.ts | 4 + .../TransactionPayConfirmation.ts | 273 +++++++++++++--- .../Predict/PredictDetailsPage.ts | 294 ++++++++++++++---- .../page-objects/Predict/PredictMarketList.ts | 201 +++++++++--- tests/page-objects/wallet/TabBarComponent.ts | 25 +- 5 files changed, 657 insertions(+), 140 deletions(-) diff --git a/app/components/UI/Predict/Predict.testIds.ts b/app/components/UI/Predict/Predict.testIds.ts index 6d66ce1b27d3..3ae07dc98b4b 100644 --- a/app/components/UI/Predict/Predict.testIds.ts +++ b/app/components/UI/Predict/Predict.testIds.ts @@ -242,6 +242,10 @@ export const PredictBalanceSelectorsIDs = { BALANCE_CARD: 'predict-balance-card', } as const; +export const PredictBalanceSelectorsText = { + AVAILABLE_BALANCE: enContent.predict.available_balance, +} as const; + // ======================================== // PREDICT ADD FUNDS SELECTORS // ======================================== diff --git a/tests/page-objects/Confirmation/TransactionPayConfirmation.ts b/tests/page-objects/Confirmation/TransactionPayConfirmation.ts index 527f633b9d06..92df69b864f8 100644 --- a/tests/page-objects/Confirmation/TransactionPayConfirmation.ts +++ b/tests/page-objects/Confirmation/TransactionPayConfirmation.ts @@ -2,47 +2,189 @@ import { ConfirmationRowComponentIDs, TransactionPayComponentIDs, } from '../../../app/components/Views/confirmations/ConfirmationView.testIds'; -import Matchers from '../../framework/Matchers'; -import { Assertions, Gestures } from '../../framework'; +import { + Assertions, + Gestures, + Matchers, + PlaywrightMatchers, + UnifiedGestures, + asDetoxElement, + asPlaywrightElement, + encapsulated, + encapsulatedAction, + type EncapsulatedElementType, +} from '../../framework'; class TransactionPayConfirmation { - get bridgeTime(): DetoxElement { - return Matchers.getElementByID(ConfirmationRowComponentIDs.BRIDGE_TIME); + get bridgeTime(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(ConfirmationRowComponentIDs.BRIDGE_TIME), + appium: () => + PlaywrightMatchers.getElementById( + ConfirmationRowComponentIDs.BRIDGE_TIME, + { + exact: true, + }, + ), + }); } - get payWithRow(): DetoxElement { - return Matchers.getElementByID(ConfirmationRowComponentIDs.PAY_WITH); + get payWithRow(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(ConfirmationRowComponentIDs.PAY_WITH), + appium: () => + PlaywrightMatchers.getElementById( + ConfirmationRowComponentIDs.PAY_WITH, + { + exact: true, + }, + ), + }); } - get payWithSymbol(): DetoxElement { - return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_SYMBOL); + get payWithSymbol(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_SYMBOL), + appium: () => + PlaywrightMatchers.getElementById( + TransactionPayComponentIDs.PAY_WITH_SYMBOL, + { + exact: true, + }, + ), + }); } - get payWithFiat(): DetoxElement { - return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_FIAT); + get payWithFiat(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_FIAT), + appium: () => + PlaywrightMatchers.getElementById( + TransactionPayComponentIDs.PAY_WITH_FIAT, + { + exact: true, + }, + ), + }); } - get payWithBalance(): DetoxElement { - return Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_BALANCE); + get payWithBalance(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_BALANCE), + appium: () => + PlaywrightMatchers.getElementById( + TransactionPayComponentIDs.PAY_WITH_BALANCE, + { + exact: true, + }, + ), + }); } - get keyboardContinueButton(): DetoxElement { - return Matchers.getElementByID( - TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON, - ); + get keyboardContinueButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID( + TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON, + ), + appium: () => + PlaywrightMatchers.getElementById( + TransactionPayComponentIDs.KEYBOARD_CONTINUE_BUTTON, + { + exact: true, + }, + ), + }); + } + + get amount(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByID(ConfirmationRowComponentIDs.AMOUNT), + appium: () => + PlaywrightMatchers.getElementById(ConfirmationRowComponentIDs.AMOUNT, { + exact: true, + }), + }); } - get total(): DetoxElement { - return Matchers.getElementByID(ConfirmationRowComponentIDs.TOTAL); + get total(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByID(ConfirmationRowComponentIDs.TOTAL), + appium: () => + PlaywrightMatchers.getElementById(ConfirmationRowComponentIDs.TOTAL, { + exact: true, + }), + }); } - get transactionFee(): DetoxElement { - return Matchers.getElementByID(ConfirmationRowComponentIDs.TRANSACTION_FEE); + get transactionFee(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(ConfirmationRowComponentIDs.TRANSACTION_FEE), + appium: () => + PlaywrightMatchers.getElementById( + ConfirmationRowComponentIDs.TRANSACTION_FEE, + { + exact: true, + }, + ), + }); + } + + get payWithTokenList(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST), + appium: () => + PlaywrightMatchers.getElementById( + TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST, + { + exact: true, + }, + ), + }); + } + + private getKeypadButton(key: string): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText(key), + appium: () => PlaywrightMatchers.getElementByText(key), + }); + } + + private async expectText( + elem: EncapsulatedElementType, + text: string, + description: string, + ): Promise { + await encapsulatedAction({ + detox: async () => { + await Assertions.expectElementToHaveText(asDetoxElement(elem), text, { + description, + }); + }, + appium: async () => { + const resolved = await asPlaywrightElement(elem); + const actualText = (await resolved.textContent()) + .replace(/\s+/gu, ' ') + .trim(); + if (!actualText.includes(text)) { + throw new Error( + `${description}: expected text containing "${text}" but got "${actualText}"`, + ); + } + }, + }); } async tapPayWithRow(): Promise { - await Gestures.waitAndTap(this.payWithRow, { - elemDescription: 'Pay With Row', + await UnifiedGestures.waitAndTap(this.payWithRow, { + description: 'Pay With Row', }); } @@ -52,30 +194,60 @@ class TransactionPayConfirmation { ); } - async tapPayWithToken(tokenSymbol: string): Promise { - const tokenElement = Matchers.getElementByText( - tokenSymbol, - ) as unknown as DetoxElement; - await Gestures.scrollToElement(tokenElement, this.tokenListScrollView, { - direction: 'down', - scrollAmount: 200, - elemDescription: `Pay With Token ${tokenSymbol}`, + async isAmountEntryVisible(): Promise { + await Assertions.expectElementToBeVisible(this.keyboardContinueButton, { + description: 'Transaction pay keyboard continue button', + timeout: 15000, }); - await Gestures.waitAndTap(Matchers.getElementByText(tokenSymbol), { - elemDescription: `Pay With Token ${tokenSymbol}`, + } + + async isPayWithTokenListVisible(): Promise { + await Assertions.expectElementToBeVisible(this.payWithTokenList, { + description: 'Pay with token list', + timeout: 15000, + }); + } + + async tapPayWithToken(tokenSymbol: string, index = 0): Promise { + await encapsulatedAction({ + detox: async () => { + const tokenElement = Matchers.getElementByText( + tokenSymbol, + ) as unknown as DetoxElement; + await Gestures.scrollToElement(tokenElement, this.tokenListScrollView, { + direction: 'down', + scrollAmount: 200, + elemDescription: `Pay With Token ${tokenSymbol}`, + }); + await Gestures.waitAndTap(Matchers.getElementByText(tokenSymbol), { + elemDescription: `Pay With Token ${tokenSymbol}`, + }); + }, + appium: async () => { + const tokenElements = + await PlaywrightMatchers.getAllElementsByText(tokenSymbol); + if (tokenElements.length === 0) { + throw new Error( + `No pay with token option found for "${tokenSymbol}"`, + ); + } + await UnifiedGestures.tapAtIndex(tokenElements, index, { + description: `Pay With Token ${tokenSymbol}`, + }); + }, }); } async tapKeyboardContinueButton(): Promise { - await Gestures.waitAndTap(this.keyboardContinueButton, { - elemDescription: 'Keyboard Continue Button', + await UnifiedGestures.waitAndTap(this.keyboardContinueButton, { + description: 'Keyboard Continue Button', }); } async tapKeyboardAmount(amount: string): Promise { for (const char of amount) { - await Gestures.waitAndTap(Matchers.getElementByText(char), { - elemDescription: `Keyboard Key ${char}`, + await UnifiedGestures.waitAndTap(this.getKeypadButton(char), { + description: `Keyboard Key ${char}`, }); } } @@ -86,20 +258,33 @@ class TransactionPayConfirmation { } async verifyBridgeTime(time: string): Promise { - await Assertions.expectElementToHaveText(this.bridgeTime, time, { - description: 'Bridge time should be correct', - }); + await this.expectText( + this.bridgeTime, + time, + 'Bridge time should be correct', + ); + } + + async verifyAmount(amount: string): Promise { + await this.expectText(this.amount, amount, 'Amount should be correct'); } async verifyTotal(total: string): Promise { - await Assertions.expectElementToHaveText(this.total, total, { - description: 'Total should be correct', - }); + await this.expectText(this.total, total, 'Total should be correct'); } async verifyTransactionFee(fee: string): Promise { - await Assertions.expectElementToHaveText(this.transactionFee, fee, { - description: 'Transaction fee should be correct', + await this.expectText( + this.transactionFee, + fee, + 'Transaction fee should be correct', + ); + } + + async verifyTransactionFeeVisible(): Promise { + await Assertions.expectElementToBeVisible(this.transactionFee, { + description: 'Transaction fee row should be visible', + timeout: 15000, }); } } diff --git a/tests/page-objects/Predict/PredictDetailsPage.ts b/tests/page-objects/Predict/PredictDetailsPage.ts index 8d938c4d2f2d..cee38d84b685 100644 --- a/tests/page-objects/Predict/PredictDetailsPage.ts +++ b/tests/page-objects/Predict/PredictDetailsPage.ts @@ -1,4 +1,13 @@ -import { Matchers, Gestures } from '../../framework'; +import { + Assertions, + Gestures, + Matchers, + PlaywrightMatchers, + UnifiedGestures, + encapsulated, + encapsulatedAction, + type EncapsulatedElementType, +} from '../../framework'; import { PredictBalanceSelectorsIDs, PredictBuyPreviewSelectorsIDs, @@ -6,71 +15,193 @@ import { PredictMarketDetailsSelectorsText, } from '../../../app/components/UI/Predict/Predict.testIds'; class PredictDetailsPage { - get container(): DetoxElement { - return Matchers.getElementByID(PredictMarketDetailsSelectorsIDs.SCREEN); + get container(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictMarketDetailsSelectorsIDs.SCREEN), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.SCREEN, + { exact: true }, + ), + }); + } + + get positionsTab(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByText( + PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT, + ), + appium: () => + PlaywrightMatchers.getElementByText( + PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT, + ), + }); + } + + get aboutTab(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByText( + PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT, + ), + appium: () => + PlaywrightMatchers.getElementByText( + PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT, + ), + }); + } + + get outcomesTab(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByText( + PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT, + ), + appium: () => + PlaywrightMatchers.getElementByText( + PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT, + ), + }); } - get positionsTab(): DetoxElement { - return Matchers.getElementByText( - PredictMarketDetailsSelectorsText.POSITIONS_TAB_TEXT, - ); + + get aboutTabContent(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID( + PredictMarketDetailsSelectorsIDs.ABOUT_TAB_CONTENT, + ), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.ABOUT_TAB_CONTENT, + { exact: true }, + ), + }); } - get aboutTab(): DetoxElement { - return Matchers.getElementByText( - PredictMarketDetailsSelectorsText.ABOUT_TAB_TEXT, - ); + + get outcomesTabContent(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID( + PredictMarketDetailsSelectorsIDs.OUTCOMES_TAB_CONTENT, + ), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.OUTCOMES_TAB_CONTENT, + { exact: true }, + ), + }); } - get outcomesTab(): DetoxElement { - return Matchers.getElementByText( - PredictMarketDetailsSelectorsText.OUTCOMES_TAB_TEXT, - ); + + get cashOutButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID( + PredictMarketDetailsSelectorsIDs.MARKET_DETAILS_CASH_OUT_BUTTON, + ), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.MARKET_DETAILS_CASH_OUT_BUTTON, + { exact: true }, + ), + }); + } + + get claimButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID( + PredictMarketDetailsSelectorsIDs.CLAIM_WINNINGS_BUTTON, + ), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.CLAIM_WINNINGS_BUTTON, + { exact: true }, + ), + }); + } + + get backButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByLabel('Back') as unknown as DetoxElement, + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketDetailsSelectorsIDs.BACK_BUTTON, + { exact: true }, + ), + }); } - get cashOutButton(): DetoxElement { - return Matchers.getElementByID( - PredictMarketDetailsSelectorsIDs.MARKET_DETAILS_CASH_OUT_BUTTON, - ); + + get balanceCard(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictBalanceSelectorsIDs.BALANCE_CARD), + appium: () => + PlaywrightMatchers.getElementById( + PredictBalanceSelectorsIDs.BALANCE_CARD, + { + exact: true, + }, + ), + }); } - get claimButton(): DetoxElement { - return Matchers.getElementByID( - PredictMarketDetailsSelectorsIDs.CLAIM_WINNINGS_BUTTON, - ); + + get placeBetButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictBuyPreviewSelectorsIDs.PLACE_BET_BUTTON), + appium: () => + PlaywrightMatchers.getElementById( + PredictBuyPreviewSelectorsIDs.PLACE_BET_BUTTON, + { exact: true }, + ), + }); } - get backButton(): DetoxElement { - return Matchers.getElementByLabel('Back') as unknown as DetoxElement; + + get volumeLabel(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText('Volume'), + appium: () => PlaywrightMatchers.getElementByText('Volume'), + }); } - get balanceCard(): DetoxElement { - return Matchers.getElementByID(PredictBalanceSelectorsIDs.BALANCE_CARD); + + async waitForScreenToDisplay(): Promise { + await Assertions.expectElementToBeVisible(this.container, { + description: 'Predict market details screen', + timeout: 15000, + }); } - get placeBetButton(): DetoxElement { - return Matchers.getElementByID( - PredictBuyPreviewSelectorsIDs.PLACE_BET_BUTTON, - ); + async isVisible(): Promise { + await this.waitForScreenToDisplay(); } async tapBackButton(): Promise { - await Gestures.waitAndTap(this.backButton, { - elemDescription: 'Back button', + await UnifiedGestures.waitAndTap(this.backButton, { + description: 'Back button', }); } async tapPositionsTab(): Promise { - await Gestures.waitAndTap(this.positionsTab, { - elemDescription: 'Positions tab', + await UnifiedGestures.waitAndTap(this.positionsTab, { + description: 'Positions tab', }); } async tapAboutTab(): Promise { - await Gestures.waitAndTap(this.aboutTab, { - elemDescription: 'About tab', + await UnifiedGestures.waitAndTap(this.aboutTab, { + description: 'About tab', }); } async tapOutcomesTab(): Promise { - await Gestures.waitAndTap(this.outcomesTab, { - elemDescription: 'Outcomes tab', + await UnifiedGestures.waitAndTap(this.outcomesTab, { + description: 'Outcomes tab', }); } async tapCashOutButton(): Promise { - await Gestures.waitAndTap(this.cashOutButton, { - elemDescription: 'Cash out button', + await UnifiedGestures.waitAndTap(this.cashOutButton, { + description: 'Cash out button', }); } @@ -80,8 +211,8 @@ class PredictDetailsPage { /Celtics[\s•\n]*83¢/, )) as unknown as DetoxElement; - await Gestures.waitAndTap(celticsButton, { - elemDescription: 'Celtics outcome button', + await UnifiedGestures.waitAndTap(celticsButton, { + description: 'Celtics outcome button', }); } @@ -92,8 +223,8 @@ class PredictDetailsPage { const digitElement = (await Matchers.getElementByText( digit, )) as unknown as DetoxElement; - await Gestures.waitAndTap(digitElement, { - elemDescription: `tap ${digit} on keypad`, + await UnifiedGestures.waitAndTap(digitElement, { + description: `Tap ${digit} on keypad`, }); } } @@ -103,8 +234,8 @@ class PredictDetailsPage { 'Done', )) as unknown as DetoxElement; - await Gestures.waitAndTap(continueButton, { - elemDescription: 'Done button', + await UnifiedGestures.waitAndTap(continueButton, { + description: 'Done button', }); } @@ -113,26 +244,75 @@ class PredictDetailsPage { 'Continue', )) as unknown as DetoxElement; - await Gestures.waitAndTap(continueButton, { - elemDescription: 'Continue button', + await UnifiedGestures.waitAndTap(continueButton, { + description: 'Continue button', }); } async tapOpenPosition(): Promise { - await Gestures.waitAndTap(this.placeBetButton, { - elemDescription: 'Place bet button', - delay: 1000, // this ensures the positions values are stabilized + await encapsulatedAction({ + detox: async () => { + await Gestures.waitAndTap(this.placeBetButton as DetoxElement, { + elemDescription: 'Place bet button', + delay: 1000, + }); + }, + appium: async () => { + await UnifiedGestures.waitAndTap(this.placeBetButton, { + description: 'Place bet button', + }); + }, }); } async tapClaimWinningsButton(): Promise { - // Claim button is animated - use delay instead of checkStability - // checkStability would timeout if animation is continuous - await Gestures.waitAndTap(this.claimButton, { - elemDescription: 'Tap claim winnings button on market details page', - delay: 3000, + await encapsulatedAction({ + detox: async () => { + await Gestures.waitAndTap(this.claimButton as DetoxElement, { + elemDescription: 'Tap claim winnings button on market details page', + delay: 3000, + }); + }, + appium: async () => { + await UnifiedGestures.waitAndTap(this.claimButton, { + description: 'Tap claim winnings button on market details page', + }); + }, + }); + } + + async isAboutTabContentDisplayed(): Promise { + await Assertions.expectElementToBeVisible(this.aboutTabContent, { + description: 'About tab content', + timeout: 15000, + }); + } + + async isOutcomesTabContentDisplayed(): Promise { + await Assertions.expectElementToBeVisible(this.outcomesTabContent, { + description: 'Outcomes tab content', + timeout: 15000, }); } + + async verifyVolumeTextDisplayed(): Promise { + await Assertions.expectElementToBeVisible(this.volumeLabel, { + description: 'Volume label', + timeout: 15000, + }); + } + + async hasOutcomesTab(): Promise { + try { + await Assertions.expectElementToBeVisible(this.outcomesTab, { + description: 'Outcomes tab', + timeout: 2000, + }); + return true; + } catch { + return false; + } + } } export default new PredictDetailsPage(); diff --git a/tests/page-objects/Predict/PredictMarketList.ts b/tests/page-objects/Predict/PredictMarketList.ts index 80fcd1644131..f2d78db65188 100644 --- a/tests/page-objects/Predict/PredictMarketList.ts +++ b/tests/page-objects/Predict/PredictMarketList.ts @@ -1,63 +1,168 @@ -import { Gestures, Matchers } from '../../framework'; import { + Assertions, + Matchers, + PlaywrightMatchers, + UnifiedGestures, + encapsulated, + type EncapsulatedElementType, +} from '../../framework'; +import { + PredictBalanceSelectorsIDs, + PredictBalanceSelectorsText, PredictMarketListSelectorsIDs, getPredictMarketListSelector, } from '../../../app/components/UI/Predict/Predict.testIds'; -// Type for category tabs type CategoryTab = 'trending' | 'new' | 'sports' | 'crypto' | 'politics'; +const CATEGORY_LABELS: Record = { + trending: 'Trending', + new: 'New', + sports: 'Sports', + crypto: 'Crypto', + politics: 'Politics', +}; + class PredictMarketList { - get container(): DetoxElement { - return Matchers.getElementByID(PredictMarketListSelectorsIDs.CONTAINER); + get container(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictMarketListSelectorsIDs.CONTAINER), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketListSelectorsIDs.CONTAINER, + { exact: true }, + ), + }); + } + + get errorContainer(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictMarketListSelectorsIDs.EMPTY_STATE), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketListSelectorsIDs.EMPTY_STATE, + { exact: true }, + ), + }); + } + + get categoryTabs(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictMarketListSelectorsIDs.CATEGORY_TABS), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketListSelectorsIDs.CATEGORY_TABS, + { exact: true }, + ), + }); + } + + get backButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictMarketListSelectorsIDs.BACK_BUTTON), + appium: () => + PlaywrightMatchers.getElementById( + PredictMarketListSelectorsIDs.BACK_BUTTON, + { exact: true }, + ), + }); } - get errorContainer(): DetoxElement { - return Matchers.getElementByID(PredictMarketListSelectorsIDs.EMPTY_STATE); + get addFundsButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText('Add funds'), + appium: () => PlaywrightMatchers.getElementByText('Add funds'), + }); } - get categoryTabs(): DetoxElement { - return Matchers.getElementByID(PredictMarketListSelectorsIDs.CATEGORY_TABS); + get balanceCard(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByID(PredictBalanceSelectorsIDs.BALANCE_CARD), + appium: () => + PlaywrightMatchers.getElementById( + PredictBalanceSelectorsIDs.BALANCE_CARD, + { + exact: true, + }, + ), + }); } - get backButton(): DetoxElement { - return Matchers.getElementByID(PredictMarketListSelectorsIDs.BACK_BUTTON); + + get availableBalanceLabel(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByText( + PredictBalanceSelectorsText.AVAILABLE_BALANCE, + ), + appium: () => + PlaywrightMatchers.getElementByText( + PredictBalanceSelectorsText.AVAILABLE_BALANCE, + ), + }); } - getMarketCard(category: CategoryTab, cardIndex: number): DetoxElement { - return Matchers.getElementByID( - getPredictMarketListSelector.marketCardByCategory(category, cardIndex), + getMarketCard( + category: CategoryTab, + cardIndex: number, + ): EncapsulatedElementType { + const marketCardId = getPredictMarketListSelector.marketCardByCategory( + category, + cardIndex, ); + + return encapsulated({ + detox: () => Matchers.getElementByID(marketCardId), + appium: () => + PlaywrightMatchers.getElementById(marketCardId, { exact: true }), + }); } - getPositionItem(positionId: string): DetoxElement { - return Matchers.getElementByID(`position-${positionId}`); + getPositionItem(positionId: string): EncapsulatedElementType { + const selector = `position-${positionId}`; + return encapsulated({ + detox: () => Matchers.getElementByID(selector), + appium: () => + PlaywrightMatchers.getElementById(selector, { exact: true }), + }); + } + + private getCategoryTab(category: CategoryTab): EncapsulatedElementType { + const label = CATEGORY_LABELS[category]; + + return encapsulated({ + detox: () => Matchers.getElementByText(label), + appium: () => PlaywrightMatchers.getElementByText(label), + }); + } + + async waitForScreenToDisplay(): Promise { + await Assertions.expectElementToBeVisible(this.container, { + description: 'Predict market list container', + timeout: 15000, + }); + } + + async isContainerDisplayed(): Promise { + await this.waitForScreenToDisplay(); } - // Actions async tapMarketCard( category: CategoryTab = 'trending', cardIndex: number = 1, ): Promise { - const marketCard = this.getMarketCard(category, cardIndex); - await Gestures.waitAndTap(marketCard, { - elemDescription: `Tapping Predict Market Card ${cardIndex} in ${category} category`, + await UnifiedGestures.waitAndTap(this.getMarketCard(category, cardIndex), { + description: `Predict market card ${cardIndex} in ${category} category`, }); } async tapCategoryTab(category: CategoryTab): Promise { - const categoryLabels = { - trending: 'Trending', - new: 'New', - sports: 'Sports', - crypto: 'Crypto', - politics: 'Politics', - }; - - const tabElement = (await Matchers.getElementByText( - categoryLabels[category], - )) as unknown as DetoxElement; - await Gestures.waitAndTap(tabElement, { - elemDescription: `Tapping ${category} category tab`, + await UnifiedGestures.waitAndTap(this.getCategoryTab(category), { + description: `${category} category tab`, }); } @@ -74,8 +179,8 @@ class PredictMarketList { by.text('Yes').withAncestor(by.id(parentId)), ) as unknown as DetoxElement; - await Gestures.waitAndTap(yesByTextWithAncestor, { - elemDescription: `Tap Yes in ${category} feed index ${cardIndex}`, + await UnifiedGestures.waitAndTap(yesByTextWithAncestor, { + description: `Yes option in ${category} feed index ${cardIndex}`, }); } @@ -92,14 +197,34 @@ class PredictMarketList { by.text('No').withAncestor(by.id(parentId)), ) as unknown as DetoxElement; - await Gestures.waitAndTap(noByTextWithAncestor, { - elemDescription: `Tap No in ${category} feed index ${cardIndex}`, + await UnifiedGestures.waitAndTap(noByTextWithAncestor, { + description: `No option in ${category} feed index ${cardIndex}`, + }); + } + + async tapAddFundsButton(): Promise { + await UnifiedGestures.waitAndTap(this.addFundsButton, { + description: 'Predict add funds button', + }); + } + + async isBalanceCardDisplayed(): Promise { + await Assertions.expectElementToBeVisible(this.balanceCard, { + description: 'Predict balance card', + timeout: 15000, + }); + } + + async isAvailableBalanceDisplayed(): Promise { + await Assertions.expectElementToBeVisible(this.availableBalanceLabel, { + description: 'Available balance label', + timeout: 15000, }); } async tapBackButton(): Promise { - await Gestures.waitAndTap(this.backButton, { - elemDescription: 'Tap Back button on market feed', + await UnifiedGestures.waitAndTap(this.backButton, { + description: 'Back button on predict market list', }); } } diff --git a/tests/page-objects/wallet/TabBarComponent.ts b/tests/page-objects/wallet/TabBarComponent.ts index 5de83f198fb4..ddaad718a30e 100644 --- a/tests/page-objects/wallet/TabBarComponent.ts +++ b/tests/page-objects/wallet/TabBarComponent.ts @@ -30,6 +30,22 @@ class TabBarComponent { }); } + get tabBarBrowserButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByID(TabBarSelectorIDs.BROWSER), + appium: { + android: () => + PlaywrightMatchers.getElementById(TabBarSelectorIDs.BROWSER, { + exact: true, + }), + ios: () => + PlaywrightMatchers.getElementByAccessibilityId( + TabBarSelectorIDs.BROWSER, + ), + }, + }); + } + get tabBarWalletButton(): EncapsulatedElementType { return encapsulated({ detox: () => Matchers.getElementByID(TabBarSelectorIDs.WALLET), @@ -137,8 +153,9 @@ class TabBarComponent { await UnifiedGestures.waitAndTap(this.tabBarWalletButton, { timeout: 2000, }); - await Assertions.expectElementToBeVisible(WalletView.container, { + await Assertions.expectElementToBeVisible(WalletView.totalBalance, { timeout: 500, + description: 'Wallet total balance should be visible', }); }, { @@ -150,6 +167,12 @@ class TabBarComponent { ); } + async tapBrowser(): Promise { + await UnifiedGestures.waitAndTap(this.tabBarBrowserButton, { + description: 'Tab Bar - Browser Button', + }); + } + async tapActions(): Promise { await UnifiedGestures.waitAndTap(this.tabBarActionButton, { description: 'Tab Bar - Trade Button', From f40d342fef29141c0f56a3ebb7aba46900e4df8f Mon Sep 17 00:00:00 2001 From: Curtis David Date: Wed, 18 Mar 2026 16:43:05 -0400 Subject: [PATCH 2/5] fix cursor bot --- .../Predict/PredictDetailsPage.ts | 58 +++++++++----- .../page-objects/Predict/PredictMarketList.ts | 76 ++++++++----------- 2 files changed, 68 insertions(+), 66 deletions(-) diff --git a/tests/page-objects/Predict/PredictDetailsPage.ts b/tests/page-objects/Predict/PredictDetailsPage.ts index cee38d84b685..690a13e6fc22 100644 --- a/tests/page-objects/Predict/PredictDetailsPage.ts +++ b/tests/page-objects/Predict/PredictDetailsPage.ts @@ -167,6 +167,40 @@ class PredictDetailsPage { }); } + private getOpenPositionValueButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => + Matchers.getElementByText( + /Celtics[\s•\n]*83¢/, + ) as unknown as DetoxElement, + appium: () => + PlaywrightMatchers.getElementByXPath( + `//*[ (contains(@text,'Celtics') and contains(@text,'83¢')) or (contains(@label,'Celtics') and contains(@label,'83¢')) or (contains(@name,'Celtics') and contains(@name,'83¢')) ]`, + ), + }); + } + + private getKeypadDigitButton(digit: string): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText(digit), + appium: () => PlaywrightMatchers.getElementByText(digit), + }); + } + + private getDoneButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText('Done'), + appium: () => PlaywrightMatchers.getElementByText('Done'), + }); + } + + private getContinueButton(): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText('Continue'), + appium: () => PlaywrightMatchers.getElementByText('Continue'), + }); + } + async waitForScreenToDisplay(): Promise { await Assertions.expectElementToBeVisible(this.container, { description: 'Predict market details screen', @@ -206,12 +240,7 @@ class PredictDetailsPage { } async tapOpenPositionValue(): Promise { - // Use regex to match both "Celtics\n83¢" and "Celtics • 83¢" formats - const celticsButton = (await Matchers.getElementByText( - /Celtics[\s•\n]*83¢/, - )) as unknown as DetoxElement; - - await UnifiedGestures.waitAndTap(celticsButton, { + await UnifiedGestures.waitAndTap(this.getOpenPositionValueButton(), { description: 'Celtics outcome button', }); } @@ -220,31 +249,20 @@ class PredictDetailsPage { const digits = amount.split(''); for (const digit of digits) { - const digitElement = (await Matchers.getElementByText( - digit, - )) as unknown as DetoxElement; - await UnifiedGestures.waitAndTap(digitElement, { + await UnifiedGestures.waitAndTap(this.getKeypadDigitButton(digit), { description: `Tap ${digit} on keypad`, }); } } async tapDoneButton(): Promise { - const continueButton = (await Matchers.getElementByText( - 'Done', - )) as unknown as DetoxElement; - - await UnifiedGestures.waitAndTap(continueButton, { + await UnifiedGestures.waitAndTap(this.getDoneButton(), { description: 'Done button', }); } async tapContinueButton(): Promise { - const continueButton = (await Matchers.getElementByText( - 'Continue', - )) as unknown as DetoxElement; - - await UnifiedGestures.waitAndTap(continueButton, { + await UnifiedGestures.waitAndTap(this.getContinueButton(), { description: 'Continue button', }); } diff --git a/tests/page-objects/Predict/PredictMarketList.ts b/tests/page-objects/Predict/PredictMarketList.ts index f2d78db65188..e1d1cfee6c81 100644 --- a/tests/page-objects/Predict/PredictMarketList.ts +++ b/tests/page-objects/Predict/PredictMarketList.ts @@ -1,5 +1,4 @@ import { - Assertions, Matchers, PlaywrightMatchers, UnifiedGestures, @@ -131,7 +130,7 @@ class PredictMarketList { }); } - private getCategoryTab(category: CategoryTab): EncapsulatedElementType { + getCategoryTab(category: CategoryTab): EncapsulatedElementType { const label = CATEGORY_LABELS[category]; return encapsulated({ @@ -140,15 +139,26 @@ class PredictMarketList { }); } - async waitForScreenToDisplay(): Promise { - await Assertions.expectElementToBeVisible(this.container, { - description: 'Predict market list container', - timeout: 15000, - }); - } + getMarketOutcomeButton( + category: CategoryTab, + cardIndex: number, + outcome: 'Yes' | 'No', + ): EncapsulatedElementType { + const parentId = getPredictMarketListSelector.marketCardByCategory( + category, + cardIndex, + ); - async isContainerDisplayed(): Promise { - await this.waitForScreenToDisplay(); + return encapsulated({ + detox: () => + element( + by.text(outcome).withAncestor(by.id(parentId)), + ) as unknown as DetoxElement, + appium: () => + PlaywrightMatchers.getElementByXPath( + `//*[contains(@resource-id,'${parentId}') or contains(@name,'${parentId}')]//*[(@text='${outcome}' or @content-desc='${outcome}' or @label='${outcome}' or @name='${outcome}')]`, + ), + }); } async tapMarketCard( @@ -170,36 +180,24 @@ class PredictMarketList { category: CategoryTab = 'new', cardIndex: number = 1, ): Promise { - const parentId = getPredictMarketListSelector.marketCardByCategory( - category, - cardIndex, + await UnifiedGestures.waitAndTap( + this.getMarketOutcomeButton(category, cardIndex, 'Yes'), + { + description: `Yes option in ${category} feed index ${cardIndex}`, + }, ); - - const yesByTextWithAncestor = element( - by.text('Yes').withAncestor(by.id(parentId)), - ) as unknown as DetoxElement; - - await UnifiedGestures.waitAndTap(yesByTextWithAncestor, { - description: `Yes option in ${category} feed index ${cardIndex}`, - }); } async tapNoBasedOnCategoryAndIndex( category: CategoryTab = 'new', cardIndex: number = 1, ): Promise { - const parentId = getPredictMarketListSelector.marketCardByCategory( - category, - cardIndex, + await UnifiedGestures.waitAndTap( + this.getMarketOutcomeButton(category, cardIndex, 'No'), + { + description: `No option in ${category} feed index ${cardIndex}`, + }, ); - - const noByTextWithAncestor = element( - by.text('No').withAncestor(by.id(parentId)), - ) as unknown as DetoxElement; - - await UnifiedGestures.waitAndTap(noByTextWithAncestor, { - description: `No option in ${category} feed index ${cardIndex}`, - }); } async tapAddFundsButton(): Promise { @@ -208,20 +206,6 @@ class PredictMarketList { }); } - async isBalanceCardDisplayed(): Promise { - await Assertions.expectElementToBeVisible(this.balanceCard, { - description: 'Predict balance card', - timeout: 15000, - }); - } - - async isAvailableBalanceDisplayed(): Promise { - await Assertions.expectElementToBeVisible(this.availableBalanceLabel, { - description: 'Available balance label', - timeout: 15000, - }); - } - async tapBackButton(): Promise { await UnifiedGestures.waitAndTap(this.backButton, { description: 'Back button on predict market list', From 8079da9662a6e6444aba4fccda027c33796d46c8 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Wed, 18 Mar 2026 17:59:03 -0400 Subject: [PATCH 3/5] remove unnecessary assertion --- tests/page-objects/wallet/TabBarComponent.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/page-objects/wallet/TabBarComponent.ts b/tests/page-objects/wallet/TabBarComponent.ts index ddaad718a30e..bea0b3df5072 100644 --- a/tests/page-objects/wallet/TabBarComponent.ts +++ b/tests/page-objects/wallet/TabBarComponent.ts @@ -153,10 +153,6 @@ class TabBarComponent { await UnifiedGestures.waitAndTap(this.tabBarWalletButton, { timeout: 2000, }); - await Assertions.expectElementToBeVisible(WalletView.totalBalance, { - timeout: 500, - description: 'Wallet total balance should be visible', - }); }, { // Each attempt: ~2.5s (2s tap + 0.5s assertion). 15 retries ≈ ~37s total budget. From cd679d4ef0b07f87e15121454008c9c102ee41a3 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Wed, 18 Mar 2026 18:52:12 -0400 Subject: [PATCH 4/5] add back proper assertion --- tests/page-objects/wallet/TabBarComponent.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/page-objects/wallet/TabBarComponent.ts b/tests/page-objects/wallet/TabBarComponent.ts index bea0b3df5072..ed431297d6e2 100644 --- a/tests/page-objects/wallet/TabBarComponent.ts +++ b/tests/page-objects/wallet/TabBarComponent.ts @@ -153,6 +153,9 @@ class TabBarComponent { await UnifiedGestures.waitAndTap(this.tabBarWalletButton, { timeout: 2000, }); + await Assertions.expectElementToBeVisible(WalletView.container, { + timeout: 500, + }); }, { // Each attempt: ~2.5s (2s tap + 0.5s assertion). 15 retries ≈ ~37s total budget. From 0415ddacf4cf0d891dc9aaaee811c1ce6ad5df67 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Thu, 19 Mar 2026 11:30:43 -0400 Subject: [PATCH 5/5] clean up code to be more structured --- tests/framework/GestureStrategy.ts | 6 ++ .../TransactionPayConfirmation.ts | 76 ++++++++++--------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/tests/framework/GestureStrategy.ts b/tests/framework/GestureStrategy.ts index 8a4f062ebcb5..891ca19cd651 100644 --- a/tests/framework/GestureStrategy.ts +++ b/tests/framework/GestureStrategy.ts @@ -22,6 +22,10 @@ export interface UnifiedGestureOptions { speed?: 'fast' | 'slow'; /** Swipe percentage (0–1) — Detox only; Appium ignores this */ percentage?: number; + /** Scroll direction — Detox only; used by scrollToElement */ + direction?: 'up' | 'down' | 'left' | 'right'; + /** Scroll amount in px — Detox only; used by scrollToElement */ + scrollAmount?: number; } /** @@ -220,6 +224,8 @@ export class DetoxGestureStrategy implements GestureStrategy { { timeout: opts?.timeout, elemDescription: opts?.description, + direction: opts?.direction, + scrollAmount: opts?.scrollAmount, }, ); } diff --git a/tests/page-objects/Confirmation/TransactionPayConfirmation.ts b/tests/page-objects/Confirmation/TransactionPayConfirmation.ts index 92df69b864f8..a2dac2ce323a 100644 --- a/tests/page-objects/Confirmation/TransactionPayConfirmation.ts +++ b/tests/page-objects/Confirmation/TransactionPayConfirmation.ts @@ -4,7 +4,7 @@ import { } from '../../../app/components/Views/confirmations/ConfirmationView.testIds'; import { Assertions, - Gestures, + FrameworkDetector, Matchers, PlaywrightMatchers, UnifiedGestures, @@ -150,6 +150,36 @@ class TransactionPayConfirmation { }); } + get tokenListScrollViewIdentifier(): Promise { + return Matchers.getIdentifier( + TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST, + ); + } + + private getTokenOptionAt( + tokenSymbol: string, + index: number, + ): EncapsulatedElementType { + return encapsulated({ + detox: () => Matchers.getElementByText(tokenSymbol, index), + appium: async () => { + const elements = + await PlaywrightMatchers.getAllElementsByText(tokenSymbol); + if (elements.length === 0) { + throw new Error( + `No pay with token option found for "${tokenSymbol}"`, + ); + } + if (index >= elements.length) { + throw new Error( + `Token index ${index} out of bounds (${elements.length} elements)`, + ); + } + return elements[index]; + }, + }); + } + private getKeypadButton(key: string): EncapsulatedElementType { return encapsulated({ detox: () => Matchers.getElementByText(key), @@ -188,12 +218,6 @@ class TransactionPayConfirmation { }); } - get tokenListScrollView(): Promise { - return Matchers.getIdentifier( - TransactionPayComponentIDs.PAY_WITH_TOKEN_LIST, - ); - } - async isAmountEntryVisible(): Promise { await Assertions.expectElementToBeVisible(this.keyboardContinueButton, { description: 'Transaction pay keyboard continue button', @@ -209,33 +233,17 @@ class TransactionPayConfirmation { } async tapPayWithToken(tokenSymbol: string, index = 0): Promise { - await encapsulatedAction({ - detox: async () => { - const tokenElement = Matchers.getElementByText( - tokenSymbol, - ) as unknown as DetoxElement; - await Gestures.scrollToElement(tokenElement, this.tokenListScrollView, { - direction: 'down', - scrollAmount: 200, - elemDescription: `Pay With Token ${tokenSymbol}`, - }); - await Gestures.waitAndTap(Matchers.getElementByText(tokenSymbol), { - elemDescription: `Pay With Token ${tokenSymbol}`, - }); - }, - appium: async () => { - const tokenElements = - await PlaywrightMatchers.getAllElementsByText(tokenSymbol); - if (tokenElements.length === 0) { - throw new Error( - `No pay with token option found for "${tokenSymbol}"`, - ); - } - await UnifiedGestures.tapAtIndex(tokenElements, index, { - description: `Pay With Token ${tokenSymbol}`, - }); - }, - }); + const tokenElement = this.getTokenOptionAt(tokenSymbol, index); + const opts = { description: `Pay With Token ${tokenSymbol}` }; + + if (FrameworkDetector.isDetox()) { + await UnifiedGestures.scrollToElement( + tokenElement, + this.tokenListScrollViewIdentifier, + { ...opts, direction: 'down', scrollAmount: 200 }, + ); + } + await UnifiedGestures.waitAndTap(tokenElement, opts); } async tapKeyboardContinueButton(): Promise {