E2E Testing Architecture and Framework
June 25, 2026 · View on GitHub
⚠️ ESSENTIAL: Read E2E Testing Overview for complete setup guide
- E2E Framework Structure
- Playwright: local emulator
buildPathand app install - E2E Testing Best Practices
- Playwright Node Runtime Shims
- E2E Test Examples and Patterns
- E2E Testing Anti-Patterns (AVOID THESE)
- E2E Code Review Checklist
- AI E2E Testing System
E2E Framework Structure
- Regression Testing Scenarios (
tests/regression/) - Regression Test files organized by feature - Snoke Testing Scenarios (
tests/smoke/) - Smoke Test files organized by feature (Detox) - Appium smoke (
tests/smoke-appium/) - Playwright + Appium smoke parity withtests/smoke/— see appium-smoke-testing.md - TypeScript Framework (
tests/framework/): Modern testing framework with type safety - Page Objects (
tests/page-objects/): Page Object Model implementation - Selectors (
tests/selectors/): Element selectors organized by feature - Fixtures (
tests/framework/fixtures/): Test data and state management - API Mocking (
tests/api-mocking/): Comprehensive API mocking system - WebSocket Mocking (
tests/websocket/): Local WebSocket server for mocking real-time connections (docs) - Playwright local emulator (Appium): When using
EmulatorProvider,use.app.buildPathvs no path controls install vs pre-installed app — PLAYWRIGHT_LOCAL_EMULATOR.md
Core E2E Framework Classes:
Assertions.ts- Enhanced assertions with auto-retry and detailed error messagesGestures.ts- Robust user interactions with configurable element state checkingMatchers.ts- Type-safe element selectors with flexible optionsUtilities.ts- Core utilities with specialized element state checkingFixtureBuilder.ts- Builder pattern for creating test fixtures
Key E2E Directories:
tests/framework/- TypeScript framework foundation (USE THIS)tests/smoke/- Detox smoke tests organized by featuretests/smoke-appium/- Appium smoke tests (Playwright); same layout astests/smoke/tests/regression/- Regression Test files organized by featuretests/page-objects/- Page Object classes following POM patterntests/selectors/- Element selectors (avoid direct use in tests)tests/api-mocking/- API mocking utilities and responsestests/framework/fixtures/- Test fixtures and data
E2E Testing Best Practices
CRITICAL: Always Use withFixtures Pattern
Every E2E test MUST use withFixtures for proper test setup and cleanup:
import { withFixtures } from '../framework/fixtures/FixtureHelper';
import FixtureBuilder from '../framework/fixtures/FixtureBuilder';
await withFixtures(
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
},
async () => {
// Test code here
},
);
Framework Usage (MANDATORY):
- ✅ ALWAYS import from
tests/framework/index.ts(not individual files) - ✅ ALWAYS use modern TypeScript framework methods
- ❌ NEVER use legacy methods marked with
@deprecated - ❌ NEVER use
TestHelpers.delay()- use proper waiting instead
Page Object Model (REQUIRED):
- ✅ ALWAYS use Page Object pattern - no direct selectors in test specs
- ✅ ALWAYS define element selectors in page objects or selector files
- ✅ ALWAYS access UI elements through page object methods
- ❌ NEVER use
element(by.id())directly in test specs - ❌ NEVER use raw Detox assertions in test specs
Test Structure Requirements:
- Use descriptive test names without 'should' prefix
- Include
descriptionparameters in ALL assertions and gestures - Control application state programmatically via fixtures, not UI
- Use proper element state configuration (visibility, enabled, stability)
- Handle expected failures with try/catch, not broad timeouts
API Mocking (ESSENTIAL):
- All tests run with API mocking enabled by default
- Use
testSpecificMockparameter inwithFixturesfor test-specific mocks - Default mocks are loaded from
tests/api-mocking/mock-responses/defaults/ - Feature flags mocked via
setupRemoteFeatureFlagsMockhelper
Playwright Node Runtime Shims
Playwright specs and FixtureBuilder run in Node, while the app runs in React Native/Metro. If a native-only module fails during Playwright import or fixture setup, keep the app on the native implementation and isolate the Node workaround in test infrastructure.
We need this shim mechanism because some app dependencies patch CommonJS packages to call native modules such as @metamask/native-utils. Those calls are valid in the mobile app, but Playwright loads the same dependency graph in Node where React Native/Nitro native modules are unavailable.
- Use
tests/framework/nodeNativeUtilsShim.cjsfor@metamask/native-utilsin Playwright's Node process. - Register Node shims from Playwright framework entrypoints before importing code that may load native modules.
- Do not add JS fallbacks to shared dependency patches unless the app runtime also needs them.
- Validate with
yarn playwright test --list --project android --config tests/playwright.config.tsbefore running devices.
Element State Configuration:
// Default behavior (recommended for most cases)
await Gestures.tap(button, { description: 'tap button' });
// checkVisibility: true, checkEnabled: true, checkStability: false
// For animated elements
await Gestures.tap(animatedButton, {
checkStability: true,
description: 'tap animated button',
});
// For loading/disabled elements
await Gestures.tap(loadingButton, {
checkEnabled: false,
description: 'tap loading button',
});
E2E Test Examples and Patterns
- ALWAYS use
withFixtures- every test must use this pattern - Framework Migration: Use TypeScript framework (
tests/framework/) - API Mocking: All tests run with mocked APIs - use
testSpecificMockfor test-specific needs - Page Objects: Mandatory pattern - no direct element access in test specs
- Element State: Configure visibility, enabled, and stability checking appropriately
- Debugging: Check test output for unmocked API requests and framework warnings
- Performance: Use
checkStability: falseby default, enable only for animated elements - Check
docs/testing/e2e-testing.mdfor comprehensive testing guidelines
Basic E2E Test Structure:
import { SmokeE2E } from '../../tags';
import { loginToApp } from '../../tests/flows/wallet.flow';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
import WalletView from '../../pages/wallet/WalletView';
import Assertions from '../../framework/Assertions';
describe(SmokeE2E('Feature Name'), () => {
beforeAll(async () => {
jest.setTimeout(150000);
});
it('performs expected behavior', async () => {
await withFixtures(
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
},
async () => {
await loginToApp();
// Use page object methods, never direct selectors
await WalletView.tapSendButton();
// Use framework assertions with descriptions
await Assertions.expectElementToBeVisible(WalletView.sendButton, {
description: 'send button should be visible',
});
},
);
});
});
Advanced Test with API Mocking:
import { Mockttp } from 'mockttp';
import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper';
import { confirmationsRedesignedFeatureFlags } from '../../api-mocking/mock-responses/feature-flags-mocks';
const testSpecificMock = async (mockServer: Mockttp) => {
await setupRemoteFeatureFlagsMock(
mockServer,
Object.assign({}, ...confirmationsRedesignedFeatureFlags),
);
};
await withFixtures(
{
fixture: new FixtureBuilder()
.withGanacheNetwork()
.withPermissionControllerConnectedToTestDapp()
.build(),
restartDevice: true,
testSpecificMock,
},
async () => {
// Test implementation with mocked APIs
},
);
FixtureBuilder Usage Patterns:
// Basic fixture
new FixtureBuilder().build();
// With popular networks
new FixtureBuilder().withPopularNetworks().build();
// With connected test dapp
new FixtureBuilder()
.withPermissionControllerConnectedToTestDapp(buildPermissions(['0x539']))
.build();
// With tokens and contacts
new FixtureBuilder()
.withAddressBookControllerContactBob()
.withTokensControllerERC20()
.build();
Page Object Implementation Example:
import { Matchers, Gestures, Assertions } from '../../framework';
import { WalletViewSelectors } from './WalletView.selectors';
class WalletView {
get sendButton() {
return Matchers.getElementByID(WalletViewSelectors.SEND_BUTTON);
}
get receiveButton() {
return Matchers.getElementByID(WalletViewSelectors.RECEIVE_BUTTON);
}
async tapSendButton(): Promise<void> {
await Gestures.tap(this.sendButton, {
description: 'tap send button',
});
}
async verifySendButtonVisible(): Promise<void> {
await Assertions.expectElementToBeVisible(this.sendButton, {
description: 'send button should be visible',
});
}
}
export default new WalletView();
E2E Testing Anti-Patterns (AVOID THESE)
❌ PROHIBITED Patterns:
// DON'T: Use TestHelpers.delay()
await TestHelpers.delay(5000);
// DON'T: Use deprecated methods
await Assertions.checkIfVisible(element);
// DON'T: Use direct element selectors in tests
element(by.id('send-button')).tap();
// DON'T: Use raw Detox assertions
await waitFor(element).toBeVisible();
// DON'T: Missing descriptions
await Gestures.tap(button);
await Assertions.expectElementToBeVisible(element);
// DON'T: Manual retry loops
let attempts = 0;
while (attempts < 5) {
try {
await Gestures.tap(button);
break;
} catch {
attempts++;
}
}
✅ CORRECT Patterns:
// DO: Use proper waiting with framework
await Assertions.expectElementToBeVisible(element, {
description: 'element should be visible',
});
// DO: Use modern framework methods
await Gestures.tap(button, { description: 'tap submit button' });
// DO: Use Page Object methods
await WalletView.tapSendButton();
// DO: Use framework retry mechanisms
await Utilities.executeWithRetry(
async () => {
await Gestures.tap(button, { timeout: 2000 });
await Assertions.expectElementToBeVisible(nextScreen, { timeout: 2000 });
},
{
timeout: 30000,
description: 'tap button and verify navigation',
},
);
E2E Code Review Checklist
Before submitting any E2E test, verify:
- Uses
withFixturespattern for test setup - Imports from
tests/framework/index.ts(not individual files) - No usage of
TestHelpers.delay()orsetTimeout() - No deprecated methods (check
@deprecatedtags) - All assertions have descriptive
descriptionparameters - All gestures have descriptive
descriptionparameters - Page Object pattern used (no direct selectors in tests)
- Element selectors defined in page objects or selector files
- Appropriate timeouts for operations (not magic numbers)
- API mocking configured when needed
- Tests work on both iOS and Android platforms
- Test names are descriptive without 'should' prefix
- Uses FixtureBuilder for test data setup
Existing tooling
| Tool | Type | Current use | When to use | Notes and Limitations |
|---|---|---|---|---|
| Component View Tests | White box testing | UI integration tests | - When following a full user flow is not needed - When we want to test individual component and view rendering | - Does not require builds - Low cost (faster runtime) - Fast feedback loop |
| Detox | Grey box testing | E2E | - When we want to test user flows - Run on PR basis | - High cost - Low tool (detox) maintenance - JS/TS based test files - Handles deeply nested elements better (easier to find and locate elements) - Uses emulators/simulators - Allows runs with local Builds - Can't be used with real devices (cloud included) |
| Maestro | Black box testing | TBD | TBD | - Still in experimentation phase (!) - Struggles with deeply nested elements - YAML based spec files - Allows runs with local builds - Can run on real devices (cloud) but can't be used with real devices |
| Appium | Black box testing | Performance tests | - When we want to test user flows as a end user - When we want to measure and report performance stats | - High cost - Struggles with deeply nested - Uses a Cloud provider for real device testing elements - Does not allow runs with local builds |