From d9f11c67373425f6d31bb81e05f8fabbbf8bcffd Mon Sep 17 00:00:00 2001 From: Juanmi <95381763+juanmigdr@users.noreply.github.com> Date: Fri, 12 Dec 2025 09:37:47 +0100 Subject: [PATCH 1/6] fix: O(n) API calls to bulk-scan (#23803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** After a thorough profiling investigation, one of the issues I found is that we are making O(n) API calls to the `bulk-scan` endpoint when NFT auto-detection was being triggered, I have modified the code so that we make 1 single API call per 250 urls. image Core changes: https://github.com/MetaMask/core/pull/7411 ## **Changelog** CHANGELOG entry: reduced number of calls to bulk-scan for NFT detection ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-2068 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/d0232dec-b144-4f27-b085-0de417a9af20 image ### **After** https://github.com/user-attachments/assets/053bca9b-9ee8-439b-bec7-4381fc6bc0b3 image ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Switch NFT detection controller init from `addNft` to bulk `addNfts` and upgrade `@metamask/assets-controllers` to ^94.0.0. > > - **Engine**: > - Update `app/core/Engine/controllers/nft-detection-controller-init.ts` to bind `addNfts` instead of `addNft` when constructing `NftDetectionController`. > - Adjust tests in `app/core/Engine/controllers/nft-detection-controller-init.test.ts` to expect `addNfts`. > - **Dependencies**: > - Bump `@metamask/assets-controllers` to `^94.0.0` (with corresponding lockfile updates). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8f4a00412d1841625b4b41f84f5b31b7bb3b1d67. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../nft-detection-controller-init.test.ts | 4 +- .../nft-detection-controller-init.ts | 2 +- package.json | 2 +- yarn.lock | 74 ++++++++++++++++--- 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/app/core/Engine/controllers/nft-detection-controller-init.test.ts b/app/core/Engine/controllers/nft-detection-controller-init.test.ts index 1838c760c27..e940c112427 100644 --- a/app/core/Engine/controllers/nft-detection-controller-init.test.ts +++ b/app/core/Engine/controllers/nft-detection-controller-init.test.ts @@ -28,7 +28,7 @@ function getInitRequestMock(): jest.Mocked< requestMock.getController.mockImplementation((name: string) => { 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 c60a493bb08..232341dbc36 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/package.json b/package.json index 45263e0f298..2288b34da37 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/yarn.lock b/yarn.lock index 7ff52b87dc7..fb0f467e13a 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" From 1387d2b360a42597b65fdb270350af3859ec8807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20=C5=81ucka?= <5708018+PatrykLucka@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:24:57 +0100 Subject: [PATCH 2/6] feat: update transition of DeFiProtocolPositionDetails screen (#23911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The DeFi detail page (DeFiProtocolPositionDetails) was transitioning into the viewport like a bottom sheet (sliding up from bottom) due to the parent Stack.Navigator using mode={'modal'}. This PR changes the transition to slide in from right to left (standard push navigation) by adding a custom cardStyleInterpolator with horizontal translation, matching the pattern used by other screens like TrendingTokensFullView, ExploreSearchScreen, and PerpsScreenStack. ## **Changelog** CHANGELOG entry: Changed DeFi protocol detail page to slide in from right instead of from bottom ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-263 ## **Manual testing steps** ```gherkin Feature: DeFi Protocol Position Details Navigation Scenario: user opens DeFi protocol position details Given user is on the Wallet view with DeFi positions visible When user taps on a DeFi protocol position Then the DeFi Protocol Position Details screen slides in from right to left And user can swipe from left edge to go back (gesture navigation) ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/f1437617-fac2-4fbb-aa97-6c9a18a03272 ### **After** https://github.com/user-attachments/assets/2521f279-8fd3-4bd1-a49a-c3270e511a70 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Switches `DeFiProtocolPositionDetails` to a right-to-left slide transition via custom `cardStyleInterpolator`, updating snapshots accordingly. > > - **Navigation** > - `app/components/Nav/Main/MainNavigator.js` > - `DeFiProtocolPositionDetails`: enable `animationEnabled` and add horizontal `cardStyleInterpolator` (slides in from right), with `headerShown: true`. > - **Tests** > - `app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap`: update snapshot to include new screen options. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a7a46fadde7427bd545bc114c37aa966127589c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- app/components/Nav/Main/MainNavigator.js | 13 +++++++++++++ .../Main/__snapshots__/MainNavigator.test.tsx.snap | 2 ++ 2 files changed, 15 insertions(+) diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 21428861aea..96a6bb4487a 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -1257,6 +1257,19 @@ const MainNavigator = () => { 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 bf4ec4e82ea..4e1ad2ac041 100644 --- a/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap +++ b/app/components/Nav/Main/__snapshots__/MainNavigator.test.tsx.snap @@ -265,6 +265,8 @@ exports[`MainNavigator matches rendered snapshot 1`] = ` name="DeFiProtocolPositionDetails" options={ { + "animationEnabled": true, + "cardStyleInterpolator": [Function], "headerShown": true, } } From 0f86a83ca9455fbd97e03395651c6898b9a1c27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20=C5=81ucka?= <5708018+PatrykLucka@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:25:02 +0100 Subject: [PATCH 3/6] feat: Update NFT details screen transitions (#23912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The NFT details pages (NftDetails and NftDetailsFullImage) were transitioning into the viewport like a bottom sheet (sliding up from bottom) due to the parent Stack.Navigator using mode={'modal'}. This PR changes the transition to slide in from right to left (standard push navigation) by adding a custom cardStyleInterpolator with horizontal translation, matching the pattern used by other screens like TrendingTokensFullView, ExploreSearchScreen, and PerpsScreenStack. ## **Changelog** CHANGELOG entry: Updated NFT details pages to slide in from right instead of from bottom ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-271 ## **Manual testing steps** ```gherkin Feature: NFT Details Navigation Scenario: user opens NFT details Given user is on the Wallet view with NFTs visible When user taps on an NFT Then the NFT Details screen slides in from right to left And user can swipe from left edge to go back (gesture navigation) Scenario: user opens NFT full image Given user is viewing NFT details When user taps on the NFT image to view full screen Then the NFT Full Image screen slides in from right to left And user can swipe from left edge to go back (gesture navigation) ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/80629490-2cff-4080-8feb-35d2929dfafa ### **After** https://github.com/user-attachments/assets/8a635fac-4e98-42fe-b251-5b2880072c6b ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Switch NFT details screens to a right-to-left push animation with a custom cardStyleInterpolator. > > - **Navigation (MainNavigator.js)**: > - Add `options` to `NftDetails` and `NftDetailsFullImage` to enable horizontal slide transition (`animationEnabled: true` + custom `cardStyleInterpolator`). > - **Tests**: > - Update snapshot to reflect new `options` on `NftDetails` and `NftDetailsFullImage`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d47ecf951dbbb2fe7e82953c1f42321314b96585. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- app/components/Nav/Main/MainNavigator.js | 35 ++++++++++++++++++- .../__snapshots__/MainNavigator.test.tsx.snap | 12 +++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 96a6bb4487a..a71c31b7200 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], + }), + }, + ], + }, + }), + }} /> Date: Fri, 12 Dec 2025 11:25:22 +0100 Subject: [PATCH 4/6] feat: Update StakeScreens with custom navigation options (#23913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When selecting "Deposit" from the Trade Menu, the Deposit page was sliding in from the bottom (default modal behavior). Per design requirements, it should slide in from the right, consistent with other navigation flows. Applied the same cardStyleInterpolator pattern already used by: - Settings (Routes.SETTINGS_VIEW) - TrendingTokensFullView - ExploreSearchScreen - SitesFullView - Perps ## **Changelog** CHANGELOG entry: Updated Deposit page transition to slide in from the right instead of bottom ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-235 ## **Manual testing steps** ```gherkin Feature: Deposit page navigation transition Scenario: user opens Deposit page from Trade Menu Given the user is on the Wallet home screen And the user has Deposit enabled When user taps on the Trade/Fund action button And user selects "Deposit" from the menu Then the Deposit page slides in from the right side of the screen And user can swipe from left edge to go back ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/fba1e102-92cc-4cf5-8bb5-d94bedbb48a4 ### **After** https://github.com/user-attachments/assets/d18e1e5b-5b89-451d-87d5-b18446180cc9 ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Configures `StakeScreens` to hide the header and use a right-to-left slide transition; updates snapshot accordingly. > > - **Navigation**: > - **`MainNavigator.js`**: Add custom options to `Stack.Screen` for `StakeScreens`: > - `headerShown: false` > - Enable right-to-left slide via `animationEnabled: true` and custom `cardStyleInterpolator`. > - **Tests**: > - Update snapshot `MainNavigator.test.tsx.snap` to reflect new `StakeScreens` options. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f185550c377ae047af18ad088d2bc35c00ff99d5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- app/components/Nav/Main/MainNavigator.js | 21 ++++++++++++++++++- .../__snapshots__/MainNavigator.test.tsx.snap | 7 +++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index a71c31b7200..8eab51c3896 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -1141,7 +1141,26 @@ const MainNavigator = () => { component={BridgeModalStack} options={clearStackNavigatorOptions} /> - + ({ + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }), + }} + /> Date: Fri, 12 Dec 2025 12:09:38 +0100 Subject: [PATCH 5/6] fix: perf test perps (#23863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix perps performance e2e tests. Tasks done: - Updated tests according UI changes - Remove dead code - Unified numeric keyboard component for different flows - Added a patch in appwright to set geoLocation in BrowserStack and avoid perps blocks -> from main finally ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > Updates perps performance tests to new UI, adds open/close position flow, refactors numeric input via shared AmountScreen, and sets BrowserStack geoLocation to FR. > > - **Tests (performance)**: > - Add `appwright/tests/performance/login/perps-add-funds.spec.js` (add funds flow + quote timings). > - Add `appwright/tests/performance/login/perps-position-management.spec.js` (select market, set leverage, place order, close position with retry, per-device account selection). > - Remove outdated `perps-onboarding.spec.js`. > - **Screen Objects (Perps)**: > - New: `PerpsMarketDetailsView`, `PerpsOrderView` (leverage, keypad, place order), `PerpsPositionDetailsView` (close with retry, state check), `PerpsPositionsView`, `PerpsClosePositionView`. > - Update `PerpsDepositScreen`: new getters (`backButton`, `addFundsButton`, `totalText`), visibility checks, `tapBackButton`, `isAddFundsVisible`, `isTotalVisible`. > - Update `PerpsMarketListView`: header expect, `selectMarket(symbol)`. > - Update `PerpsTabView`: new tab id, add `startTrading` action, expect usage. > - **Utilities/Infra**: > - Add `selectAccountDevice` in `Flows.js` (map device to account); adjust flows usage. > - Remove `TimerHelper.withTimer` helper. > - Set BrowserStack `geoLocation: 'FR'`. > - `BridgeScreen.enterSourceTokenAmount` now delegates to `AmountScreen`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eb2abebca82b9c97ffb833394e5700e158d897fd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../performance/login/perps-add-funds.spec.js | 68 +++++++++++ .../login/perps-position-management.spec.js | 113 ++++++++++++++++++ .../onboarding/perps-onboarding.spec.js | 97 --------------- appwright/utils/Flows.js | 49 ++++++++ appwright/utils/TimersHelper.js | 16 --- .../browserstack/BrowserStackConfigBuilder.ts | 1 + wdio/screen-objects/BridgeScreen.js | 25 +--- wdio/screen-objects/PerpsClosePositionView.js | 24 ++++ wdio/screen-objects/PerpsDepositScreen.js | 32 ++++- wdio/screen-objects/PerpsMarketDetailsView.js | 31 +++++ wdio/screen-objects/PerpsMarketListView.js | 12 +- wdio/screen-objects/PerpsOrderView.js | 64 ++++++++++ .../PerpsPositionDetailsView.js | 56 +++++++++ wdio/screen-objects/PerpsPositionsView.js | 23 ++++ wdio/screen-objects/PerpsTabView.js | 13 +- 15 files changed, 480 insertions(+), 144 deletions(-) create mode 100644 appwright/tests/performance/login/perps-add-funds.spec.js create mode 100644 appwright/tests/performance/login/perps-position-management.spec.js delete mode 100644 appwright/tests/performance/onboarding/perps-onboarding.spec.js create mode 100644 wdio/screen-objects/PerpsClosePositionView.js create mode 100644 wdio/screen-objects/PerpsMarketDetailsView.js create mode 100644 wdio/screen-objects/PerpsOrderView.js create mode 100644 wdio/screen-objects/PerpsPositionDetailsView.js create mode 100644 wdio/screen-objects/PerpsPositionsView.js 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 00000000000..a604f901e00 --- /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 00000000000..e98d1cac4e8 --- /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 195a3fc3493..00000000000 --- 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 1ecd29d43b5..e1696f31bc2 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 44620eea341..2fb62ff5a82 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 e67d6c89acb..c9c00497e75 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/wdio/screen-objects/BridgeScreen.js b/wdio/screen-objects/BridgeScreen.js index c29f50ece01..cc47d4da7b3 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 00000000000..bcbe90ba097 --- /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 8edeb2180df..881ae6a1c17 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 00000000000..f3f85ef828b --- /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 9ce4137e1d1..4b5d92b4723 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 00000000000..5df4e2eec74 --- /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 00000000000..07fdc3b2cce --- /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 00000000000..b9454c4dd65 --- /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 27e5547167e..7ecbda41d3d 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 } } From 28a08fe35ec7ecb755403889c0e62a67ed6ec437 Mon Sep 17 00:00:00 2001 From: Jorge Carrasco Date: Fri, 12 Dec 2025 12:30:04 +0100 Subject: [PATCH 6/6] chore(infra): optimize Android E2E build for lg runner (16 vCPUs, 48GB) (#23869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR optimizes Android E2E builds to prevent "Gradle Daemon disappeared unexpectedly" crashes on the LG runner (16 vCPUs, 48GB RAM). ### Root Cause Analysis The "Daemon disappeared" symptom is consistent with **process termination under memory pressure** (e.g., OS OOM kill). On **lg (48GB)**, the previous GitHub Actions Gradle config (`-Xmx16g`, `workers.max=6`, `daemon=true`) could overlap with Node/Metro memory usage and native compilation spikes. ### Solution: Optimized Gradle Memory Settings Following [Gradle 8.10.2 Performance Best Practices](https://docs.gradle.org/8.10.2/userguide/performance.html), we tuned `gradle.properties.github` for the 48GB runner. #### Improved Gradle Logging for E2E - E2E builds now run Gradle with **`--stacktrace --info`** via `scripts/build.sh` to provide more actionable logs when the build fails. - Android CI uses **JDK 17** via `setup-e2e-env` defaults: [setup-e2e-env action](https://github.com/MetaMask/github-tools/blob/v1/.github/actions/setup-e2e-env/action.yml) #### JVM Memory Changes | Setting | Before | After | Reason | |---------|--------|-------|--------| | **Heap (`-Xmx`)** | 16GB | **12GB** | Leave room for Node.js/Metro | | **Initial Heap (`-Xms`)** | (none) | **4GB** | Set JVM initial heap to reduce early heap resizing: [Java launcher docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html) | | **MaxGCPauseMillis** | (none) | **500ms** | Tune G1 pause-time goal for CI throughput: [G1 GC tuning guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html) | | **ExitOnOutOfMemoryError** | (none) | **enabled** | Fail fast on JVM OOM: [Java launcher docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html) | | **file.encoding** | (none) | **UTF-8** | Pin JVM default charset (JDK 17 default can depend on OS/locale): [Charset.defaultCharset()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/charset/Charset.html#defaultCharset()) | | **OptimizeStringConcat** | enabled | **removed** | Remove non-standard `-XX` tuning flag from baseline config (prefer defaults): [Java launcher docs](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html) | #### Gradle Settings Changes | Setting | Before | After | Reason | |---------|--------|-------|--------| | **Workers** | 6 | **2** | Prevent memory contention | | **Daemon** | true | **false** | Disable persistent daemon between CI builds: [Gradle Daemon](https://docs.gradle.org/8.10.2/userguide/gradle_daemon.html#sec:disabling_the_daemon) | | **configureondemand** | true | **removed** | Not recommended for modern Gradle + Android builds: [Gradle performance guide](https://docs.gradle.org/8.10.2/userguide/performance.html); [Android/AGP compatibility](https://stackoverflow.com/questions/49990933/configuration-on-demand-is-not-supported-by-the-current-version-of-the-android-g) | #### Unchanged Settings (kept as-is) | Setting | Value | Why Kept | |---------|-------|----------| | `parallel` | true | [Recommended for multi-project builds](https://docs.gradle.org/8.10.2/userguide/performance.html#parallel_execution) | | `caching` | true | [Recommended build cache usage](https://docs.gradle.org/8.10.2/userguide/build_cache.html) | | `vfs.watch` | false | Already disabled in baseline config; we keep it disabled for CI | | `MaxMetaspaceSize` | 1g | Kept from baseline config | | `UseG1GC` | enabled | Kept from baseline config | | `G1HeapRegionSize` | 16m | Kept from baseline config | | `UseStringDeduplication` | enabled | Kept from baseline config | ### Memory Budget (48GB Runner) > Note: rough budgeting (actual usage varies by task and input changes) ``` ┌─────────────────────────────────────────────────────────┐ │ Component │ Allocation │ ├─────────────────────────┼───────────────────────────────┤ │ Gradle Heap │ 12GB │ │ Gradle Metaspace │ 1GB │ │ Node.js (Metro) │ 8GB (--max-old-space-size) │ │ OS + System + native │ remainder │ └─────────────────────────┴───────────────────────────────┘ ``` ### Additional Optimizations - **Skip AAB bundle for E2E** - E2E tests only use APK files. - **Removed AAB references** from the E2E build workflow. - **Runner moved from xl → lg** for this workflow after tuning: lg (48GB) is sufficient. ### Documentation References **Gradle 8.10.2:** - [Performance Guide](https://docs.gradle.org/8.10.2/userguide/performance.html) - [Gradle Daemon](https://docs.gradle.org/8.10.2/userguide/gradle_daemon.html) - [Build Cache](https://docs.gradle.org/8.10.2/userguide/build_cache.html) **Java 17:** - [Java launcher docs (heap, -X/-XX, OOM behavior)](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html) - [Charset.defaultCharset()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/charset/Charset.html#defaultCharset()) - [G1 GC tuning guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html) ### Official Runner Specs ([source](https://cirrus-runners.app/setup/#__tabbed_3_2)) | Runner | vCPUs | RAM | Disk | |--------|-------|-----|------| | **lg** | 16 | 48 GB | 200 GB | | xl | 32 | 96 GB | 400 GB | ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [INFRA-3174](https://consensyssoftware.atlassian.net/browse/INFRA-3174) ## **Manual testing steps** ```gherkin Feature: Android E2E Build Optimization Scenario: Build completes without daemon disappearance Given the PR uses tuned Gradle memory settings And Gradle runs with --stacktrace --info for E2E When the Android E2E build workflow runs Then the build completes without the "Daemon disappeared" failure And APK artifacts are uploaded successfully ``` ## **Screenshots/Recordings** ### **Before** Build failing with: ``` Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed) ``` ### **After** Build results will be visible in PR checks after workflow runs. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [INFRA-3174]: https://consensyssoftware.atlassian.net/browse/INFRA-3174?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .github/workflows/build-android-e2e.yml | 21 +-------------------- .github/workflows/run-e2e-workflow.yml | 4 ---- android/gradle.properties.github | 14 +++++++------- scripts/build.sh | 11 +++++++++-- 4 files changed, 17 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml index 35d674bcee5..7019ff20104 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 fcc4a9e05e6..6ec79db1012 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 768591f0851..f3582e40f7c 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/scripts/build.sh b/scripts/build.sh index 54827d14451..7c0c54a341f 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"