From 1c80a315f85ba6a3f95cd133e53f11f9f7247949 Mon Sep 17 00:00:00 2001 From: Christopher Ferreira Date: Fri, 17 Oct 2025 16:08:46 +0100 Subject: [PATCH 1/2] test: removes old framework and organizes the framework --- e2e/api-specs/ConfirmationsRejectionRule.js | 6 +- e2e/framework/.eslintrc.js | 52 --- e2e/framework/Constants.ts | 12 + e2e/framework/DeepLink.ts | 26 +- e2e/framework/Gestures.ts | 34 ++ e2e/framework/PerpsE2E.ts | 19 - e2e/{utils => framework}/SoftAssert.ts | 2 +- e2e/framework/fixtures/FixtureHelper.ts | 2 +- e2e/helpers.js | 2 +- e2e/init.js | 3 +- e2e/pages/Browser/TestSnaps.ts | 3 +- e2e/specs/analytics/import-wallet.spec.ts | 2 +- e2e/specs/analytics/new-wallet.spec.ts | 2 +- e2e/specs/analytics/opt-out.ts | 2 +- e2e/specs/card/card-button.spec.ts | 2 +- e2e/specs/card/card-home-add-funds.spec.ts | 2 +- e2e/specs/card/card-home-manage-card.spec.ts | 2 +- .../dapp-initiated-transfer.spec.ts | 2 +- e2e/specs/perps/helpers/perps-modifiers.ts | 22 + e2e/specs/perps/perps-limit-long-fill.spec.ts | 4 +- .../perps/perps-position-liquidation.spec.ts | 10 +- .../quarantine/offramp-cashout.failing.ts | 2 +- e2e/specs/quarantine/offramp.failing.ts | 2 +- e2e/specs/quarantine/onramp.failing.ts | 2 +- e2e/specs/quarantine/swap-deeplink.failing.ts | 18 +- .../quarantine/swap-segment-smoke.failing.ts | 20 +- e2e/specs/ramps/onramp-parameters.spec.ts | 2 +- e2e/specs/snaps/test-snap-dialog.spec.ts | 2 +- .../snaps/test-snap-interactive-ui.spec.ts | 2 +- e2e/specs/snaps/test-snap-lifecycle.spec.ts | 2 +- e2e/specs/swaps/bridge-action-smoke.spec.ts | 2 +- e2e/specs/swaps/swap-action-smoke.spec.ts | 2 +- e2e/utils/Assertions.js | 389 ------------------ e2e/utils/Gestures.js | 281 ------------- e2e/utils/Matchers.js | 171 -------- e2e/utils/Utilities.js | 154 ------- e2e/viewHelper.ts | 2 +- 37 files changed, 137 insertions(+), 1127 deletions(-) delete mode 100644 e2e/framework/PerpsE2E.ts rename e2e/{utils => framework}/SoftAssert.ts (97%) create mode 100644 e2e/specs/perps/helpers/perps-modifiers.ts delete mode 100644 e2e/utils/Assertions.js delete mode 100644 e2e/utils/Gestures.js delete mode 100644 e2e/utils/Matchers.js delete mode 100644 e2e/utils/Utilities.js diff --git a/e2e/api-specs/ConfirmationsRejectionRule.js b/e2e/api-specs/ConfirmationsRejectionRule.js index 776bdac65df5..a451d6a194bc 100644 --- a/e2e/api-specs/ConfirmationsRejectionRule.js +++ b/e2e/api-specs/ConfirmationsRejectionRule.js @@ -3,8 +3,8 @@ import { device } from 'detox'; import { addToQueue } from './helpers'; import paramsToObj from '@open-rpc/test-coverage/build/utils/params-to-obj'; import TestHelpers from '../helpers'; -import Matchers from '../utils/Matchers'; -import Gestures from '../utils/Gestures'; +import Matchers from '../framework/Matchers'; +import Gestures from '../framework/Gestures'; import ConnectBottomSheet from '../pages/Browser/ConnectBottomSheet'; import AssetWatchBottomSheet from '../pages/Transactions/AssetWatchBottomSheet'; import SpamFilterModal from '../pages/Browser/SpamFilterModal'; @@ -14,7 +14,7 @@ import ConnectedAccountsModal from '../pages/Browser/ConnectedAccountsModal'; // eslint-disable-next-line import/no-nodejs-modules import fs from 'fs'; -import Assertions from '../utils/Assertions'; +import Assertions from '../framework/Assertions'; import PermissionSummaryBottomSheet from '../pages/Browser/PermissionSummaryBottomSheet'; const getBase64FromPath = async (path) => { diff --git a/e2e/framework/.eslintrc.js b/e2e/framework/.eslintrc.js index 0000969b2a58..311dc363bace 100644 --- a/e2e/framework/.eslintrc.js +++ b/e2e/framework/.eslintrc.js @@ -89,57 +89,5 @@ module.exports = { ], }, }, - { - files: ['**/e2e/pages/**/*.{js,ts}'], - rules: { - 'no-restricted-imports': [ - 'error', - { - paths: [ - { - name: '../utils/Gestures', - message: - 'Do not import Gestures from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Gestures.js', - message: - 'Do not import Gestures from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Assertions', - message: - 'Do not import Assertions from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Assertions.js', - message: - 'Do not import Assertions from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Utilities', - message: - 'Do not import Utilities from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Utilities.js', - message: - 'Do not import Utilities from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Matchers', - message: - 'Do not import Matchers from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - { - name: '../utils/Matchers.js', - message: - 'Do not import Matchers from e2e/utils/. Use e2e/framework/index.ts instead.', - }, - ], - }, - ], - }, - }, ], }; diff --git a/e2e/framework/Constants.ts b/e2e/framework/Constants.ts index 2260e37e83f3..048eda5e2b76 100644 --- a/e2e/framework/Constants.ts +++ b/e2e/framework/Constants.ts @@ -51,6 +51,18 @@ export const DEFAULT_SOLANA_TEST_DAPP_PATH = path.join( 'dist', ); +/** + * The schemes for the E2E deep links. + * @enum {string} + * @example + * { + * E2EDeeplinkSchemes.PERPS, + * } + */ +export enum E2EDeeplinkSchemes { + PERPS = 'e2e://perps/', +} + /** * The variants of the dapp to load for test. * @enum {string} diff --git a/e2e/framework/DeepLink.ts b/e2e/framework/DeepLink.ts index deb550ca15d1..142f0ef5213b 100644 --- a/e2e/framework/DeepLink.ts +++ b/e2e/framework/DeepLink.ts @@ -1,14 +1,15 @@ /* * Cross-platform deep link opener for Detox tests. */ +import { createLogger } from './logger'; +import { E2EDeeplinkSchemes } from './Constants'; -// Import device from Detox types to avoid shadowing warnings -// eslint-disable-next-line @typescript-eslint/no-explicit-any -declare const device: any; // eslint-disable-line @typescript-eslint/no-explicit-any +const logger = createLogger({ + name: 'E2E - DeepLink', +}); export async function openE2EUrl(url: string): Promise { - // eslint-disable-next-line no-console - console.log(`[E2E] DeepLink open: ${url}`); + logger.debug(`Opening E2E DeepLink: ${url}`); if (device.getPlatform() === 'ios' && device.openURL) { await device.openURL({ url }); return; @@ -17,9 +18,18 @@ export async function openE2EUrl(url: string): Promise { // On Android, our production manifest doesn't declare the custom "e2e" scheme. // Reuse the already-declared "expo-metamask" scheme to transport the command. // Mapping: e2e://perps/? -> expo-metamask://e2e/perps/? - const mappedUrl = url.startsWith('e2e://perps/') - ? url.replace('e2e://perps/', 'expo-metamask://e2e/perps/') - : url; + let mappedUrl = url; + // Handle all E2EDeeplinkSchemes + for (const deeplinkScheme of Object.values(E2EDeeplinkSchemes)) { + if (url.startsWith(deeplinkScheme)) { + mappedUrl = url.replace( + deeplinkScheme, + `expo-metamask://${deeplinkScheme}`, + ); + break; + } + } + if (device.openURL) { await device.openURL({ url: mappedUrl }); return; diff --git a/e2e/framework/Gestures.ts b/e2e/framework/Gestures.ts index e593f87b0f6b..35e7bfec220a 100644 --- a/e2e/framework/Gestures.ts +++ b/e2e/framework/Gestures.ts @@ -355,6 +355,40 @@ export default class Gestures { ); } + /** + * Type text into a web element within a webview using JavaScript injection. + * @param {Promise} element - The web element to type into. + * @param {string} text - The text to type. + */ + static async typeInWebElement( + elem: Promise, + text: string, + ): Promise { + try { + await ( + await elem + ).runScript( + ( + el: { + focus: () => void; + value: string; + _valueTracker?: { setValue: (v: string) => void }; + dispatchEvent: (event: { bubbles?: boolean }) => void; + }, + value: string, + ) => { + el.focus(); + el.value = value; + el._valueTracker && el._valueTracker.setValue(''); + el.dispatchEvent(new Event('input', { bubbles: true })); + }, + [text], + ); + } catch { + await this.typeText(elem, text); + } + } + /** * Replace text in field with retry * @returns A Promise that resolves when the text is successfully replaced diff --git a/e2e/framework/PerpsE2E.ts b/e2e/framework/PerpsE2E.ts deleted file mode 100644 index 8da72dc7886e..000000000000 --- a/e2e/framework/PerpsE2E.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { openE2EUrl } from './DeepLink'; - -class PerpsE2E { - static async updateMarketPrice(symbol: string, price: string): Promise { - await openE2EUrl( - `e2e://perps/push-price?symbol=${encodeURIComponent( - symbol, - )}&price=${encodeURIComponent(price)}`, - ); - } - - static async triggerLiquidation(symbol: string): Promise { - await openE2EUrl( - `e2e://perps/force-liquidation?symbol=${encodeURIComponent(symbol)}`, - ); - } -} - -export default PerpsE2E; diff --git a/e2e/utils/SoftAssert.ts b/e2e/framework/SoftAssert.ts similarity index 97% rename from e2e/utils/SoftAssert.ts rename to e2e/framework/SoftAssert.ts index 859284b7dc51..783558fd56a5 100644 --- a/e2e/utils/SoftAssert.ts +++ b/e2e/framework/SoftAssert.ts @@ -1,4 +1,4 @@ -import { createLogger } from '../framework/logger'; +import { createLogger } from './logger'; const logger = createLogger({ name: 'SoftAssert', diff --git a/e2e/framework/fixtures/FixtureHelper.ts b/e2e/framework/fixtures/FixtureHelper.ts index 3ff455a1293b..8e9297cd1e5c 100644 --- a/e2e/framework/fixtures/FixtureHelper.ts +++ b/e2e/framework/fixtures/FixtureHelper.ts @@ -12,7 +12,7 @@ import { getLocalTestDappPort, getMockServerPort, } from './FixtureUtils'; -import Utilities from '../../utils/Utilities'; +import Utilities from '../../framework/Utilities'; import TestHelpers from '../../helpers'; import { startMockServer, diff --git a/e2e/helpers.js b/e2e/helpers.js index f13736dece52..1542245bb369 100644 --- a/e2e/helpers.js +++ b/e2e/helpers.js @@ -6,7 +6,7 @@ import { getMockServerPort, getSecondTestDappPort, } from './framework/fixtures/FixtureUtils'; -import Utilities from './utils/Utilities'; +import Utilities from './framework/Utilities'; import { resolveConfig } from 'detox/internals'; import { createLogger } from './framework/logger'; diff --git a/e2e/init.js b/e2e/init.js index 0acf2835b439..55e07ee552e9 100644 --- a/e2e/init.js +++ b/e2e/init.js @@ -1,6 +1,5 @@ /* eslint-env jest */ -import { logger } from './framework'; -import Utilities from './utils/Utilities'; +import Utilities from './framework/Utilities'; /** * Before all tests, modify the app launch arguments to include the blacklistURLs. diff --git a/e2e/pages/Browser/TestSnaps.ts b/e2e/pages/Browser/TestSnaps.ts index c6ef72041f24..6055f0cf66fc 100644 --- a/e2e/pages/Browser/TestSnaps.ts +++ b/e2e/pages/Browser/TestSnaps.ts @@ -18,7 +18,6 @@ import TestHelpers from '../../helpers'; import Assertions from '../../framework/Assertions'; import { IndexableWebElement } from 'detox/detox'; import Utilities from '../../framework/Utilities'; -import LegacyGestures from '../../utils/Gestures'; import { ConfirmationFooterSelectorIDs } from '../../selectors/Confirmation/ConfirmationView.selectors'; import { waitForTestSnapsToLoad } from '../../viewHelper'; import { RetryOptions } from '../../framework'; @@ -315,7 +314,7 @@ class TestSnaps { TestSnapInputSelectorWebIDS[locator], ) as Promise; // New gestures currently don't support web elements - await LegacyGestures.typeInWebElement(webElement, message); + await Gestures.typeInWebElement(webElement, message); } async approveSignRequest() { diff --git a/e2e/specs/analytics/import-wallet.spec.ts b/e2e/specs/analytics/import-wallet.spec.ts index 627f140602c5..2149098dcd45 100644 --- a/e2e/specs/analytics/import-wallet.spec.ts +++ b/e2e/specs/analytics/import-wallet.spec.ts @@ -13,7 +13,7 @@ import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, } from '../identity/utils/constants'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { Mockttp } from 'mockttp'; diff --git a/e2e/specs/analytics/new-wallet.spec.ts b/e2e/specs/analytics/new-wallet.spec.ts index 5015ff901c24..991f05436841 100644 --- a/e2e/specs/analytics/new-wallet.spec.ts +++ b/e2e/specs/analytics/new-wallet.spec.ts @@ -5,7 +5,7 @@ import { CreateNewWallet } from '../../viewHelper'; import TestHelpers from '../../helpers'; import Assertions from '../../framework/Assertions'; import { getEventsPayloads, onboardingEvents } from './helpers'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; diff --git a/e2e/specs/analytics/opt-out.ts b/e2e/specs/analytics/opt-out.ts index 0d706aafb6da..438a9e361127 100644 --- a/e2e/specs/analytics/opt-out.ts +++ b/e2e/specs/analytics/opt-out.ts @@ -14,7 +14,7 @@ import { getEventsPayloads, onboardingEvents, } from './helpers'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; describe( RegressionWalletPlatform( diff --git a/e2e/specs/card/card-button.spec.ts b/e2e/specs/card/card-button.spec.ts index a064acdccf06..360ab6571079 100644 --- a/e2e/specs/card/card-button.spec.ts +++ b/e2e/specs/card/card-button.spec.ts @@ -7,7 +7,7 @@ import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { testSpecificMock } from '../../api-mocking/mock-responses/cardholder-mocks'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; import CardHomeView from '../../pages/Card/CardHomeView'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { CustomNetworks } from '../../resources/networks.e2e'; describe.skip(SmokeCard('Card NavBar Button'), () => { diff --git a/e2e/specs/card/card-home-add-funds.spec.ts b/e2e/specs/card/card-home-add-funds.spec.ts index e39975bcd6f6..49f20e8af6be 100644 --- a/e2e/specs/card/card-home-add-funds.spec.ts +++ b/e2e/specs/card/card-home-add-funds.spec.ts @@ -7,7 +7,7 @@ import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { testSpecificMock } from '../../api-mocking/mock-responses/cardholder-mocks'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; import CardHomeView from '../../pages/Card/CardHomeView'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { CustomNetworks } from '../../resources/networks.e2e'; describe.skip(SmokeCard('CardHome - Add Funds'), () => { diff --git a/e2e/specs/card/card-home-manage-card.spec.ts b/e2e/specs/card/card-home-manage-card.spec.ts index 991de9452892..50c98b24edbe 100644 --- a/e2e/specs/card/card-home-manage-card.spec.ts +++ b/e2e/specs/card/card-home-manage-card.spec.ts @@ -7,7 +7,7 @@ import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { testSpecificMock } from '../../api-mocking/mock-responses/cardholder-mocks'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; import CardHomeView from '../../pages/Card/CardHomeView'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { CustomNetworks } from '../../resources/networks.e2e'; describe.skip(SmokeCard('CardHome - Manage Card'), () => { diff --git a/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts b/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts index 12644dd3750f..a101540ba7f3 100644 --- a/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts +++ b/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts @@ -16,7 +16,7 @@ import { import TestDApp from '../../../pages/Browser/TestDApp'; import { DappVariants } from '../../../framework/Constants'; import { EventPayload, getEventsPayloads } from '../../analytics/helpers'; -import SoftAssert from '../../../utils/SoftAssert'; +import SoftAssert from '../../../framework/SoftAssert'; import { Mockttp } from 'mockttp'; import { setupMockRequest, diff --git a/e2e/specs/perps/helpers/perps-modifiers.ts b/e2e/specs/perps/helpers/perps-modifiers.ts new file mode 100644 index 000000000000..d592b1a61b6b --- /dev/null +++ b/e2e/specs/perps/helpers/perps-modifiers.ts @@ -0,0 +1,22 @@ +import { openE2EUrl } from '../../../framework/DeepLink'; +import { E2EDeeplinkSchemes } from '../../../framework/Constants'; + +class PerpsE2EModifiers { + static async updateMarketPrice(symbol: string, price: string): Promise { + await openE2EUrl( + `${E2EDeeplinkSchemes.PERPS}push-price?symbol=${encodeURIComponent( + symbol, + )}&price=${encodeURIComponent(price)}`, + ); + } + + static async triggerLiquidation(symbol: string): Promise { + await openE2EUrl( + `${E2EDeeplinkSchemes.PERPS}force-liquidation?symbol=${encodeURIComponent( + symbol, + )}`, + ); + } +} + +export default PerpsE2EModifiers; diff --git a/e2e/specs/perps/perps-limit-long-fill.spec.ts b/e2e/specs/perps/perps-limit-long-fill.spec.ts index 959e97d293a2..835b65ce6074 100644 --- a/e2e/specs/perps/perps-limit-long-fill.spec.ts +++ b/e2e/specs/perps/perps-limit-long-fill.spec.ts @@ -10,7 +10,7 @@ import PerpsMarketListView from '../../pages/Perps/PerpsMarketListView'; import PerpsMarketDetailsView from '../../pages/Perps/PerpsMarketDetailsView'; import PerpsOrderView from '../../pages/Perps/PerpsOrderView'; import PerpsView from '../../pages/Perps/PerpsView'; -import PerpsE2E from '../../framework/PerpsE2E'; +import PerpsE2EModifiers from './helpers/perps-modifiers'; describe(RegressionTrade('Perps - ETH limit long fill'), () => { it.skip('creates ETH limit long at -10%, shows open order, then fills after -15%', async () => { @@ -63,7 +63,7 @@ describe(RegressionTrade('Perps - ETH limit long fill'), () => { // Push the price -15% to ensure the order is executed // Default ETH price in mock is 2500.00, -15% => 2125.00 - await PerpsE2E.updateMarketPrice('ETH', '2125.00'); + await PerpsE2EModifiers.updateMarketPrice('ETH', '2125.00'); // Navigate to ETH again to verify order is gone and position is present await TabBarComponent.tapActions(); diff --git a/e2e/specs/perps/perps-position-liquidation.spec.ts b/e2e/specs/perps/perps-position-liquidation.spec.ts index a8a9b654a425..1b3e6c215155 100644 --- a/e2e/specs/perps/perps-position-liquidation.spec.ts +++ b/e2e/specs/perps/perps-position-liquidation.spec.ts @@ -10,7 +10,7 @@ import { PERPS_ARBITRUM_MOCKS } from '../../api-mocking/mock-responses/perps-arb import PerpsMarketDetailsView from '../../pages/Perps/PerpsMarketDetailsView'; import PerpsView from '../../pages/Perps/PerpsView'; import { createLogger, LogLevel } from '../../framework/logger'; -import PerpsE2E from '../../framework/PerpsE2E'; +import PerpsE2EModifiers from './helpers/perps-modifiers'; import Assertions from '../../framework/Assertions'; import Matchers from '../../framework/Matchers'; import { PerpsPositionsViewSelectorsIDs } from '../../selectors/Perps/Perps.selectors'; @@ -54,8 +54,8 @@ describe(RegressionTrade('Perps Position'), () => { await PerpsView.tapBackButtonMarketList(); // add price change and liquidation -> not yet liquidated - await PerpsE2E.updateMarketPrice('BTC', '80000.00'); - await PerpsE2E.triggerLiquidation('BTC'); + await PerpsE2EModifiers.updateMarketPrice('BTC', '80000.00'); + await PerpsE2EModifiers.triggerLiquidation('BTC'); logger.info('🔥 E2E Mock: Liquidation triggered. Not yet liquidated'); // Assertion 1: still have 2 positions (the default and the recently opened) @@ -63,8 +63,8 @@ describe(RegressionTrade('Perps Position'), () => { await PerpsView.ensurePerpsTabPositionVisible('BTC', 3, 'long', 1); // add price change and force liquidation - BTC below 30k triggers default BTC liquidation - await PerpsE2E.updateMarketPrice('BTC', '30000.00'); - await PerpsE2E.triggerLiquidation('BTC'); + await PerpsE2EModifiers.updateMarketPrice('BTC', '30000.00'); + await PerpsE2EModifiers.triggerLiquidation('BTC'); logger.info('🔥 E2E Mock: Liquidation triggered. Liquidated'); // Assertion 2: only BTC 3x is visible diff --git a/e2e/specs/quarantine/offramp-cashout.failing.ts b/e2e/specs/quarantine/offramp-cashout.failing.ts index 649a06b85511..507cd9eb77d1 100644 --- a/e2e/specs/quarantine/offramp-cashout.failing.ts +++ b/e2e/specs/quarantine/offramp-cashout.failing.ts @@ -13,7 +13,7 @@ import { findEvent, getEventsPayloads, } from '../analytics/helpers'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants'; import { Mockttp } from 'mockttp'; import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup'; diff --git a/e2e/specs/quarantine/offramp.failing.ts b/e2e/specs/quarantine/offramp.failing.ts index ade4917b0370..780e315097af 100644 --- a/e2e/specs/quarantine/offramp.failing.ts +++ b/e2e/specs/quarantine/offramp.failing.ts @@ -11,7 +11,7 @@ import SellGetStartedView from '../../pages/Ramps/SellGetStartedView'; import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; import QuotesView from '../../pages/Ramps/QuotesView'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants'; import TestHelpers from '../../helpers'; diff --git a/e2e/specs/quarantine/onramp.failing.ts b/e2e/specs/quarantine/onramp.failing.ts index b615fe060b5a..d9fb4d0bf2ec 100644 --- a/e2e/specs/quarantine/onramp.failing.ts +++ b/e2e/specs/quarantine/onramp.failing.ts @@ -9,7 +9,7 @@ import Assertions from '../../framework/Assertions'; import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; import QuotesView from '../../pages/Ramps/QuotesView'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants'; import { Mockttp } from 'mockttp'; diff --git a/e2e/specs/quarantine/swap-deeplink.failing.ts b/e2e/specs/quarantine/swap-deeplink.failing.ts index 7a9b65be2102..f84f37bb30a7 100644 --- a/e2e/specs/quarantine/swap-deeplink.failing.ts +++ b/e2e/specs/quarantine/swap-deeplink.failing.ts @@ -1,28 +1,28 @@ 'use strict'; /* eslint-disable no-console */ import { Mockttp } from 'mockttp'; -import { loginToApp } from '../../viewHelper.js'; -import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { loginToApp } from '../../viewHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import Ganache from '../../../app/util/test/ganache'; import { loadFixture, stopFixtureServer, startFixtureServer, -} from '../../framework/fixtures/FixtureHelper.ts'; +} from '../../framework/fixtures/FixtureHelper'; import TestHelpers from '../../helpers.js'; -import FixtureServer from '../../framework/fixtures/FixtureServer.ts'; +import FixtureServer from '../../framework/fixtures/FixtureServer'; import { getFixturesServerPort, getMockServerPort, -} from '../../framework/fixtures/FixtureUtils.ts'; +} from '../../framework/fixtures/FixtureUtils'; import { SmokeTrade } from '../../tags.js'; -import Assertions from '../../utils/Assertions.js'; +import Assertions from '../../framework/Assertions'; import { startMockServer, stopMockServer } from '../../api-mocking/mock-server'; import QuoteView from '../../pages/swaps/QuoteView'; -import Matchers from '../../utils/Matchers.js'; -import Gestures from '../../utils/Gestures.js'; +import Matchers from '../../framework/Matchers'; +import Gestures from '../../framework/Gestures'; import { Assertions as FrameworkAssertions } from '../../framework'; -import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks.ts'; +import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks'; import { localNodeOptions } from '../swaps/helpers/constants'; const fixtureServer: FixtureServer = new FixtureServer(); diff --git a/e2e/specs/quarantine/swap-segment-smoke.failing.ts b/e2e/specs/quarantine/swap-segment-smoke.failing.ts index ee8a10098291..f84f37bb30a7 100644 --- a/e2e/specs/quarantine/swap-segment-smoke.failing.ts +++ b/e2e/specs/quarantine/swap-segment-smoke.failing.ts @@ -1,29 +1,29 @@ 'use strict'; /* eslint-disable no-console */ import { Mockttp } from 'mockttp'; -import { loginToApp } from '../../viewHelper.js'; -import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { loginToApp } from '../../viewHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import Ganache from '../../../app/util/test/ganache'; import { loadFixture, stopFixtureServer, startFixtureServer, -} from '../../framework/fixtures/FixtureHelper.ts'; +} from '../../framework/fixtures/FixtureHelper'; import TestHelpers from '../../helpers.js'; -import FixtureServer from '../../framework/fixtures/FixtureServer.ts'; +import FixtureServer from '../../framework/fixtures/FixtureServer'; import { getFixturesServerPort, getMockServerPort, -} from '../../framework/fixtures/FixtureUtils.ts'; +} from '../../framework/fixtures/FixtureUtils'; import { SmokeTrade } from '../../tags.js'; -import Assertions from '../../utils/Assertions.js'; +import Assertions from '../../framework/Assertions'; import { startMockServer, stopMockServer } from '../../api-mocking/mock-server'; import QuoteView from '../../pages/swaps/QuoteView'; -import Matchers from '../../utils/Matchers.js'; -import Gestures from '../../utils/Gestures.js'; +import Matchers from '../../framework/Matchers'; +import Gestures from '../../framework/Gestures'; import { Assertions as FrameworkAssertions } from '../../framework'; -import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks.ts'; -import { localNodeOptions } from '../swaps/helpers/constants.ts'; +import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks'; +import { localNodeOptions } from '../swaps/helpers/constants'; const fixtureServer: FixtureServer = new FixtureServer(); diff --git a/e2e/specs/ramps/onramp-parameters.spec.ts b/e2e/specs/ramps/onramp-parameters.spec.ts index f5fb87851a32..9fbb56ba8c1f 100644 --- a/e2e/specs/ramps/onramp-parameters.spec.ts +++ b/e2e/specs/ramps/onramp-parameters.spec.ts @@ -13,7 +13,7 @@ import SelectRegionView from '../../pages/Ramps/SelectRegionView'; import SelectPaymentMethodView from '../../pages/Ramps/SelectPaymentMethodView'; import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; import { EventPayload, getEventsPayloads } from '../analytics/helpers'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants'; import Matchers from '../../framework/Matchers'; import { Mockttp } from 'mockttp'; diff --git a/e2e/specs/snaps/test-snap-dialog.spec.ts b/e2e/specs/snaps/test-snap-dialog.spec.ts index dc6b774aecc9..3963553f4755 100644 --- a/e2e/specs/snaps/test-snap-dialog.spec.ts +++ b/e2e/specs/snaps/test-snap-dialog.spec.ts @@ -3,7 +3,7 @@ import { loginToApp } from '../../viewHelper'; import TabBarComponent from '../../pages/wallet/TabBarComponent'; import TestSnaps from '../../pages/Browser/TestSnaps'; import Assertions from '../../framework/Assertions'; -import Gestures from '../../utils/Gestures'; +import Gestures from '../../framework/Gestures'; import { Matchers } from '../../framework'; import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; diff --git a/e2e/specs/snaps/test-snap-interactive-ui.spec.ts b/e2e/specs/snaps/test-snap-interactive-ui.spec.ts index eff7e1420713..342eef3e8e64 100644 --- a/e2e/specs/snaps/test-snap-interactive-ui.spec.ts +++ b/e2e/specs/snaps/test-snap-interactive-ui.spec.ts @@ -5,7 +5,7 @@ import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import TabBarComponent from '../../pages/wallet/TabBarComponent'; import TestSnaps from '../../pages/Browser/TestSnaps'; import { Assertions } from '../../framework'; -import Matchers from '../../utils/Matchers'; +import Matchers from '../../framework/Matchers'; jest.setTimeout(150_000); diff --git a/e2e/specs/snaps/test-snap-lifecycle.spec.ts b/e2e/specs/snaps/test-snap-lifecycle.spec.ts index 12a47efcfff7..3fbc75d61735 100644 --- a/e2e/specs/snaps/test-snap-lifecycle.spec.ts +++ b/e2e/specs/snaps/test-snap-lifecycle.spec.ts @@ -5,7 +5,7 @@ import TestHelpers from '../../helpers'; import TestSnaps from '../../pages/Browser/TestSnaps'; import TabBarComponent from '../../pages/wallet/TabBarComponent'; import { FlaskBuildTests } from '../../tags'; -import Assertions from '../../utils/Assertions'; +import Assertions from '../../framework/Assertions'; import { loginToApp } from '../../viewHelper'; jest.setTimeout(150_000); diff --git a/e2e/specs/swaps/bridge-action-smoke.spec.ts b/e2e/specs/swaps/bridge-action-smoke.spec.ts index a3bf2e728bfa..c8b50a14ba5a 100644 --- a/e2e/specs/swaps/bridge-action-smoke.spec.ts +++ b/e2e/specs/swaps/bridge-action-smoke.spec.ts @@ -11,7 +11,7 @@ import Assertions from '../../framework/Assertions'; import ActivitiesView from '../../pages/Transactions/ActivitiesView'; import { prepareSwapsTestEnvironment } from './helpers/prepareSwapsTestEnvironment'; import { testSpecificMock } from './helpers/bridge-mocks'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; enum eventsToCheck { BRIDGE_BUTTON_CLICKED = 'Bridge Button Clicked', diff --git a/e2e/specs/swaps/swap-action-smoke.spec.ts b/e2e/specs/swaps/swap-action-smoke.spec.ts index 9d27fe453b3c..5bb22250730d 100644 --- a/e2e/specs/swaps/swap-action-smoke.spec.ts +++ b/e2e/specs/swaps/swap-action-smoke.spec.ts @@ -1,6 +1,6 @@ import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import { LocalNodeType } from '../../framework/types'; -import SoftAssert from '../../utils/SoftAssert'; +import SoftAssert from '../../framework/SoftAssert'; import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import Assertions from '../../framework/Assertions'; import WalletView from '../../pages/wallet/WalletView'; diff --git a/e2e/utils/Assertions.js b/e2e/utils/Assertions.js deleted file mode 100644 index aea624226d9b..000000000000 --- a/e2e/utils/Assertions.js +++ /dev/null @@ -1,389 +0,0 @@ -import { waitFor, element, by } from 'detox'; -import { Buffer } from 'buffer/'; -import Matchers from './Matchers'; - -// Global timeout variable -const TIMEOUT = 15000; - -/** - * Class representing a set of assertions for Detox testing. - */ -class Assertions { - /** - * Check if an element with the specified ID is visible. - * @param {Promise} element - The element to check. - * @param timeout - */ - static async checkIfVisible(element, timeout = TIMEOUT) { - return device.getPlatform() === 'ios' - ? await waitFor(await element) - .toExist() - .withTimeout(timeout) - : await waitFor(await element) - .toBeVisible() - .withTimeout(timeout); - } - - /** - * Check if an element with the specified web selector exists. - * @param {Promise} element - The element to check. - */ - static async webViewElementExists(element) { - // rename this. We are checking if element is visible. - return await expect(await element).toExist(); - } - - /** - * Check if an element with the specified ID is not visible. - * @param {Promise} element - The element to check. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfNotVisible(element, timeout = TIMEOUT) { - return await waitFor(await element) - .not.toBeVisible() - .withTimeout(timeout); - } - - /** - * Check if an element with the specified ID does have the specified text. - * @param {Promise} element - The element to check. - * @param {string} text - The text content to check. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfElementToHaveText(element, text, timeout = TIMEOUT) { - // Rename me. The naming convention here is terrible. - - return await waitFor(await element) - .toHaveText(text) - .withTimeout(timeout); - } - - /** - * Check if an element with the specified ID does have the specified label. - * @param {Promise} element - The element to check. - * @param {string} label - The label content to check. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfElementHasLabel(element, label, timeout = TIMEOUT) { - return await waitFor(await element) - .toHaveLabel(label) - .withTimeout(timeout); - } - - /** - * Check if text is visible. - * @param {string} text - The text to check if displayed. - * @param {number} [index=0] - Index of the element if multiple elements match. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfTextIsDisplayed(text, timeout = TIMEOUT) { - const element = Matchers.getElementByText(text); - return this.checkIfVisible(element, timeout); - } - - /** - * Check if text is not visible. - * @param {string} text - The text to check if not displayed. - * @param {number} [index=0] - Index of the element if multiple elements match. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfTextIsNotDisplayed(text, timeout = TIMEOUT) { - const element = Matchers.getElementByText(text); - return this.checkIfNotVisible(element, timeout); - } - - /** - * Check if an element with the specified ID does not have the specified text. - * @param {Promise} element - The element to check. - * @param {string} text - The text content to check. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfElementNotToHaveText(element, text, timeout = TIMEOUT) { - // Rename me. The naming convention here is terrible. - - return await waitFor(await element) - .not.toHaveText(text) - .withTimeout(timeout); - } - - /** - * Check if an element with the specified ID does not have the specified label. - * @param {Promise} element - The element to check. - * @param {string} label - The label content to check. - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfElementDoesNotHaveLabel( - element, - label, - timeout = TIMEOUT, - ) { - // Rename me. The naming convention here is terrible. - - return await waitFor(await element) - .not.toHaveLabel(label) - .withTimeout(timeout); - } - - /** - * Check if the toggle with the specified ID is in the "on" state. - * @param {Promise} element - The ID of the toggle element. - */ - static async checkIfToggleIsOn(element) { - return expect(await element).toHaveToggleValue(true); - } - - /** - * Check if the toggle with the specified ID is in the "off" state. - * @param {Promise} element - The toggle element. - */ - static async checkIfToggleIsOff(element) { - return expect(await element).toHaveToggleValue(false); - } - - /** - * Check if two text values match exactly. - * Automatically normalizes non-breaking spaces and other whitespace characters. - * @param {string} actualText - The actual text value to check. - * @param {string} expectedText - The expected text value to match against. - */ - static async checkIfTextMatches(actualText, expectedText) { - try { - if (!actualText || !expectedText) { - throw new Error('Both actual and expected text must be provided'); - } - - // Normalize non-breaking spaces to regular spaces for comparison - const normalizedActual = actualText.replace(/\u00A0/g, ' '); - const normalizedExpected = expectedText.replace(/\u00A0/g, ' '); - - return expect(normalizedActual).toBe(normalizedExpected); - } catch (error) { - // Check normalized versions for comparison - const normalizedActual = actualText.replace(/\u00A0/g, ' '); - const normalizedExpected = expectedText.replace(/\u00A0/g, ' '); - - if (normalizedActual !== normalizedExpected) { - // Provide detailed debugging information - const actualBytes = Buffer.from(actualText, 'utf8').toString('hex'); - const expectedBytes = Buffer.from(expectedText, 'utf8').toString('hex'); - throw new Error( - `Text matching failed.\nExpected: "${expectedText}"\nActual: "${actualText}"\n` + - `Expected (hex): ${expectedBytes}\nActual (hex): ${actualBytes}\n` + - `Expected (normalized): "${normalizedExpected}"\nActual (normalized): "${normalizedActual}"`, - ); - } - } - } - - /** - * Check if two objects match exactly. - * Note: This assertion does not test UI elements. It is intended for testing values such as events from the mock server or other non-UI data. - * @param {Object} actualObject - The actual object to check. - * @param {Object} expectedObject - The expected object to match against. - */ - static async checkIfObjectsMatch(actualObject, expectedObject) { - try { - if (!actualObject || !expectedObject) { - throw new Error('Both actual and expected objects must be provided'); - } - - return expect(actualObject).toEqual(expectedObject); - } catch (error) { - if (JSON.stringify(actualObject) !== JSON.stringify(expectedObject)) { - throw new Error( - `Object matching failed.\nExpected: ${JSON.stringify( - expectedObject, - null, - 2, - )}\nActual: ${JSON.stringify(actualObject, null, 2)}`, - ); - } - } - } - - /** - * Check if an array has the expected length. - * Note: This assertion does not test UI elements. It is intended for testing values such as events from the mock server or other non-UI data. - * @param {Array} array - The array to check. - * @param {number} expectedLength - The expected length of the array. - */ - static async checkIfArrayHasLength(array, expectedLength) { - try { - if (!Array.isArray(array)) { - throw new Error('The provided value is not an array'); - } - - if (typeof expectedLength !== 'number') { - throw new Error('Expected length must be a number'); - } - - return expect(array.length).toBe(expectedLength); - } catch (error) { - if (array.length !== expectedLength) { - throw new Error( - `Array length assertion failed.\nExpected length: ${expectedLength}\nActual length: ${array.length}`, - ); - } - } - } - - /** - * Check if a value is defined (not null, not undefined, not an empty string). - * Also evaluates a Boolean value. - * Note: This assertion does not test UI elements. It is intended for testing values such as events from the mock server or other non-UI data. - * @param {*} value - The value to check. - */ - static async checkIfValueIsDefined(value) { - // 0 evaluates to false, so we need to handle it separately - if (typeof value === 'number') { - return; - } - - if (!value) { - throw new Error('Value is not present (falsy value)'); - } - } - - /** - * Checks if the actual object contains all key/value pairs from the partial object. - * Throws an error if the assertion fails, listing all issues found. - * @param {Object} actual - The object to check against - * @param {Object} partial - The partial object with expected key/value pairs - * @param {boolean} deep - Whether to perform deep comparison for nested objects (default: true) - */ - static async checkIfObjectContains(actual, partial, deep = true) { - return new Promise((resolve, reject) => { - const errors = []; - - function check(actualObj, partialObj, path = '') { - if ( - typeof actualObj !== 'object' || - typeof partialObj !== 'object' || - actualObj === null || - partialObj === null - ) { - if (actualObj !== partialObj) { - errors.push( - `Value mismatch at "${path || 'root'}": expected ${JSON.stringify( - partialObj, - )}, got ${JSON.stringify(actualObj)}`, - ); - } - return; - } - - for (const key in partialObj) { - const currentPath = path ? `${path}.${key}` : key; - if (!Object.prototype.hasOwnProperty.call(actualObj, key)) { - errors.push(`Missing key at "${currentPath}" in actual object`); - continue; - } - - if ( - deep && - typeof partialObj[key] === 'object' && - partialObj[key] !== null - ) { - check(actualObj[key], partialObj[key], currentPath); - } else if (actualObj[key] !== partialObj[key]) { - errors.push( - `Value mismatch at "${currentPath}": expected ${JSON.stringify( - partialObj[key], - )}, got ${JSON.stringify(actualObj[key])}`, - ); - } - } - } - - check(actual, partial); - - if (errors.length > 0) { - reject( - new Error('Object contains assertion failed:\n' + errors.join('\n')), - ); - } else { - resolve(); - } - }); - } - - /** - * Checks if the actual object contains all keys from the expected array - * @param {Object} actual - The object to check against - * @param {Object} validations - Object with keys and their expected values - */ - static checkIfObjectHasKeysAndValidValues(actual, validations) { - const errors = []; - - for (const [key, validation] of Object.entries(validations)) { - if (!Object.prototype.hasOwnProperty.call(actual, key)) { - errors.push(`Missing key: ${key}`); - continue; - } - - const value = actual[key]; - - if (typeof validation === 'string') { - const actualType = typeof value; - - if (Array.isArray(value) && validation === 'array') continue; - if (value === null && validation === 'null') continue; - - // Check type - if ( - actualType !== validation && - !(Array.isArray(value) && validation === 'array') - ) { - errors.push( - `Type mismatch for key "${key}": expected "${validation}", got "${actualType}"`, - ); - } - } else if (typeof validation === 'function') { - try { - const valid = validation(value); - if (!valid) { - errors.push( - `Validation failed for key "${key}": custom validator returned false`, - ); - } - } catch (err) { - errors.push(`Validation error for key "${key}": ${err.message}`); - } - } - } - - if (errors.length > 0) { - throw new Error('Object validation failed:\n' + errors.join('\n')); - } - } - - /** - * Check if element is enabled - * @param {Promise} element - The element to check - * @return {Promise} - Resolves to true if the element is enabled, false otherwise - */ - static async checkIfEnabled(element) { - return (await (await element).getAttributes()).enabled; - } - - /** - * Check if element is disabled - * @param {Promise} element - The element to check - * @return {Promise} - Resolves to true if the element is disabled, false otherwise - */ - static async checkIfDisabled(element) { - return (await (await element).getAttributes()).enabled; - } - - /** - * Check if label contains text - * @param {string} text - The text to check if the label contains - * @param {number} [timeout=TIMEOUT] - Timeout in milliseconds. - */ - static async checkIfLabelContainsText(text, timeout = TIMEOUT) { - const labelMatcher = element(by.label(new RegExp(text))); - return await waitFor(labelMatcher).toExist().withTimeout(timeout); - } -} - -export default Assertions; diff --git a/e2e/utils/Gestures.js b/e2e/utils/Gestures.js deleted file mode 100644 index 5b03cb2a9e8c..000000000000 --- a/e2e/utils/Gestures.js +++ /dev/null @@ -1,281 +0,0 @@ -/* eslint-disable no-console */ -import { waitFor, expect } from 'detox'; -import Utilities from './Utilities'; - -/** - * Class for handling user actions (Gestures) - */ -class Gestures { - /** - * Helper function to add delay before performing an action. - * Useful when elements are visible but not fully interactive yet. - * - * @param {number} delayMs - Delay in milliseconds - * @returns {Promise} - */ - static async delayBeforeAction(delayMs) { - if (delayMs > 0) { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - } - - /** - * Tap an element and long press. - * - * @param {Promise} element - The element to tap - * @param {number} timeout - Timeout for waiting (default: 2000ms) - */ - static async tapAndLongPress(element, timeout = 2000) { - await (await element).longPress(timeout); - } - - /** - * Tap an element at a specific point. - * - * @param {Promise} element - The element to tap - * @param {Object} point - Coordinates { x, y } where the element will be tapped - */ - static async tapAtPoint(element, point) { - await (await element).tap(point); - } - - /** - * Wait for an element to be visible and then tap it. - * - * @param {Promise} element - The element to tap - - */ - static async tap(element) { - await (await element).tap(); - } - - /** - * Tap an element with text partial text matching before tapping it - * - * @param {string} textPattern - Regular expression pattern to match the text - */ - static async tapTextBeginingWith(textPattern) { - await element(by.text(new RegExp(`^/${textPattern} .*$/`))).tap(); - } - - /** - * Wait for an element to be visible and then tap it. - * - * @param {Promise} elementToTap - The element to tap - * @param {Object} [options={}] - Configuration options - * @param {number} [options.timeout=15000] - Timeout for waiting in milliseconds - * @param {number} [options.delayBeforeTap=0] - Additional delay in milliseconds before tapping after element is visible - * @param {boolean} [options.skipVisibilityCheck=false] - When true, skips the initial visibility check before tapping. Useful for elements that may be technically present but not passing Detox's visibility threshold. - * @param {boolean} [options.experimentalWaitForStability=false] - EXPERIMENTAL: When true, waits for element stability before tapping. - */ - static async waitAndTap(element, options = {}) { - const { - timeout = 15000, - delayBeforeTap = 0, - skipVisibilityCheck = false, - } = options; - const elementToTap = await element; - if (!skipVisibilityCheck) { - await (device.getPlatform() === 'ios' - ? waitFor(elementToTap).toExist() - : waitFor(elementToTap).toBeVisible() - ).withTimeout(timeout); - } - await this.delayBeforeAction(delayBeforeTap); // in some cases the element is visible but not fully interactive yet. - await Utilities.waitForElementToBeEnabled(elementToTap); - await (await elementToTap).tap(); - } - - /** - * Wait for an element at a specific index to be visible and then tap it. - * - * @param {Promise} element - The element to tap - * @param {number} index - Index of the element to tap - * @param {number} timeout - Timeout for waiting (default: 15000ms) - */ - static async tapAtIndex(element, index, timeout = 15000) { - const itemElementAtIndex = (await element).atIndex(index); - await waitFor(itemElementAtIndex).toBeVisible().withTimeout(timeout); - await itemElementAtIndex.tap(); - } - - /** - * Wait for an element to be visible and then tap it. - * - * @param {Promise} element - The element to tap - * @param {Object} options - Options for the tap operation - * @param {number} [options.timeout=15000] - Timeout for waiting (default: 15000ms) - * @param {number} [options.delayBeforeTap=0] - Delay before tapping in milliseconds (default: 0ms) - */ - static async tapWebElement(element, options = {}) { - const { timeout = 15000, delayBeforeTap = 0 } = options; - - // For web elements, we need to use a different approach to wait - const start = Date.now(); - while (Date.now() - start < timeout) { - try { - await expect(await element).toExist(); - - // Add delay before tap if specified - await this.delayBeforeAction(delayBeforeTap); - - await (await element).tap(); - return; - } catch { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - throw new Error('Web element not found or not tappable'); - } - - /** - * Type text into a web element within a webview using JavaScript injection. - * @param {Promise} element - The web element to type into. - * @param {string} text - The text to type. - */ - static async typeInWebElement(element, text) { - try { - await ( - await element - ).runScript( - (el, value) => { - el.focus(); - el.value = value; - el._valueTracker && el._valueTracker.setValue(''); - el.dispatchEvent(new Event('input', { bubbles: true })); - }, - [text], - ); - } catch { - await (await element).typeText(text); - } - } - - /** - * Double tap an element by text. - * - * @param {Promise} element - The element to double tap - */ - static async doubleTap(element) { - await (await element).multiTap(2); - } - - /** - * Clear the text field of an element identified by ID. - * - * @param {Promise} element - The element to clear - * @param {number} timeout - Timeout for waiting (default: 8000ms) - - */ - static async clearField(element, timeout = 2500) { - await waitFor(await element) - .toBeVisible() - .withTimeout(timeout); - - await (await element).replaceText(''); - } - - /** - * Type text into an element and hide the keyboard. - * - * @param {Promise} element - The element to type into - * @param {string} text - Text to be typed into the element - */ - static async typeTextAndHideKeyboard(element, text) { - await this.clearField(element); - - await (await element).typeText(text + '\n'); - } - - /** - * Type text into an element without hiding the keyboard. - * - * @param {Promise} element - The element to type into - * @param {string} text - Text to be typed into the element - */ - static async typeTextWithoutKeyboard(element, text) { - await (await element).typeText(text); - } - - /** - * Replace the text in the field of an element identified by ID. - * - * @param {Promise} element - The element to replace the text in - * @param {string} text - Text to replace the existing text in the element - */ - static async replaceTextInField(element, text, timeout = 10000) { - await waitFor(await element) - .toBeVisible() - .withTimeout(timeout); - - await (await element).replaceText(text); - } - - /** - * Swipe on an element identified by ID. - * - * @param {Promise} element - The element to swipe on - * @param {Detox.Direction} direction - Direction of the swipe - left | right | top | bottom | up | down - * @param {Detox.Speed} [speed] - Speed of the swipe (fast, slow) - * @param {number} [percentage] - Percentage of the swipe (0 to 1) - * @param {number} [xStart] - X-coordinate to start the swipe - * @param {number} [yStart] - Y-coordinate to start the swipe - */ - static async swipe(element, direction, speed, percentage, xStart, yStart) { - await (await element).swipe(direction, speed, percentage, xStart, yStart); - } - - /** - * Swipe on an element identified by ID. - * - * @param {Promise} element - The element to swipe on - * @param {Detox.Direction} direction - Direction of the swipe - left | right | top | bottom | up | down - * @param {Detox.Speed} [speed] - Speed of the swipe (fast, slow) - * @param {number} [percentage] - Percentage of the swipe (0 to 1) - * @param {number} [xStart] - X-coordinate to start the swipe - * @param {number} [yStart] - Y-coordinate to start the swipe - * @param {number} index - Index of the element (default 0) - */ - static async swipeAtIndex( - element, - direction, - speed, - percentage, - xStart, - yStart, - index = 0, - ) { - await (await element) - .atIndex(index) - .swipe(direction, speed, percentage, xStart, yStart); - } - - /** - * Scrolls the web element until its top is at the top of the viewport. - * @param {Promise} element - The element to scroll to the viewport. - */ - static async scrollToWebViewPort(element) { - await (await element).scrollToView(); - } - - /** - * Dynamically Scrolls to an element identified by ID. - * - * @param {Promise} destinationElement - The element to scroll up to - * @param {Promise} scrollIdentifier - The identifier (by.id) NOT element (element(by.id)). Keep this distinction in mind. If you pass in an elementID this method would not work as intended - * @param {Detox.Direction} direction - Direction of the scroll (up, down, left, right). The default is down. - * @param {number} [scrollAmount=350] - The amount to scroll (default is 350). Optional parameter. */ - static async scrollToElement( - destinationElement, - scrollIdentifier, - direction = 'down', - scrollAmount = 350, - ) { - await waitFor(await destinationElement) - .toBeVisible() - .whileElement(await scrollIdentifier) - .scroll(scrollAmount, direction); - } -} - -export default Gestures; diff --git a/e2e/utils/Matchers.js b/e2e/utils/Matchers.js deleted file mode 100644 index 2a2ee7ee98b8..000000000000 --- a/e2e/utils/Matchers.js +++ /dev/null @@ -1,171 +0,0 @@ -import { web, system } from 'detox'; - -/** - * Utility class for matching (locating) UI elements - */ -class Matchers { - /** - * Get element by ID. - * - * @param {string | RegExp } elementId - Match elements with the specified testID - * @param {number} [index] - Index of the element (default: 0) - * @return {Promise} - Resolves to the located element - */ - static async getElementByID(elementId, index) { - if (index) { - return element(by.id(elementId)).atIndex(index); - } - return element(by.id(elementId)); - } - - /** - * Get element by text. - * - * @param {string} text - Match elements with the specified text - * @param {number} index - Index of the element (default: 0) - * @return {Promise} - Resolves to the located element - */ - static async getElementByText(text, index = 0) { - return element(by.text(text)).atIndex(index); - } - - /** - * Get element that match by id and label. - * This strategy matches elements by combining 2 matchers together. - * Elements returned match the provided ID and Label at the same time. - * At this moment, this strategy is only used when trying to select a custom network. - * TODO: remove the dependency of by.id and by.label. This only reduce further possible acceptable matchers. - * - * @param {string} id - Match elements with the specified text - * @param {string | RegExp} label - Match elements with the specified text - * @param {number} index - Index of the element (default: 0) - * @return {Promise} - Resolves to the located element - */ - static async getElementByIDAndLabel(id, label, index = 0) { - return element(by.id(id).and(by.label(label))).atIndex(index); - } - - /** - * Get element by label. - * - * @param {string} label - Match elements with the specified accessibility label (iOS) or content description (Android) - * @param {number} index - Index of the element (default: 0) - * @return {Promise} - Resolves to the located element - */ - static async getElementByLabel(label, index = 0) { - return element(by.label(label)).atIndex(index); - } - - /** - * Get element by descendant. - * - * @param {string} parentElement - Matches elements with at least one descendant that matches the specified matcher. - * @param {string} childElement - The ID of the child element to locate within the parent element. - * @return {Promise} - Resolves to the located element - */ - static async getElementByDescendant(parentElement, childElement) { - return element(by.id(parentElement).withDescendant(by.id(childElement))); - } - - /** - * Get element with ancestor. - * - * @param {string} childElement - The ID of the child element to locate within the parent element. - * @param {string} parentElement - Matches elements with at least one descendant that matches the specified matcher. - * @return {Promise} - Resolves to the located element - */ - static async getElementIDWithAncestor(childElement, parentElement) { - return element(by.id(childElement).withAncestor(by.id(parentElement))); - } - - /** - * Get Native WebView instance by elementId - * - * Because Android Webview might have more that one WebView instance present on the main activity, the correct element - * is select based on its parent element id. - * @param {string} elementId The web ID of the browser webview - * @returns {Detox.WebViewElement} WebView element - */ - static getWebViewByID(elementId) { - if (process.env.CI) { - return device.getPlatform() === 'ios' - ? web(by.id(elementId)) - : web(by.type('android.webkit.WebView').withAncestor(by.id(elementId))); - } - return web(by.id(elementId)); - } - - /** - * Get element by web ID. - * - * @param {string} webviewID - The web ID of the inner element to locate within the webview - * @param {string} innerID - The web ID of the browser webview - * @return {Promise} Resolves to the located element - */ - static async getElementByWebID(webviewID, innerID) { - const myWebView = this.getWebViewByID(webviewID); - return myWebView.element(by.web.id(innerID)); - } - - /** - * Get element by CSS selector. - * @param {string} webviewID - The web ID of the browser webview - * @param {string} selector - CSS selector to locate the element - * @return {Promise} - Resolves to the located element - */ - static async getElementByCSS(webviewID, selector) { - const myWebView = this.getWebViewByID(webviewID); - return myWebView.element(by.web.cssSelector(selector)).atIndex(0); - } - - /** - * Get element by XPath. - * @param {string} webviewID - The web ID of the browser webview - * @param {string} xpath - XPath expression to locate the element - * @return {Promise} - Resolves to the located element - */ - static async getElementByXPath(webviewID, xpath) { - const myWebView = this.getWebViewByID(webviewID); - return myWebView.element(by.web.xpath(xpath)); - } - - /** - * Get element by href. - * @param {string} webviewID - The web ID of the browser webview - * @param {string} url - URL string to locate the element - * @return {Promise} - Resolves to the located element - */ - static async getElementByHref(webviewID, url) { - const myWebView = web(by.id(webviewID)); - return myWebView.element(by.web.href(url)).atIndex(0); - } - - /** - * Creates a Detox matcher for identifying an element by its ID. - * - * @param {string} selectorString - The selector string for identifying the element - * @returns {Matcher} A Detox matcher that identifies elements by the specified ID. - * - * @description - * This method does not create an element but instead generates only a matcher. - * The purpose is to create a matcher that can be used for identification purposes, - * without performing any actions on the element. - * - - */ - static async getIdentifier(selectorString) { - return by.id(selectorString); - } - - /** - * Get system dialogs in the system-level (e.g. permissions, alerts, etc.), by text. - * - * @param {string} text - Match elements with the specified text - * @return {Promise} - Resolves to the located element - */ - static async getSystemElementByText(text) { - return system.element(by.system.label(text)); - } -} - -export default Matchers; diff --git a/e2e/utils/Utilities.js b/e2e/utils/Utilities.js deleted file mode 100644 index 248cd2e35977..000000000000 --- a/e2e/utils/Utilities.js +++ /dev/null @@ -1,154 +0,0 @@ -import { blacklistURLs } from '../resources/blacklistURLs.json'; -import { waitFor } from 'detox'; -// eslint-disable-next-line import/no-nodejs-modules -import { setTimeout as asyncSetTimeout } from 'node:timers/promises'; - -export default class Utilities { - /** - * Formats an array of strings into a regex pattern string for exact matching. - * This method takes an array of strings and returns a string formatted - * for use in a regex pattern, designed to match any one of the provided strings exactly. - * The resulting string is suitable for inclusion in a larger regex pattern. - * - * @param {string[]} regexstrings - An array of strings to be formatted for exact matching in a regex pattern. - * @returns {string} A string formatted for exact matching within a regex pattern, - * encapsulating the input strings in a way that they can be matched as literals. - * @example - * // returns '\\("apple","banana","cherry"\\)' - * formatForExactMatchGroup(['apple', 'banana', 'cherry']); - */ - static formatForExactMatchGroup(regexstrings) { - return `\\("${regexstrings.join('","')}"\\)`; - } - - /** - * A getter method that returns a formatted string of blacklisted URLs for exact matching in a regex pattern. - * This method leverages `formatForExactMatchGroup` to format the `blacklistURLs` array into a regex pattern string, - * suitable for matching any one of the blacklisted URLs exactly. The `blacklistURLs` should be defined - * within the class or accessible in the class context. - * - * @returns {string} A regex pattern string formatted for exact matching of blacklisted URLs. - * @example - */ - static get BlacklistURLs() { - return this.formatForExactMatchGroup(blacklistURLs); - } - - static async waitForElementToBeEnabled( - element, - timeout = 3500, - interval = 100, - ) { - const startTime = Date.now(); - let isEnabled = false; - while (Date.now() - startTime < timeout) { - isEnabled = await (await element).getAttributes(); - if (isEnabled) { - break; - } - await new Promise((resolve) => setTimeout(resolve, interval)); - } - if (!isEnabled) { - throw new Error('Element is not enabled'); - } - } - - /** - * Waits for an element to become stable (not moving) by checking its position multiple times. - * - * @param {Promise} element - The element to check for stability - * @param {Object} [options={}] - Configuration options - * @param {number} [options.timeout=5000] - Maximum time to wait for stability (ms) - * @param {number} [options.interval=200] - Time between position checks (ms) - * @param {number} [options.stableCount=3] - Number of consecutive stable checks required - */ - static async waitForElementToStopMoving(element, options = {}) { - const { timeout = 5000, interval = 200, stableCount = 3 } = options; - let lastPosition = null; - let stableChecks = 0; - const fallBackTimeout = 2000; - const start = Date.now(); - - const getPosition = async (element) => { - try { - const attributes = await element.getAttributes(); - if ( - attributes.frame && - typeof attributes.frame.x === 'number' && - typeof attributes.frame.y === 'number' - ) { - return { x: attributes.frame.x, y: attributes.frame.y }; - } - return null; - } catch { - return null; - } - }; - - while (Date.now() - start < timeout) { - const el = await element; - - const position = await getPosition(el); - - if (!position) { - await new Promise((resolve) => setTimeout(resolve, fallBackTimeout)); - return; // Return early if position is not available - } - - if ( - lastPosition && - position.x === lastPosition.x && - position.y === lastPosition.y - ) { - stableChecks += 1; - if (stableChecks >= stableCount) return; - } else { - lastPosition = position; - stableChecks = 1; - } - - await new Promise((resolve) => setTimeout(resolve, interval)); - } - - throw new Error('Element did not become stable in time'); - } - - /** - * Waits for a condition to be met within a given timeout period. - * - * Note: Copied directly from the extension implementation - * - * @param {() => Promise} condition - The condition to wait for. This function must return a boolean indicating whether the condition is met. - * @param {object} options - Options for the wait. - * @param {number} options.timeout - The maximum amount of time (in milliseconds) to wait for the condition to be met. - * @param {number} options.interval - The interval (in milliseconds) between checks for the condition. - * @returns {Promise} A promise that resolves when the condition is met or the timeout is reached. - * @throws {Error} Throws an error if the condition is not met within the timeout period. - */ - static async waitUntil(condition, { interval, timeout }) { - const startTime = Date.now(); - const endTime = startTime + timeout; - - // Loop indefinitely until condition met or timeout - // eslint-disable-next-line no-constant-condition - while (true) { - const result = await condition(); - if (result === true) { - return; // Condition met - } - - const currentTime = Date.now(); - if (currentTime >= endTime) { - throw new Error(`Condition not met within ${timeout}ms.`); - } - - // Calculate remaining time to ensure we don't overshoot the timeout - const remainingTime = endTime - currentTime; - const waitTime = Math.min(interval, remainingTime); - - // always yield to the event loop, even for an interval of `0`, to avoid a - // macro-task deadlock - await asyncSetTimeout(waitTime, null, { ref: false }); - } - } -} diff --git a/e2e/viewHelper.ts b/e2e/viewHelper.ts index ff292f1a452e..968fea2d74b1 100644 --- a/e2e/viewHelper.ts +++ b/e2e/viewHelper.ts @@ -22,7 +22,7 @@ import { CustomNetworks } from './resources/networks.e2e'; import ToastModal from './pages/wallet/ToastModal'; import TestDApp from './pages/Browser/TestDApp'; import OnboardingSheet from './pages/Onboarding/OnboardingSheet'; -import Matchers from './utils/Matchers'; +import Matchers from './framework/Matchers'; import { BrowserViewSelectorsIDs } from './selectors/Browser/BrowserView.selectors'; import { createLogger } from './framework/logger'; import Utilities, { sleep } from './framework/Utilities'; From 314a97f5976fcd8b3ab211a8eba99d0c7adcc1f0 Mon Sep 17 00:00:00 2001 From: Christopher Ferreira Date: Fri, 17 Oct 2025 16:17:13 +0100 Subject: [PATCH 2/2] test: fix lint --- e2e/specs/multichain-accounts/delete-account.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/multichain-accounts/delete-account.spec.ts b/e2e/specs/multichain-accounts/delete-account.spec.ts index 0b7def43d91a..df71e7f485d6 100644 --- a/e2e/specs/multichain-accounts/delete-account.spec.ts +++ b/e2e/specs/multichain-accounts/delete-account.spec.ts @@ -7,7 +7,7 @@ import { import AccountDetails from '../../pages/MultichainAccounts/AccountDetails'; import DeleteAccount from '../../pages/MultichainAccounts/DeleteAccount'; import Assertions from '../../framework/Assertions'; -import Matchers from '../../utils/Matchers'; +import Matchers from '../../framework/Matchers'; import WalletView from '../../pages/wallet/WalletView'; import TestHelpers from '../../helpers'; import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet';