⚠️ 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
- Flask E2E Testing (Snaps Support)
- Setup Troubleshooting
- Appium
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 15 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 15 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 Runway Script (Recommended)
yarn install:ios:runway --skipInstallMethod B: Manual Download from Runway
-
Navigate to Runway builds
-
Download the latest version of the app
-
Copy and rename the build:
# Copy your downloaded .app file to the prebuild path cp /path/to/your/downloaded/AAA.app 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'
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.
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 Appium/WebDriver.io/Cucumber test infrastructure has been removed. This section is kept for historical reference only.
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 Appwright, 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/
Legacy Appium Documentation (for reference only)
Supported Platform: Android
Test Location: wdio
We have two separate configurations for testing the different variants of our applications:
- QA Variant (local): Runs in debug mode on your local machine.
- QA Variant (production): Runs in production mode on BrowserStack.
We use the QA variant for Appium tests because of our screen-blocking mechanism, which would otherwise prevent tests from getting past the wallet setup screen.
We require two sets of capabilities to handle app upgrade tests, leading to the creation of two configurations: defaultCapabilities and upgradeCapabilities.
const defaultCapabilities = [
{
platformName: 'Android',
noReset: false,
fullReset: false,
maxInstances: 1,
build: 'Android App Launch Times Tests',
device: process.env.BROWSERSTACK_DEVICE || 'Google Pixel 6',
os_version: process.env.BROWSERSTACK_OS_VERSION || '12.0',
app: process.env.BROWSERSTACK_APP_URL,
'browserstack.debug': true,
'browserstack.local': true,
},
];This configuration is our standard, as it only requires one app per install.
const upgradeCapabilities = [
{
platformName: 'Android',
noReset: false,
fullReset: false,
maxInstances: 1,
build: 'Android App Upgrade Tests',
device: process.env.BROWSERSTACK_DEVICE || 'Google Pixel 6',
os_version: process.env.BROWSERSTACK_OS_VERSION || '12.0',
app: process.env.PRODUCTION_APP_URL || process.env.BROWSERSTACK_APP_URL,
'browserstack.debug': true,
'browserstack.local': true,
'browserstack.midSessionInstallApps': [process.env.BROWSERSTACK_APP_URL],
},
];This configuration requires two applications: the current production app and the app built from the branch.
Note: You can, if you choose to, run the tests against any one of the devices and operating systems mentioned in the browserstack device list.
We use flags like --performance and --upgrade to determine which capabilities to use for specific tests.
const { selectedCapabilities, defaultTagExpression } = (() => {
if (isAppUpgrade) {
return {
selectedCapabilities: upgradeCapabilities,
defaultTagExpression: '@upgrade and @androidApp',
};
} else if (isPerformance) {
return {
selectedCapabilities: defaultCapabilities,
defaultTagExpression: '@performance and @androidApp',
};
} else {
return {
selectedCapabilities: defaultCapabilities,
defaultTagExpression: '@smoke and @androidApp',
};
}
})();You can run your E2E tests on local simulators either in development mode (with automatic code refresh) or without it.
Install dependencies:
yarn setupEnsure that the bundler compiles all files before running the tests to avoid build breaks. Use:
yarn watch:cleanTo start an iOS QA build:
yarn start:ios:qaTo start an Android QA build:
yarn start:android:qaThen, run the tests on the simulator:
yarn test:wdio:iosyarn test:wdio:androidTo run specific tests, use the --spec option:
yarn test:wdio:android --spec ./wdio/features/performance/ColdStartLaunchTimes.featureNote: Ensure that your installed simulator names match the configurations in wdio/config/android.config.debug.js and wdio/config/ios.config.debug.js.
To trigger tests locally on BrowserStack:
- Retrieve your BrowserStack username and access key from the App Automate section.
- Update
config.userandconfig.keyinandroid.config.browserstackwith your BrowserStack credentials. - Upload your app to BrowserStack via the
create_qa_builds_pipeline. Grab theapp_urlfrombrowserstack_uploaded_apps.json. - Update
process.env.BROWSERSTACK_APP_URLwith the correctapp_url. - Run your tests using the appropriate flag (e.g., for performance tests):
yarn test:wdio:android:browserstack --performanceYou can also run Appium tests on CI using Bitrise pipelines:
app_launch_times_pipelineapp_upgrade_pipeline
For more details on our CI pipelines, see the Bitrise Pipelines Overview.
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
This is a recent mobile framework which was built using appium and playwright. We adopted it to meet our need for running performance focused end to end tests on real iOS and Android devices through BrowserStack.
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'Update the config file with the appropriate BrowserStack app URL. You’ll need a BrowserStack URL first. To get it:
- Run
create_qa_builds_pipelineon Bitrise - Once done, open the Artifacts tab and find
browserstack_uploaded_apps.json(frombuild_android_qaandbuild_ios_qa).
See this build as an example.
The first entry in that JSON will include your app’s URL (look for the bs:// prefix).
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 S23 Ultra', // this can be changed
osVersion: process.env.BROWSERSTACK_OS_VERSION || '13.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-appwright:android-bsyarn run-appwright:ios-bsYou need to make sure that the artifact is created. Download the binary from the runway and place it in a folder accessible to Appwright.
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-appwright:androidyarn run-appwright: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.
Our CI/CD process is automated through various Bitrise pipelines, each designed to streamline and optimize different aspects of our E2E testing.
- Workflows:
- Build: Creates iOS and Android artifacts.
- Test: Executes regression tests across both platforms.
- Manual Trigger: Typically run on release branches but can be manually triggered in the Bitrise dashboard.
- Function: Measures and monitors app launch times on real devices using BrowserStack to ensure consistent performance over time.
- Nightly: Automatically runs on the main branch.
- Manual Trigger: Select the desired branch in the Bitrise dashboard and choose
app_upgrade_pipelinefrom the pipeline dropdown menu.
- Function: Automates testing of app upgrades to verify smooth transitions between versions.
- Configuration: Requires the
PRODUCTION_APP_URLenvironment variable to be set with the current production build's BrowserStack URL.You would need to search and updatePRODUCTION_APP_URLin the bitrise.yml with the production browserstack build URL. - Manual Trigger: Select the desired branch in the Bitrise dashboard and choose
app_upgrade_pipelinefrom the pipeline dropdown menu.
- Detox Tests: Test reports are displayed directly in the Bitrise UI, offering a visual representation of test results and execution details. Screenshots on test failures are also captured and stored in a zip file. You can download these screenshots from the
Artifactstab in Bitrise. - API Spec and Appium Tests: HTML reporters generate and display test results. Access these HTML reports through the Bitrise build artifacts section for detailed analysis.
-
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