diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml index 35d674bcee51..7019ff201040 100644 --- a/.github/workflows/build-android-e2e.yml +++ b/.github/workflows/build-android-e2e.yml @@ -9,9 +9,6 @@ on: apk-uploaded: description: 'Whether the APK was successfully uploaded' value: ${{ jobs.build-android-apks.outputs.apk-uploaded }} - aab-uploaded: - description: 'Whether the AAB was successfully uploaded' - value: ${{ jobs.build-android-apks.outputs.aab-uploaded }} inputs: build_type: description: 'The type of build to perform' @@ -32,17 +29,15 @@ on: jobs: build-android-apks: name: Build Android E2E APKs - runs-on: ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xl # Bumped from lg to xl to prevent Daemon disappearance issue (Daemon OOM issue in CI) + runs-on: ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg # lg runner: 16 vCPUs, 48GB RAM timeout-minutes: 40 env: GRADLE_USER_HOME: /home/admin/_work/.gradle CACHE_GENERATION: v1 # Increment this to bust the cache (v1, v2, v3, etc.) outputs: apk-uploaded: ${{ steps.upload-apk.outcome == 'success' }} - aab-uploaded: ${{ steps.upload-aab.outcome == 'success' }} apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }} test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }} - aab-target-path: ${{ steps.determine-target-paths.outputs.aab-target-path }} artifact_name: ${{ steps.determine-target-paths.outputs.artifact_name }} steps: @@ -88,14 +83,12 @@ jobs: { echo "apk-target-path=android/app/build/outputs/apk/flask/release" echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/flask/release" - echo "aab-target-path=android/app/build/outputs/bundle/flaskRelease" echo "artifact_name=app-flask-release" } >> "$GITHUB_OUTPUT" elif [[ "${{ inputs.build_type }}" == "main" ]]; then { echo "apk-target-path=android/app/build/outputs/apk/prod/release" echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/prod/release" - echo "aab-target-path=android/app/build/outputs/bundle/prodRelease" echo "artifact_name=app-prod-release" } >> "$GITHUB_OUTPUT" else @@ -110,7 +103,6 @@ jobs: path: | ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk - ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab # Include Gradle properties in key to force rebuild when properties change # Keep the `hashFiles` call for Gradle config in-sync with these steps: # - "Cache Gradle dependencies" @@ -241,7 +233,6 @@ jobs: path: | ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk - ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab # Keep the `hashFiles` call for Gradle config in-sync with these steps: # - "Check and restore cached APKs if Fingerprint is found" # - "Cache Gradle dependencies" @@ -264,13 +255,3 @@ jobs: path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk retention-days: 7 if-no-files-found: error - - - name: Upload Android AAB - id: upload-aab - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.aab - path: ${{ steps.determine-target-paths.outputs.aab-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.aab - retention-days: 7 - if-no-files-found: warn - continue-on-error: true diff --git a/.github/workflows/run-e2e-workflow.yml b/.github/workflows/run-e2e-workflow.yml index fcc4a9e05e62..6ec79db10127 100644 --- a/.github/workflows/run-e2e-workflow.yml +++ b/.github/workflows/run-e2e-workflow.yml @@ -56,7 +56,6 @@ jobs: outputs: apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }} test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }} - aab-target-path: ${{ steps.determine-target-paths.outputs.aab-target-path }} env: PREBUILT_IOS_APP_PATH: artifacts/MetaMask.app @@ -131,14 +130,12 @@ jobs: { echo "apk-target-path=android/app/build/outputs/apk/flask/release" echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/flask/release" - echo "aab-target-path=android/app/build/outputs/bundle/flaskRelease" echo "artifact_name=app-flask-release" } >> "$GITHUB_OUTPUT" elif [[ "${{ inputs.build_type }}" == "main" ]]; then { echo "apk-target-path=android/app/build/outputs/apk/prod/release" echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/prod/release" - echo "aab-target-path=android/app/build/outputs/bundle/prodRelease" echo "artifact_name=app-prod-release" } >> "$GITHUB_OUTPUT" else @@ -152,7 +149,6 @@ jobs: echo "🏗 Setting up Android artifacts from build job..." mkdir -p ${{ steps.determine-target-paths.outputs.apk-target-path }} mkdir -p ${{ steps.determine-target-paths.outputs.test-apk-target-path }} - mkdir -p ${{ steps.determine-target-paths.outputs.aab-target-path }} - name: Download Android build artifacts if: ${{ inputs.platform == 'android' }} diff --git a/android/gradle.properties.github b/android/gradle.properties.github index 768591f08511..f3582e40f7c8 100644 --- a/android/gradle.properties.github +++ b/android/gradle.properties.github @@ -1,16 +1,16 @@ # GitHub Actions-specific Gradle settings # Optimized for E2E builds on GitHub Actions runners -# JVM configuration - balanced settings to avoid OOM while maintaining performance -# Using 16GB heap to leave room for parallel workers and native memory -org.gradle.jvmargs=-Xmx16g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:+UseStringDeduplication -XX:+OptimizeStringConcat +# JVM configuration - tuned for 48GB runner to avoid OOM while maintaining performance +# Heap: 12GB to leave room for Node.js/Metro and native memory +# ExitOnOutOfMemoryError: fail-fast on OOM for CI +org.gradle.jvmargs=-Xmx12g -Xms4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=500 -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8 # Enable performance optimizations but limit parallelism to prevent OOM org.gradle.parallel=true -org.gradle.configureondemand=true org.gradle.caching=true -org.gradle.daemon=true -org.gradle.workers.max=6 +org.gradle.daemon=false +org.gradle.workers.max=2 org.gradle.vfs.watch=false # CI-specific optimizations - enabled for GitHub Actions @@ -54,4 +54,4 @@ hermesEnabled=true android.disableResourceValidation=true # Use legacy packaging to compress native libraries in the resulting APK. -expo.useLegacyPackaging=false \ No newline at end of file +expo.useLegacyPackaging=false diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 21428861aea7..8eab51c38968 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -1080,10 +1080,43 @@ const MainNavigator = () => { component={NotificationsModeView} /> - + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} + /> ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} /> { component={BridgeModalStack} options={clearStackNavigatorOptions} /> - + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} + /> { component={DeFiProtocolPositionDetails} options={{ headerShown: true, + animationEnabled: true, + cardStyleInterpolator: ({ current, layouts }) => ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), }} /> { diff --git a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap index bf4ec4e82eae..a113f1d016d2 100644 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap @@ -131,10 +131,22 @@ exports[`MainNavigator matches rendered snapshot 1`] = ` { if (name === 'NftController') { return { - addNft: jest.fn(), + addNfts: jest.fn(), state: {}, }; } @@ -52,7 +52,7 @@ describe('NftDetectionControllerInit', () => { expect(controllerMock).toHaveBeenCalledWith({ messenger: expect.any(Object), disabled: false, - addNft: expect.any(Function), + addNfts: expect.any(Function), getNftState: expect.any(Function), }); }); diff --git a/app/core/Engine/controllers/nft-detection-controller-init.ts b/app/core/Engine/controllers/nft-detection-controller-init.ts index c60a493bb08f..232341dbc367 100644 --- a/app/core/Engine/controllers/nft-detection-controller-init.ts +++ b/app/core/Engine/controllers/nft-detection-controller-init.ts @@ -20,7 +20,7 @@ export const nftDetectionControllerInit: ControllerInitFunction< const controller = new NftDetectionController({ messenger: controllerMessenger, disabled: false, - addNft: nftController.addNft.bind(nftController), + addNfts: nftController.addNfts.bind(nftController), getNftState: () => nftController.state, }); diff --git a/appwright/tests/performance/login/perps-add-funds.spec.js b/appwright/tests/performance/login/perps-add-funds.spec.js new file mode 100644 index 000000000000..a604f901e00e --- /dev/null +++ b/appwright/tests/performance/login/perps-add-funds.spec.js @@ -0,0 +1,68 @@ +import { test } from '../../../fixtures/performance-test.js'; + +import TimerHelper from '../../../utils/TimersHelper.js'; +import LoginScreen from '../../../../wdio/screen-objects/LoginScreen.js'; +import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js'; +import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js'; +import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js'; +import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js'; +import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js'; +import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js'; +import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js'; +import { login } from '../../../utils/Flows.js'; + +async function screensSetup(device) { + const screens = [ + LoginScreen, + WalletMainScreen, + TabBarModal, + WalletActionModal, + PerpsTutorialScreen, + PerpsMarketListView, + PerpsTabView, + PerpsDepositScreen, + ]; + screens.forEach((screen) => { + screen.device = device; + }); +} + +/* Scenario 5: Perps add funds */ +test('Perps add funds', async ({ device, performanceTracker }, testInfo) => { + test.setTimeout(10 * 60 * 1000); // 10 minutes + + const selectPerpsMainScreenTimer = new TimerHelper( + 'Select Perps Main Screen', + ); + const openAddFundsTimer = new TimerHelper('Open Add Funds'); + const getQuoteTimer = new TimerHelper('Get Quote'); + await screensSetup(device); + + await login(device); + await TabBarModal.tapActionButton(); + + // Open Perps Main Screen + selectPerpsMainScreenTimer.start(); + await WalletActionModal.tapPerpsButton(); + selectPerpsMainScreenTimer.stop(); + performanceTracker.addTimer(selectPerpsMainScreenTimer); + + // Skip tutorial + await PerpsTutorialScreen.tapSkip(); + + // Open Add Funds flow + openAddFundsTimer.start(); + await PerpsTutorialScreen.tapAddFunds(); + await PerpsDepositScreen.isAmountInputVisible(); + openAddFundsTimer.stop(); + performanceTracker.addTimer(openAddFundsTimer); + + // Get quote + getQuoteTimer.start(); + await PerpsDepositScreen.fillUsdAmount(5); + await PerpsDepositScreen.isAddFundsVisible(); + await PerpsDepositScreen.isTotalVisible(); + getQuoteTimer.stop(); + performanceTracker.addTimer(getQuoteTimer); + await performanceTracker.attachToTest(testInfo); +}); diff --git a/appwright/tests/performance/login/perps-position-management.spec.js b/appwright/tests/performance/login/perps-position-management.spec.js new file mode 100644 index 000000000000..e98d1cac4e8d --- /dev/null +++ b/appwright/tests/performance/login/perps-position-management.spec.js @@ -0,0 +1,113 @@ +import { test } from '../../../fixtures/performance-test.js'; + +import TimerHelper from '../../../utils/TimersHelper.js'; +import OnboardingSheet from '../../../../wdio/screen-objects/Onboarding/OnboardingSheet.js'; +import CreatePasswordScreen from '../../../../wdio/screen-objects/Onboarding/CreatePasswordScreen.js'; +import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js'; +import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js'; +import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js'; +import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js'; +import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js'; +import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js'; +import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js'; +import PerpsMarketDetailsView from '../../../../wdio/screen-objects/PerpsMarketDetailsView.js'; +import PerpsOrderView from '../../../../wdio/screen-objects/PerpsOrderView.js'; +import PerpsClosePositionView from '../../../../wdio/screen-objects/PerpsClosePositionView.js'; +import PerpsPositionDetailsView from '../../../../wdio/screen-objects/PerpsPositionDetailsView.js'; +import PerpsPositionsView from '../../../../wdio/screen-objects/PerpsPositionsView.js'; +import { login, selectAccountDevice } from '../../../utils/Flows.js'; + +async function screensSetup(device) { + const screens = [ + OnboardingSheet, + CreatePasswordScreen, + WalletMainScreen, + TabBarModal, + WalletActionModal, + PerpsTutorialScreen, + PerpsMarketListView, + PerpsTabView, + PerpsDepositScreen, + PerpsMarketDetailsView, + PerpsOrderView, + PerpsClosePositionView, + PerpsPositionDetailsView, + PerpsPositionsView, + ]; + screens.forEach((screen) => { + screen.device = device; + }); +} + +/* Scenario 5: Perps onboarding + add funds 10 USD ARB.USDC + Open Position + Close Position */ +test('Perps open position and close it', async ({ + device, + performanceTracker, +}, testInfo) => { + test.setTimeout(10 * 60 * 1000); // 10 minutes + + const selectPerpsMainScreenTimer = new TimerHelper( + 'Select Perps Main Screen', + ); + const skipTutorialTimer = new TimerHelper('Skip Tutorial'); + const selectMarketTimer = new TimerHelper('Select Market BTC'); + const openOrderScreenTimer = new TimerHelper('Open Order Screen'); + const openPositionTimer = new TimerHelper('Open Long Position'); + const setLeverageTimer = new TimerHelper('Set Leverage'); + const closePositionTimer = new TimerHelper('Close Position'); + await screensSetup(device); + await login(device); + + // Perps requires independent account for each device to avoid clashes when running tests in parallel + await selectAccountDevice(device, testInfo); + + await TabBarModal.tapActionButton(); + + selectPerpsMainScreenTimer.start(); + await WalletActionModal.tapPerpsButton(); + selectPerpsMainScreenTimer.stop(); + performanceTracker.addTimer(selectPerpsMainScreenTimer); + + // Skip tutorial + skipTutorialTimer.start(); + await PerpsTutorialScreen.tapSkip(); + skipTutorialTimer.stop(); + performanceTracker.addTimer(skipTutorialTimer); + + selectMarketTimer.start(); + // Selecting BTC market + await PerpsMarketListView.selectMarket('BTC'); + selectMarketTimer.stop(); + performanceTracker.addTimer(selectMarketTimer); + + // TODO: Add a check to see if the position is open + // If position open, fail the test + if (await PerpsPositionDetailsView.isPositionOpen()) { + throw new Error('Position is already open'); + } + + // Open Position + openOrderScreenTimer.start(); + await PerpsMarketDetailsView.tapLongButton(); + openOrderScreenTimer.stop(); + performanceTracker.addTimer(openOrderScreenTimer); + + // Set leverage to 40x + setLeverageTimer.start(); + await PerpsOrderView.setLeverage(40); + setLeverageTimer.stop(); + performanceTracker.addTimer(setLeverageTimer); + + openPositionTimer.start(); + await PerpsOrderView.tapPlaceOrder(); + openPositionTimer.stop(); + performanceTracker.addTimer(openPositionTimer); + + // Close Position + closePositionTimer.start(); + await PerpsPositionDetailsView.closePositionWithRetry(); + closePositionTimer.stop(); + performanceTracker.addTimer(closePositionTimer); + + await performanceTracker.attachToTest(testInfo); +}); diff --git a/appwright/tests/performance/onboarding/perps-onboarding.spec.js b/appwright/tests/performance/onboarding/perps-onboarding.spec.js deleted file mode 100644 index 195a3fc34937..000000000000 --- a/appwright/tests/performance/onboarding/perps-onboarding.spec.js +++ /dev/null @@ -1,97 +0,0 @@ -import { test } from '../../../fixtures/performance-test.js'; - -import TimerHelper from '../../../utils/TimersHelper.js'; -import OnboardingSheet from '../../../../wdio/screen-objects/Onboarding/OnboardingSheet.js'; -import ImportFromSeedScreen from '../../../../wdio/screen-objects/Onboarding/ImportFromSeedScreen.js'; -import CreatePasswordScreen from '../../../../wdio/screen-objects/Onboarding/CreatePasswordScreen.js'; -import WalletMainScreen from '../../../../wdio/screen-objects/WalletMainScreen.js'; -import TabBarModal from '../../../../wdio/screen-objects/Modals/TabBarModal.js'; -import WalletActionModal from '../../../../wdio/screen-objects/Modals/WalletActionModal.js'; -import PerpsTutorialScreen from '../../../../wdio/screen-objects/PerpsTutorialScreen.js'; -import PerpsMarketListView from '../../../../wdio/screen-objects/PerpsMarketListView.js'; -import PerpsTabView from '../../../../wdio/screen-objects/PerpsTabView.js'; -import PerpsDepositScreen from '../../../../wdio/screen-objects/PerpsDepositScreen.js'; -import { onboardingFlowImportSRP } from '../../../utils/Flows.js'; - -async function screensSetup(device) { - const screens = [ - OnboardingSheet, - ImportFromSeedScreen, - CreatePasswordScreen, - WalletMainScreen, - TabBarModal, - WalletActionModal, - PerpsTutorialScreen, - PerpsMarketListView, - PerpsTabView, - PerpsDepositScreen, - ]; - screens.forEach((screen) => { - screen.device = device; - }); -} - -/* Scenario 5: Perps onboarding + add funds 10 USD ARB.USDC */ -// TODO: Fix this test: https://consensyssoftware.atlassian.net/browse/MMQA-1190 -test.skip('Perps onboarding + add funds 10 USD ARB.USDC', async ({ - device, - performanceTracker, -}, testInfo) => { - test.setTimeout(10 * 60 * 1000); // 10 minutes - await screensSetup(device); - - await onboardingFlowImportSRP(device, process.env.TEST_SRP_3); - await WalletMainScreen.isTokenVisible('ETH'); - await TabBarModal.tapTradeButton(); - - // Open Perps tab - await TimerHelper.withTimer( - performanceTracker, - 'Open Perps tab', - async () => { - await PerpsTabView.tapPerpsTab(); - await PerpsTutorialScreen.expectFirstScreenVisible(); - }, - ); - // Open Tutorial flow - await PerpsTutorialScreen.flowTapContinueTutorial(6); - - // Open Add Funds flow - await TimerHelper.withTimer( - performanceTracker, - 'Open Add Funds', - async () => { - await PerpsTutorialScreen.tapAddFunds(); - await PerpsDepositScreen.isAmountInputVisible(); - }, - ); - // Select pay token - await TimerHelper.withTimer( - performanceTracker, - 'Select pay token - 1 click USDC.arb', - async () => { - await PerpsDepositScreen.tapPayWith(); - await PerpsDepositScreen.selectPayTokenByText('USDC'); - }, - ); - - // Fill amount - await TimerHelper.withTimer( - performanceTracker, - 'Fill amount - 2 USD', - async () => { - await PerpsDepositScreen.fillUsdAmount('2'); - }, - ); - - // Cancel - await TimerHelper.withTimer( - performanceTracker, - 'Cancel - 1 click', - async () => { - await PerpsDepositScreen.checkTransactionFeeIsVisible(); - }, - ); - - await performanceTracker.attachToTest(testInfo); -}); diff --git a/appwright/utils/Flows.js b/appwright/utils/Flows.js index 1ecd29d43b5b..e1696f31bc28 100644 --- a/appwright/utils/Flows.js +++ b/appwright/utils/Flows.js @@ -19,6 +19,55 @@ import AppwrightGestures from '../../e2e/framework/AppwrightGestures.js'; import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors.js'; import { expect } from 'appwright'; +export async function selectAccountDevice(device, testInfo) { + // Access device name from testInfo.project.use.device + const deviceName = testInfo.project.use.device.name; + console.log(`📱 Device executing the test: ${deviceName}`); + + let accountName; + + // Define account mapping based on device name + // The device names must match those in appwright.config.ts or device-matrix.json + switch (deviceName) { + case 'Samsung Galaxy S23 Ultra': + accountName = 'Account 3'; + break; + case 'Google Pixel 8 Pro': + console.log( + `🔄 Account 1 is selected by default in the app for device: ${deviceName}`, + ); + return; + case 'iPhone 16 Pro Max': + accountName = 'Account 4'; + break; + case 'iPhone 12': + accountName = 'Account 5'; + break; + default: + console.log( + `🔄 Account 1 is selected by default in the app for device: ${deviceName}`, + ); + return; + } + // Account 2 is called stable and not used in this function + + console.log( + `🔄 Switching to account: ${accountName} for device: ${deviceName}`, + ); + + // Set device for screen objects + WalletMainScreen.device = device; + AccountListComponent.device = device; + + // Perform account switch + await WalletMainScreen.tapIdenticon(); + await AccountListComponent.isComponentDisplayed(); + await AccountListComponent.tapOnAccountByName(accountName); + + // Verify we are back on main screen (tapping account usually closes modal) + await WalletMainScreen.isMainWalletViewVisible(); +} + export async function onboardingFlowImportSRP(device, srp) { WelcomeScreen.device = device; TermOfUseScreen.device = device; diff --git a/appwright/utils/TimersHelper.js b/appwright/utils/TimersHelper.js index 44620eea3416..2fb62ff5a82d 100644 --- a/appwright/utils/TimersHelper.js +++ b/appwright/utils/TimersHelper.js @@ -56,22 +56,6 @@ class TimerHelper { get id() { return this._id; } - - // Runs the provided async function while timing it, and automatically - // registers the timer with the given performanceTracker. - // Usage: - // await TimerHelper.withTimer(performanceTracker, 'Step name', async () => { /* ... */ }); - static async withTimer(performanceTracker, id, fn) { - const timer = new TimerHelper(id); - timer.start(); - try { - const result = await fn(); - return result; - } finally { - timer.stop(); - performanceTracker.addTimer(timer); - } - } } export default TimerHelper; diff --git a/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts b/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts index e67d6c89acba..c9c00497e75a 100644 --- a/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts +++ b/e2e/framework/services/providers/browserstack/BrowserStackConfigBuilder.ts @@ -64,6 +64,7 @@ export class BrowserStackConfigBuilder { appProfiling: 'true', selfHeal: 'true', networkProfile: '4g-lte-advanced-good', + geoLocation: 'FR', }, 'appium:autoGrantPermissions': true, 'appium:app': appBsUrl, diff --git a/package.json b/package.json index 45263e0f298a..2288b34da37b 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "@metamask/address-book-controller": "^7.0.0", "@metamask/app-metadata-controller": "^2.0.0", "@metamask/approval-controller": "^8.0.0", - "@metamask/assets-controllers": "^93.0.0", + "@metamask/assets-controllers": "^94.0.0", "@metamask/base-controller": "^9.0.0", "@metamask/bitcoin-wallet-snap": "^1.8.0", "@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A61.0.0#~/.yarn/patches/@metamask-bridge-controller-npm-61.0.0-8c413c463f.patch", diff --git a/scripts/build.sh b/scripts/build.sh index 54827d144511..7c0c54a341fd 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -547,6 +547,8 @@ generateAndroidBinary() { local reactNativeArchitecturesArg="" # Define Test build type arg local testBuildTypeArg="" + # Define Gradle debug flags + local gradleDebugFlags="" # Check if configuration is valid if [ "$configuration" != "Debug" ] && [ "$configuration" != "Release" ] ; then @@ -572,14 +574,19 @@ generateAndroidBinary() { if [ "$METAMASK_ENVIRONMENT" = "e2e" ] ; then # Only build for x86_64 for E2E builds reactNativeArchitecturesArg="-PreactNativeArchitectures=x86_64" + # Enable Gradle debugging flags for E2E builds to investigate Daemon disappearance issues + gradleDebugFlags="--stacktrace --info" + echo "📊 E2E build: Enabling Gradle debugging flags (--stacktrace --info)" fi fi # Generate Android APKs echo "Generating Android binary for ($flavor) flavor with ($configuration) configuration" - ./gradlew $assembleApkTask $assembleTestApkTask $testBuildTypeArg $reactNativeArchitecturesArg + ./gradlew $assembleApkTask $assembleTestApkTask $testBuildTypeArg $reactNativeArchitecturesArg $gradleDebugFlags - if [ "$configuration" = "Release" ] ; then + # Skip AAB bundle for E2E environments - AAB cannot be installed on emulators + # and is only needed for Play Store distribution + if [ "$configuration" = "Release" ] && [ "$METAMASK_ENVIRONMENT" != "e2e" ] ; then # Generate AAB bundle (not needed for E2E) bundleConfiguration="bundle${flavor}Release" echo "Generating AAB bundle for ($flavor) flavor with ($configuration) configuration" diff --git a/wdio/screen-objects/BridgeScreen.js b/wdio/screen-objects/BridgeScreen.js index c29f50ece017..cc47d4da7b34 100644 --- a/wdio/screen-objects/BridgeScreen.js +++ b/wdio/screen-objects/BridgeScreen.js @@ -7,6 +7,7 @@ import { QuoteViewSelectorText } from '../../e2e/selectors/swaps/QuoteView.selec import Selectors from '../helpers/Selectors.js'; import { LoginViewSelectors } from '../../e2e/selectors/wallet/LoginView.selectors'; import { splitAmountIntoDigits } from 'appwright/utils/Utils.js'; +import AmountScreen from './AmountScreen'; class BridgeScreen { @@ -64,28 +65,8 @@ class BridgeScreen { } async enterSourceTokenAmount(amount) { - // Split amount into digits - const digits = splitAmountIntoDigits(amount); - console.log('Amount digits:', digits); - for (const digit of digits) { - if (AppwrightSelectors.isAndroid(this._device)) { - if (digit != '.') { - const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//android.widget.Button[@content-desc='${digit}']`) - await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 }); - await AppwrightGestures.tap(numberKey); - } - else { - const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//android.view.View[@text="."]`); - await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 }); - await AppwrightGestures.tap(numberKey); - } - } - else { - const numberKey = await AppwrightSelectors.getElementByXpath(this._device, `//XCUIElementTypeButton[@name="${digit}"]`); - await appwrightExpect(numberKey).toBeVisible({ timeout: 30000 }); - await AppwrightGestures.tap(numberKey); - } - } + AmountScreen.device = this._device; + await AmountScreen.enterAmount(amount); } async selectNetworkAndTokenTo(network, token) { diff --git a/wdio/screen-objects/PerpsClosePositionView.js b/wdio/screen-objects/PerpsClosePositionView.js new file mode 100644 index 000000000000..bcbe90ba097f --- /dev/null +++ b/wdio/screen-objects/PerpsClosePositionView.js @@ -0,0 +1,24 @@ +import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; +import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; + +class PerpsClosePositionView { + get device() { + return this._device; + } + + set device(device) { + this._device = device; + } + + get confirmButton() { + return AppwrightSelectors.getElementByID(this._device, 'close-position-confirm-button'); + } + + async tapConfirmButton() { + await AppwrightGestures.tap(this.confirmButton); + } +} + +export default new PerpsClosePositionView(); + + diff --git a/wdio/screen-objects/PerpsDepositScreen.js b/wdio/screen-objects/PerpsDepositScreen.js index 8edeb2180df0..881ae6a1c177 100644 --- a/wdio/screen-objects/PerpsDepositScreen.js +++ b/wdio/screen-objects/PerpsDepositScreen.js @@ -1,7 +1,7 @@ import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; import AmountScreen from './AmountScreen'; -import { expect } from 'appwright'; +import { expect as appwrightExpect } from 'appwright'; class PerpsDepositScreen { @@ -26,6 +26,10 @@ class PerpsDepositScreen { return AppwrightSelectors.getElementByID(this._device, 'custom-amount-input'); } + get backButton() { + return AppwrightSelectors.getElementByID(this._device, 'Add funds-navbar-back-button'); + } + get payWithButton() { return AppwrightSelectors.getElementByCatchAll( this._device, @@ -33,9 +37,17 @@ class PerpsDepositScreen { ); } + get addFundsButton() { + return AppwrightSelectors.getElementByText(this._device, 'Add funds'); + } + + get totalText() { + return AppwrightSelectors.getElementByText(this._device, 'Total'); + } + async isAmountInputVisible() { const input = await this.amountInput; - await input.isVisible({ timeout: 15000 }); + await appwrightExpect(input).toBeVisible(); } async selectPayTokenByText(token) { @@ -61,9 +73,23 @@ class PerpsDepositScreen { await AppwrightGestures.tap(this.cancelButton); // Use static tap method with retry logic } + async tapBackButton() { + await AppwrightGestures.tap(this.backButton); // Use static tap method with retry logic + } + async checkTransactionFeeIsVisible() { const transactionFee = await AppwrightSelectors.getElementByID(this._device, 'bridge-fee-row'); - await expect(transactionFee).toBeVisible(); + await appwrightExpect(transactionFee).toBeVisible(); + } + + async isAddFundsVisible() { + const addFunds = await this.addFundsButton; + await appwrightExpect(addFunds).toBeVisible(); + } + + async isTotalVisible() { + const total = await AppwrightSelectors.getElementByText(this._device, 'Total'); + await appwrightExpect(total).toBeVisible(); } } diff --git a/wdio/screen-objects/PerpsMarketDetailsView.js b/wdio/screen-objects/PerpsMarketDetailsView.js new file mode 100644 index 000000000000..f3f85ef828b1 --- /dev/null +++ b/wdio/screen-objects/PerpsMarketDetailsView.js @@ -0,0 +1,31 @@ +import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; +import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; + +class PerpsMarketDetailsView { + get device() { + return this._device; + } + + set device(device) { + this._device = device; + } + + get longButton() { + return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-long-button'); + } + + get shortButton() { + return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-short-button'); + } + + async tapLongButton() { + await AppwrightGestures.tap(this.longButton); + } + + async tapShortButton() { + await AppwrightGestures.tap(this.shortButton); + } +} + +export default new PerpsMarketDetailsView(); + diff --git a/wdio/screen-objects/PerpsMarketListView.js b/wdio/screen-objects/PerpsMarketListView.js index 9ce4137e1d14..4b5d92b47236 100644 --- a/wdio/screen-objects/PerpsMarketListView.js +++ b/wdio/screen-objects/PerpsMarketListView.js @@ -1,5 +1,6 @@ import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; +import { expect as appwrightExpect } from 'appwright'; class PerpsMarketListView { @@ -22,15 +23,18 @@ class PerpsMarketListView { async isHeaderVisible() { const header = await this.listHeader; - await header.isVisible({ timeout: 10000 }); + await appwrightExpect(header).toBeVisible({ timeout: 10000 }); } async tapBackButtonMarketList() { await AppwrightGestures.tap(this.backButtonMarketList); // Use static tap method with retry logic } + + async selectMarket(symbol) { + // ID format from Perps.selectors.ts: `perps-market-row-item-${symbol}` + const marketRow = await AppwrightSelectors.getElementByID(this._device, `perps-market-row-item-${symbol}`); + await AppwrightGestures.tap(marketRow); + } } export default new PerpsMarketListView(); - - - diff --git a/wdio/screen-objects/PerpsOrderView.js b/wdio/screen-objects/PerpsOrderView.js new file mode 100644 index 000000000000..5df4e2eec74c --- /dev/null +++ b/wdio/screen-objects/PerpsOrderView.js @@ -0,0 +1,64 @@ +import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; +import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; +import AmountScreen from './AmountScreen'; +import { expect as appwrightExpect } from 'appwright'; +import { splitAmountIntoDigits } from 'appwright/utils/Utils'; +import PerpsPositionDetailsView from './PerpsPositionDetailsView'; + +class PerpsOrderView { + get device() { + return this._device; + } + + set device(device) { + this._device = device; + } + + get placeOrderButton() { + return AppwrightSelectors.getElementByID(this._device, 'perps-order-view-place-order-button'); + } + + get keypad() { + return AppwrightSelectors.getElementByID(this._device, 'perps-order-view-keypad'); + } + + get leverageButton() { + return AppwrightSelectors.getElementByText(this._device, 'Leverage'); + } + + async leverageOption(leverage) { + return AppwrightSelectors.getElementByText(this._device, `${leverage}x`); + } + + async confirmLeverageButton(leverage) { + return AppwrightSelectors.getElementByText(this._device, `Set ${leverage}x`); + } + + async tapPlaceOrder() { + await AppwrightGestures.tap(this.placeOrderButton); + appwrightExpect(await PerpsPositionDetailsView.isPositionOpen()).toBe(true); + } + + // Reuse logic from AmountScreen.js for Keypad interaction + async tapNumberKey(digit) { + AmountScreen.device = this._device; + await AmountScreen.tapNumberKey(digit); + } + + async enterAmount(text) { + // Since PerpsOrderView likely only supports keypad input for amount in the UI flow being tested + const digits = splitAmountIntoDigits(text); + for (const digit of digits) { + console.log('Tapping digit:', digit); + await this.tapNumberKey(digit); + } + } + + async setLeverage(leverage) { + await AppwrightGestures.tap(this.leverageButton); + await AppwrightGestures.tap(await this.leverageOption(leverage)); + await AppwrightGestures.tap(await this.confirmLeverageButton(leverage)); + } +} + +export default new PerpsOrderView(); \ No newline at end of file diff --git a/wdio/screen-objects/PerpsPositionDetailsView.js b/wdio/screen-objects/PerpsPositionDetailsView.js new file mode 100644 index 000000000000..07fdc3b2cce4 --- /dev/null +++ b/wdio/screen-objects/PerpsPositionDetailsView.js @@ -0,0 +1,56 @@ +import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; +import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; +import Utilities from '../../e2e/framework/Utilities'; + +class PerpsPositionDetailsView { + get device() { + return this._device; + } + + set device(device) { + this._device = device; + } + + get closePositionButton() { + return AppwrightSelectors.getElementByID(this._device, 'perps-market-details-close-button'); + } + + get positionOpenButton() { + return AppwrightSelectors.getElementByID(this._device, 'position-open-button'); + } + + get confirmClosePositionButton() { + return AppwrightSelectors.getElementByID(this._device, 'close-position-confirm-button'); + } + + async tapClosePositionButton() { + await AppwrightGestures.tap(this.closePositionButton); + await AppwrightGestures.tap(this.confirmClosePositionButton); + } + + async isPositionOpen() { + const closePositionButton = await this.closePositionButton; + return await closePositionButton.isVisible(); + } + + async closePositionWithRetry() { + await Utilities.executeWithRetry(async () => { + if (await this.isPositionOpen()) { + await this.tapClosePositionButton(); + const closePositionButton = await this.closePositionButton; + await AppwrightSelectors.waitForElementToDisappear( + closePositionButton, + 'Close Position Button', + 5000, + ); + } + }, { + description: 'close position', + elemDescription: 'Close Position Button', + }); + } +} + +export default new PerpsPositionDetailsView(); + + diff --git a/wdio/screen-objects/PerpsPositionsView.js b/wdio/screen-objects/PerpsPositionsView.js new file mode 100644 index 000000000000..b9454c4dd65b --- /dev/null +++ b/wdio/screen-objects/PerpsPositionsView.js @@ -0,0 +1,23 @@ +import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; +import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; + +class PerpsPositionsView { + get device() { + return this._device; + } + + set device(device) { + this._device = device; + } + + get positionItem() { + return AppwrightSelectors.getElementByID(this._device, 'perps-positions-item'); + } + + async tapPositionItem() { + await AppwrightGestures.tap(this.positionItem); + } +} + +export default new PerpsPositionsView(); + diff --git a/wdio/screen-objects/PerpsTabView.js b/wdio/screen-objects/PerpsTabView.js index 27e5547167e7..7ecbda41d3dc 100644 --- a/wdio/screen-objects/PerpsTabView.js +++ b/wdio/screen-objects/PerpsTabView.js @@ -1,5 +1,6 @@ import AppwrightSelectors from '../../e2e/framework/AppwrightSelectors'; import AppwrightGestures from '../../e2e/framework/AppwrightGestures'; +import { expect as appwrightExpect } from 'appwright'; class PerpsTabView { @@ -13,7 +14,7 @@ class PerpsTabView { } get perpsTabButton() { - return AppwrightSelectors.getElementByID(this._device, 'wallet-perps-action'); + return AppwrightSelectors.getElementByID(this._device, 'undefined-tab-1'); } get addFundsButton() { @@ -24,17 +25,25 @@ class PerpsTabView { return AppwrightSelectors.getElementByID(this._device, 'perps-start-trading-button'); } + get startTradingButton() { + return AppwrightSelectors.getElementByText(this._device, 'Start trading'); + } + async tapPerpsTab() { await AppwrightGestures.tap(this.perpsTabButton); // Use static tap method with retry logic } + async tapStartTradingButton() { + await AppwrightGestures.tap(this.startTradingButton); // Use static tap method with retry logic + } + async tapAddFunds() { await AppwrightGestures.tap(this.addFundsButton); // Use static tap method with retry logic } async tapOnboardingButton() { const button = await this.onboardingButton; - await button.isVisible({ timeout: 5000 }); + await appwrightExpect(button).toBeVisible({ timeout: 5000 }); await AppwrightGestures.tap(this.onboardingButton); // Use static tap method with retry logic } } diff --git a/yarn.lock b/yarn.lock index 7ff52b87dc71..fb0f467e13a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7132,7 +7132,7 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^93.0.0, @metamask/assets-controllers@npm:^93.1.0": +"@metamask/assets-controllers@npm:^93.1.0": version: 93.1.0 resolution: "@metamask/assets-controllers@npm:93.1.0" dependencies: @@ -7186,6 +7186,60 @@ __metadata: languageName: node linkType: hard +"@metamask/assets-controllers@npm:^94.0.0": + version: 94.0.0 + resolution: "@metamask/assets-controllers@npm:94.0.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^4.0.0" + "@metamask/accounts-controller": "npm:^35.0.0" + "@metamask/approval-controller": "npm:^8.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.16.0" + "@metamask/core-backend": "npm:^5.0.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^21.0.0" + "@metamask/keyring-controller": "npm:^25.0.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^4.0.1" + "@metamask/network-controller": "npm:^27.0.0" + "@metamask/permission-controller": "npm:^12.1.1" + "@metamask/phishing-controller": "npm:^16.1.0" + "@metamask/polling-controller": "npm:^16.0.0" + "@metamask/preferences-controller": "npm:^22.0.0" + "@metamask/profile-sync-controller": "npm:^27.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^14.0.1" + "@metamask/snaps-sdk": "npm:^9.0.0" + "@metamask/snaps-utils": "npm:^11.0.0" + "@metamask/transaction-controller": "npm:^62.6.0" + "@metamask/utils": "npm:^11.8.1" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/86324e75db4adffbfc7c4f93138de25242360578e3aa0fd26f78ef84d4390fb04042cb1582d64139754de60f315a9b8a8458850c65b0b764b95eb6435f3bb054 + languageName: node + linkType: hard + "@metamask/auth-network-utils@npm:^0.3.0": version: 0.3.1 resolution: "@metamask/auth-network-utils@npm:0.3.1" @@ -8394,19 +8448,23 @@ __metadata: languageName: node linkType: hard -"@metamask/multichain-account-service@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/multichain-account-service@npm:4.0.0" +"@metamask/multichain-account-service@npm:^4.0.0, @metamask/multichain-account-service@npm:^4.0.1": + version: 4.0.1 + resolution: "@metamask/multichain-account-service@npm:4.0.1" dependencies: "@ethereumjs/util": "npm:^9.1.0" + "@metamask/accounts-controller": "npm:^35.0.0" "@metamask/base-controller": "npm:^9.0.0" + "@metamask/error-reporting-service": "npm:^3.0.0" "@metamask/eth-snap-keyring": "npm:^18.0.0" "@metamask/key-tree": "npm:^10.1.1" "@metamask/keyring-api": "npm:^21.0.0" + "@metamask/keyring-controller": "npm:^25.0.0" "@metamask/keyring-internal-api": "npm:^9.0.0" "@metamask/keyring-snap-client": "npm:^8.0.0" "@metamask/keyring-utils": "npm:^3.1.0" "@metamask/messenger": "npm:^0.3.0" + "@metamask/snaps-controllers": "npm:^14.0.1" "@metamask/snaps-sdk": "npm:^9.0.0" "@metamask/snaps-utils": "npm:^11.0.0" "@metamask/superstruct": "npm:^3.1.0" @@ -8414,13 +8472,9 @@ __metadata: async-mutex: "npm:^0.5.0" peerDependencies: "@metamask/account-api": ^0.12.0 - "@metamask/accounts-controller": ^35.0.0 - "@metamask/error-reporting-service": ^3.0.0 - "@metamask/keyring-controller": ^25.0.0 "@metamask/providers": ^22.0.0 - "@metamask/snaps-controllers": ^14.0.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/b5e5cb6f7d4a8e077935a2a47e230f788ada79cc25829c781e3a26f9b80acaa93980f66bb9d931498400ae3873882e2040066cc83bdea36735029dacb39ad7db + checksum: 10/a664bed3b1f54c27c26f0eec2e07b666dbc09d80fb6cad6f081fecc40b6029971988cad0a9cc010ce97fea83b962d31809aae21e37792c19e94dce509eeb98e2 languageName: node linkType: hard @@ -34239,7 +34293,7 @@ __metadata: "@metamask/address-book-controller": "npm:^7.0.0" "@metamask/app-metadata-controller": "npm:^2.0.0" "@metamask/approval-controller": "npm:^8.0.0" - "@metamask/assets-controllers": "npm:^93.0.0" + "@metamask/assets-controllers": "npm:^94.0.0" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/bitcoin-wallet-snap": "npm:^1.8.0"