⚠️ IMPORTANT: E2E Tests Should Be Your Last ResortBefore adding E2E tests, ensure that unit tests and integration tests cannot adequately cover the functionallity to check.
E2E tests are significantly slower, more brittle, and resource-intensive than unit and integration tests. Always prioritize unit and integration tests over E2E ones.
Our end-to-end (E2E) testing strategy leverages a combination of technologies to ensure robust test coverage for our mobile applications. We use Wix/Detox for the majority of our automation tests, and for specific non-functional testing like app upgrades and launch times. All tests are written in TypeScript, and use jest test runners.
- Local environment setup
- Build the app (optional)
- Use Expo prebuilds (recommended)
- Run the E2E Tests
- Appium smoke tests (Playwright)
- Flask E2E Testing (Snaps Support)
- Setup Troubleshooting
- Legacy Appium (wdio)
Firstly, you need to have installed Xcode for IOS and Android Studio. Please follow the environment setup guide to install and configure them.
Ensure that following devices are set up:
- iOS: iPhone 16 Pro
- Android: Pixel 5 API 34
Note: You can change the default devices at any time by updating the
device.typein the Detox config located at.detoxrc.js.
iOS:
- Open Xcode
- Go to Window → Devices and Simulators
- Click the + button to add a new simulator
- Select iPhone 16 Pro and create the simulator
Android:
-
Open Android Studio
-
Go to Tools → AVD Manager (Device Manager)
-
Click Create Virtual Device
-
Select a Pixel device (or similar)
-
Choose API level 34
-
Important: Name the emulator exactly Pixel_5_Pro_API_34 to match our configuration
-
Set up Android SDK path by adding this to your shell profile (
.bashrc,.zshrc, etc.):export ANDROID_SDK_ROOT="/Users/${USER}/Library/Android/sdk"
-
Copy the E2E environment variables from the example file:
cp .e2e.env.example .e2e.env
-
Ensure your
.e2e.envfile contains the following prebuild paths:# E2E prebuild paths # These paths point to a gitignored root build folder, so you may need to create this folder. export PREBUILT_IOS_APP_PATH='build/MetaMask.app' export PREBUILT_ANDROID_APK_PATH='build/MetaMask.apk' export PREBUILT_ANDROID_TEST_APK_PATH='build/MetaMask-Test.apk'
You can either use prebuilt app files from Expo (iOS only) or build the app locally.
Choose one of the following methods to download the prebuilt iOS app:
Method A: Using the install script (recommended)
Requires GitHub CLI (gh auth login).
yarn install:ios:dev --skipInstallMethod B: Manual download from GitHub Actions
-
Find a successful
Expo Dev Buildrun onmain. -
Download the
ios-app-main-dev-expoartifact:gh run download RUN_ID --repo MetaMask/metamask-mobile \ -n ios-app-main-dev-expo -D build
-
Extract the simulator zip:
mkdir -p build/MetaMask.app ditto -x -k build/metamask-simulator-*.zip build/MetaMask.app
Sometimes it is necessary to build the app locally, for example, to enable build-time feature flags (like GNS), to debug issues more effectively, or to identify and update element locators.
NOTE: Building the app locally requires significant system resources.
Please follow the native development guide for more details.
# Build the app for testing
yarn test:e2e:ios:debug:build
yarn test:e2e:android:debug:build
# These commands are hardcoded to build for `main` build type and `e2e` environment based on the .detoxrc.js fileRunning E2E tests requires two separate terminal sessions: one for the Metro bundler and one for executing the tests.
First, ensure the build watcher is running in a dedicated terminal for logs:
export METAMASK_ENVIRONMENT='e2e'
export METAMASK_BUILD_TYPE='main'
export HAS_TEST_OVERRIDES="true"
yarn setup:expo
yarn watch:clean # First time or after dependency changes
yarn watch # Subsequent runsIn a separate terminal, set up and run your tests:
Initial Setup (First Time Only)
cp .e2e.env.example .e2e.envRun All Tests
source .e2e.env && yarn test:e2e:ios:debug:run
source .e2e.env && yarn test:e2e:android:debug:runRun Specific Test Folder
source .e2e.env && yarn test:e2e:ios:debug:run tests/smoke/your-folder
source .e2e.env && yarn test:e2e:android:debug:run tests/smoke/your-folderRun Specific Test File
source .e2e.env && yarn test:e2e:ios:debug:run tests/smoke/onboarding/create-wallet.spec.js
source .e2e.env && yarn test:e2e:android:debug:run tests/smoke/onboarding/create-wallet.spec.jsRun Tests by Tag
source .e2e.env && yarn test:e2e:ios:debug:run --testNamePattern="Smoke"
source .e2e.env && yarn test:e2e:android:debug:run --testNamePattern="Smoke"To know more about the E2E testing framework, see E2E Testing Architecture and Framework.
Appium smoke tests live in tests/smoke-appium/ and run via Playwright + Appium (not Detox). They mirror Detox smoke specs and share page objects, but use a main-e2e release build — no Metro bundler required.
| Detox smoke | Appium smoke |
|---|---|
tests/smoke/ |
tests/smoke-appium/ |
yarn test:e2e:ios:debug:run |
yarn appium-smoke:ios |
| Debug build + Metro | main-e2e-MetaMask.app / main-e2e-release.apk |
Quick start (iOS, CI build):
mkdir -p build/ci-main-e2e
gh run download RUN_ID --repo MetaMask/metamask-mobile \
-n main-e2e-MetaMask.app -D build/ci-main-e2e
IOS_APP_PATH=build/ci-main-e2e/MetaMask.app \
IOS_SIMULATOR_NAME="iPhone 16 Pro" \
node scripts/e2e/prepare-ios-appium-runner.mjs
IOS_APP_PATH=build/ci-main-e2e/MetaMask.app \
IOS_SIMULATOR_UDID="$(xcrun simctl list devices booted -j | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8'));console.log(Object.values(d.devices).flat().find(x=>x.state==='Booted')?.udid||'')")" \
yarn appium-smoke:ios --grep SmokeAccountsFull guide: Appium smoke testing — builds, env vars, Android ABI caveats, CI, troubleshooting.
Flask is a special build variant that enables wider Snaps support and other experimental features. Flask E2E tests require specific configuration to enable development APIs.
Ensure you have completed the Local environment setup steps first.
Development with Hot Reload:
# Start Metro bundler for Flask development
# Ensure METAMASK_BUILD_TYPE is set to `flask` and METAMASK_ENVIRONMENT is set to `e2e` in .js.env
source .e2e.env # Ensure .js.env is sourced
yarn watch:clean # First time or after dependency changes
yarn watch # Subsequent runsBuild for E2E Testing:
# Build Flask app for E2E tests
yarn test:e2e:ios:flask:build
yarn test:e2e:android:flask:buildRun Flask E2E Tests:
# Run all Flask E2E tests
yarn test:e2e:ios:flask:run
yarn test:e2e:android:flask:run
# These commands are hardcoded to build for `flask` build type and `e2e` environment based on the .detoxrc.js file
# Run specific Flask test
yarn test:e2e:ios:flask:run test/smoke/snaps/test-snap-jsx.spec.ts
yarn test:e2e:android:flask:run tests/smoke/snaps/test-snap-jsx.spec.tsFlask E2E builds use these key environment variables:
METAMASK_BUILD_TYPE=flask # Enables Flask build variant
METAMASK_ENVIRONMENT=e2e # Enables E2E-specific configurations
BRIDGE_USE_DEV_APIS=true # Enables more snaps funcationality and dev APIsBuild Script Architecture:
- Local builds: Use
MODE=flaskDebugE2E(debug APKs/apps) - CI builds: Use
MODE=flask(release APKs/apps) - Both modes use
ENVIRONMENT=e2efor E2E-specific setup
Problem: If your .js.env file has hardcoded METAMASK_BUILD_TYPE or METAMASK_ENVIRONMENT, it will override command-line environment variables and cause Flask features (like Snaps) to be disabled.
Example of problematic .js.env:
# ❌ DON'T: Hardcoded values override everything
export METAMASK_BUILD_TYPE=main
export METAMASK_ENVIRONMENT=productionSolution: Remove or comment out these lines in .js.env, or use conditional logic:
# ✅ DO: Allow override from command line
export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-main}
export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-production}Symptoms of this issue:
- Error: "Installing Snaps is currently disabled in this version of MetaMask"
- Snaps tests work on CI but fail locally
- Flask features not available despite using Flask build commands
Problem: Testing with a Main build instead of Flask build, or testing with an old Flask build that was built before environment variables were properly configured.
How to verify you're testing the correct build:
- Check the app splash screen - it should show "Flask" logo/text
- Check Metro bundler output - should show
METAMASK_BUILD_TYPE: flask - Check build artifacts:
- iOS:
ios/build/Build/Products/Debug-iphonesimulator/MetaMask-Flask.app - Android:
android/app/build/outputs/apk/flask/debug/app-flask-debug.apk
- iOS:
Solution: Always rebuild after changing environment variables or .js.env:
# Clean previous builds
yarn watch:clean
# Rebuild Flask app
yarn test:e2e:android:flask:build # or iOSProblem: Flask development builds require Metro bundler to be running with correct environment variables.
Solution: Always start Metro bundler first with Flask environment:
# Terminal 1: Start Metro bundler
yarn watch:clean
# Terminal 2: Reinstall and run Flask app
yarn test:e2e:android:flask:run| Aspect | Main Build | Flask Build |
|---|---|---|
| Snaps Support | ❌ Limited | ✅ Enabled (with BRIDGE_USE_DEV_APIS=true) |
| Dev APIs | ❌ Limited | ✅ Full access |
| App Icon | Standard MetaMask | Flask logo |
| Bundle ID | io.metamask |
io.metamask.flask |
| E2E Mode | debugE2E |
flaskDebugE2E |
| Detox Config | android.emu.main / ios.sim.main |
android.emu.flask / ios.sim.flask |
"Installing Snaps is currently disabled" error:
- Check if
.js.envhas hardcodedMETAMASK_BUILD_TYPEorMETAMASK_ENVIRONMENT- remove them - Verify
BRIDGE_USE_DEV_APIS=trueis set during build - Rebuild the app with
yarn test:e2e:*:flask:build - Verify Flask build by checking app icon/splash screen
Metro bundler shows wrong METAMASK_BUILD_TYPE:
- Stop Metro bundler (Ctrl+C)
- Clean bundler cache:
yarn watch:clean - Restart Metro bundler:
yarn watch
App crashes or shows blank screen:
- Ensure emulator/simulator is running before building
- Check Metro bundler logs for JavaScript errors
- Try clean build:
yarn watch:clean && yarn test:e2e:*:flask:build
Tests timeout waiting for elements:
- Verify you're running Flask tests against Flask build (not Main build)
- Check if app actually has Flask features enabled
- Take screenshot to verify app state:
adb exec-out screencap -p > screenshot.png
- The application is not opening: EXPO DOESN'T SUPPORT DETOX OUT OF THE BOX SO IT IS POSSIBLE THAT, IN SLOWER COMPUTERS, LOADING FROM THE BUNDLER TAKES TOO LONG WHICH MAKES THE VERY FIRST TEST FAIL. THE FAILED TEST WILL THEN AUTOMATICALLY RESTART AND IT SHOULD WORK FROM THEN ON.
- Build folder doesn't exist: Run
mkdir buildin your project root - Simulator/Emulator not found: Ensure the device names match exactly as specified in prerequisites
- Android SDK not found: Verify
$ANDROID_SDK_ROOTis set correctly withecho $ANDROID_SDK_ROOT - My Expo Application shows an error "Failed to connect to localhost/127.0.0.1:8081": The emulator may need to have the expo port forwarded. Try
adb reverse tcp:8081 tcp:8081and rerun the test command. - Warning Logs: Warning logs may sometimes cause test failures by interfering with automation interactions. To prevent this, disable warning logs during test execution.
- Android notice: with the implementation of Expo, mobile app will need to be manually loaded on emulator before running automated E2E tests.
- install a build on the emulator
- either install the apk or keep an existing install on the emulator
- on the metro server hit 'a' on the keyboard as indicated by metro for launching emulator
- if emulator fails to launch you can launch emulator in another terminal
emulator -avd <emulator-name>- on the metro server hit 'a' on the keyboard as indicated by metro for launching emulator
- you don't need to repeat these steps unless emulator or metro server is restarted
- install a build on the emulator
⚠️ DEPRECATED: The legacy Appium/WebDriver.io/Cucumber test infrastructure (wdio/) has been removed. New Appium coverage uses Playwright — see Appium smoke tests (Playwright) and docs/testing/appium-smoke-testing.md.
We currently utilize Appium, Webdriver.io, and Cucumber to test the application launch times and the upgrade between different versions. As a brief explanation, webdriver.io is the test framework that uses Appium Server as a service. This is responsible for communicating between our tests and devices, and cucumber as the test framework.
Current approach: Performance testing is now handled by a Playwright-based mobile testing framework. See the tests/performance directory for performance tests including app launch times and feature-specific performance measurements.
Test Location: tests/performance/
Platform: iOS
Test Location: tests/smoke/api-specs/json-rpc-coverage.js
The API Spec tests use the @open-rpc/test-coverage tool to generate tests from our api-specs OpenRPC Document. These tests are currently executed only on iOS and use the same build as the Detox tests for iOS.
- Test Coverage Tool: The
test-coveragetool usesRulesandReportersto generate and report test results. These are passed as parameters in the test coverage tool call located in tests/smoke/api-specs/json-rpc-coverage.js. For more details onRulesandReporters, refer to the OpenRPC test coverage documentation.
-
Build the App:
yarn test:e2e:ios:debug:build
-
Run API Spec Tests:
yarn test:api-specs
You can get your BrowserStack username and access key from the Access key dropdown on the app automate screen in BrowserStack.
export BROWSERSTACK_USERNAME='your_username'
export BROWSERSTACK_ACCESS_KEY='your_access_key'For MM Connect performance tests on BrowserStack, you also need the BrowserStack Local tunnel running so the cloud device can reach the local Browser Playground dapp. Start the BrowserStack Local binary, then set:
export BROWSERSTACK_LOCAL='true'Do not set BROWSERSTACK_LOCAL=true unless the tunnel is running. Other BrowserStack performance suites (for example onboarding) run without local testing unless you intentionally enable it.
Update the config file with the appropriate BrowserStack app URL. You’ll need a BrowserStack URL first. To get it:
- Run Build Mobile App in Github Actions.
- Once done, open scroll down to the Artifacts section in the workflow and find the build artifacts.
See this workflow as an example.
Download the build artifact and upload it to BrowserStack App Automate service. Once the upload is complete, it will provide a BrowserStack URL that you can copy.
Add it to the config file by replacing process.env.BROWSERSTACK_ANDROID_APP_URL in the buildPath with the appropriate BrowserStack application URL:
{
name: 'browserstack-android',
use: {
platform: Platform.ANDROID,
device: {
provider: 'browserstack',
name: process.env.BROWSERSTACK_DEVICE || 'Samsung Galaxy S25 Ultra', // this can be changed
osVersion: process.env.BROWSERSTACK_OS_VERSION || '15.0', // this can be changed
},
buildPath: process.env.BROWSERSTACK_ANDROID_APP_URL, // Path to BrowserStack URL bs:// link
},
}You can repeat the same for iOS builds by replacing process.env.BROWSERSTACK_IOS_APP_URL in the config.
yarn run-playwright:android-bsyarn run-playwright:ios-bsImportant Note: We strongly advise the use of Browserstack for this as we're still going through the migration and the amulator interface isn't yet fully ready.
You need to make sure that the artifact is created. Download the binary using yarn install:ios:dev --skipInstall / yarn install:android:dev --skipInstall and place it in a folder accessible to Playwright.
Then update the build path in the ios or android config:
{
name: 'ios',
use: {
platform: Platform.IOS,
device: {
provider: 'emulator',
osVersion: '16.0', // this can be changed to your simulator version
},
buildPath: 'PATH-TO-BUILD', // Path to your .app file
},
}yarn run-playwright:androidyarn run-playwright:iosImportant: If the test fail to start, double check the OS version your simulator/emulator is running and make sure the config has the correct version.
-
Example:
FAIL tests/smoke/swaps/swap-action-smoke.spec.js (232.814 s) SmokeSwaps Swap from Actions ✓ should Swap .05 'ETH' to 'USDT' (90488 ms) ✕ should Swap 100 'USDT' to 'ETH' (50549 ms) ● SmokeSwaps Swap from Actions › should Swap 100 'USDT' to 'ETH' Test Failed: Timed out while waiting for expectation: TOBEVISIBLE WITH MATCHER(id == “swap-quote-summary”) TIMEOUT(15s) HINT: To print view hierarchy on failed actions/matches, use log-level verbose or higher. 163 | return await waitFor(element(by.id(elementId))) 164 | .toBeVisible() > 165 | .withTimeout(15000); | ^ 166 | } 167 | 168 | static async checkIfNotVisible(elementId) { at Function.withTimeout (tests/helpers.js:165:8) ...In this example, the test failed because the
swap-quote-summaryID was not found. This issue could be due to a changed testID or the swap quotes not being visible. To confirm whether either case is true, we then look at the screenshots on failure.Here we can see that the swaps quotes in fact did not load hence why the tests failed.
- Per Team: Smoke tests are divided by team, allowing targeted verification of core functionalities pertinent to each team's responsibilities.
- Benefits:
- Faster Feedback: Running a subset of tests on PRs provides quicker feedback, ensuring critical functionalities are validated without the overhead of executing all tests.
- Efficient Resource Use: Limits resource consumption and test execution time, optimizing CI/CD pipeline performance.
For detailed E2E framework documentation, patterns, and best practices, see:
- E2E Framework Guide - Comprehensive guide to the TypeScript testing framework
- Mocking Guide - Guide on how to mock API call in tests
- General E2E Best Practices - MetaMask-wide testing guidelines