From 859f8eb4abd14aaa0b678e41ee5c455640778564 Mon Sep 17 00:00:00 2001 From: cmd-ob Date: Mon, 1 Sep 2025 11:46:20 +0100 Subject: [PATCH 1/2] Refactor mobile E2E testing guidelines and remove API mocking documentation - Updated the mobile E2E testing guidelines to enhance clarity and structure, including sections on test classification, locator strategy, naming conventions, and assertions. - Removed the outdated API mocking documentation to streamline the testing resources and focus on the modern framework. --- docs/testing/e2e/mobile-e2e-api-mocking.md | 602 ++++++++++++++++++ .../testing/e2e/mobile-e2e-framework-guide.md | 431 +++++++++++++ docs/testing/e2e/mobile-e2e-guidelines.md | 555 +++++++++++----- docs/testing/e2e/mobile-e2e-mocking.md | 173 ----- 4 files changed, 1430 insertions(+), 331 deletions(-) create mode 100644 docs/testing/e2e/mobile-e2e-api-mocking.md create mode 100644 docs/testing/e2e/mobile-e2e-framework-guide.md delete mode 100644 docs/testing/e2e/mobile-e2e-mocking.md diff --git a/docs/testing/e2e/mobile-e2e-api-mocking.md b/docs/testing/e2e/mobile-e2e-api-mocking.md new file mode 100644 index 00000000..98aecd2b --- /dev/null +++ b/docs/testing/e2e/mobile-e2e-api-mocking.md @@ -0,0 +1,602 @@ +# MetaMask Mobile E2E API Mocking Guide + +This guide covers the comprehensive API mocking system used in MetaMask Mobile E2E tests. For framework usage, see the [E2E Framework Guide](./mobile-e2e-framework-guide.md). + +## 🎭 Mocking System Overview + +MetaMask Mobile E2E tests use a sophisticated API mocking system that: + +- **Intercepts all network requests** through a proxy server +- **Provides default mocks** for common APIs across all tests +- **Supports test-specific mocks** for custom scenarios +- **Handles feature flag mocking** for controlled feature testing +- **Tracks unmocked requests** for comprehensive coverage + +## πŸ—οΈ Mocking Architecture + +``` +e2e/api-mocking/ +β”œβ”€β”€ mock-responses/ +β”‚ β”œβ”€β”€ defaults/ # Default mocks for all tests +β”‚ β”‚ β”œβ”€β”€ index.ts # Aggregates all default mocks +β”‚ β”‚ β”œβ”€β”€ accounts.ts # Account-related API mocks +β”‚ β”‚ β”œβ”€β”€ price-apis.ts # Price feed mocks +β”‚ β”‚ β”œβ”€β”€ swap-apis.ts # Swap/exchange API mocks +β”‚ β”‚ β”œβ”€β”€ token-apis.ts # Token metadata mocks +β”‚ β”‚ β”œβ”€β”€ staking.ts # Staking API mocks +β”‚ β”‚ └── user-storage.ts # User storage service mocks +β”‚ β”œβ”€β”€ feature-flags-mocks.ts # Predefined feature flag configs +β”‚ └── simulations.ts # Transaction simulation mocks +β”œβ”€β”€ helpers/ +β”‚ β”œβ”€β”€ mockHelpers.ts # Core mocking utilities +β”‚ └── remoteFeatureFlagsHelper.ts # Feature flag mocking helper +└── mock-server.ts # Mock server implementation +``` + +## πŸ”§ Default Mocks System + +### How Default Mocks Work + +Default mocks are automatically loaded for all tests via `FixtureHelper.createMockAPIServer()`: + +1. **Mock server starts** on dedicated port +2. **Default mocks loaded** from [`e2e/api-mocking/mock-responses/defaults/`](https://github.com/MetaMask/metamask-mobile/tree/main/e2e/api-mocking/mock-responses/defaults) +3. **Test-specific mocks applied** (take precedence over defaults) +4. **Feature flags mocked** with default configurations +5. **Notification services mocked** automatically + +### Default Mock Categories + +Default mocks are organized by API category: + +```typescript +// From e2e/api-mocking/mock-responses/defaults/index.ts +export const DEFAULT_MOCKS = { + GET: [ + ...(ACCOUNTS_MOCKS.GET || []), + ...(PRICE_API_MOCKS.GET || []), + ...(SWAP_API_MOCKS.GET || []), + ...(TOKEN_API_MOCKS.GET || []), + ...(STAKING_MOCKS.GET || []), + ...(USER_STORAGE_MOCKS.GET || []), + // ... other categories + ], + POST: [ + ...(ACCOUNTS_MOCKS.POST || []), + ...(SWAP_API_MOCKS.POST || []), + // ... other categories + ], +}; +``` +j +### Adding New Default Mocks + +To add default mocks that benefit all tests: + +1. **Create or edit a category file** in `defaults/` folder: + +```typescript +// e2g. defaults/my-new-service.ts +import { MockApiEndpoint } from '../../framework/types'; + +export const MY_SERVICE_MOCKS = { + GET: [ + { + urlEndpoint: 'https://api.myservice.com/data', + responseCode: 200, + response: { success: true, data: [] }, + }, + ] as MockApiEndpoint[], + POST: [ + { + urlEndpoint: 'https://api.myservice.com/submit', + responseCode: 201, + response: { id: '123', status: 'created' }, + }, + ] as MockApiEndpoint[], +}; +``` + +2. **Add to the main index file**: + +```typescript +// defaults/index.ts +import { MY_SERVICE_MOCKS } from './my-new-service'; + +export const DEFAULT_MOCKS = { + GET: [ + // ... existing mocks + ...(MY_SERVICE_MOCKS.GET || []), + ], + POST: [ + // ... existing mocks + ...(MY_SERVICE_MOCKS.POST || []), + ], +}; +``` + +## 🎯 Test-Specific Mocks + +### Method 1: Using testSpecificMock Parameter + +The most common approach for custom mocks: + +```typescript +import { withFixtures } from '../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../framework/fixtures/FixtureBuilder'; +import { Mockttp } from 'mockttp'; +import { setupMockRequest } from '../api-mocking/helpers/mockHelpers'; + +describe('Custom API Test', () => { + it('handles custom API response', async () => { + const testSpecificMock = async (mockServer: Mockttp) => { + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: 'https://api.example.com/custom-endpoint', + response: { customData: 'test-value' }, + responseCode: 200, + }); + }; + + await withFixtures( + { + fixture: new FixtureBuilder().build(), + testSpecificMock, + }, + async () => { + // Test implementation that uses the custom mocked API + }, + ); + }); +}); +``` + +### Method 2: Direct Mock Server Access + +Access the mock server directly within tests: + +```typescript +await withFixtures( + { + fixture: new FixtureBuilder().build(), + }, + async ({ mockServer }) => { + // Set up additional mocks within the test + await setupMockRequest(mockServer, { + requestMethod: 'POST', + url: 'https://api.example.com/dynamic-endpoint', + response: { result: 'success' }, + responseCode: 201, + }); + + // Test implementation + }, +); +``` + +## πŸ› οΈ Mock Helper Functions + +### setupMockRequest + +For simple HTTP requests with various methods: + +```typescript +import { setupMockRequest } from '../api-mocking/helpers/mockHelpers'; + +await setupMockRequest(mockServer, { + requestMethod: 'GET', // 'GET' | 'POST' | 'PUT' | 'DELETE' + url: 'https://api.example.com/endpoint', + response: { data: 'mock-response' }, + responseCode: 200, +}); + +// URL patterns with regex +await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: /^https:\/\/api\.metamask\.io\/prices\/[a-z]+$/, + response: { price: '100.50' }, + responseCode: 200, +}); +``` + +### setupMockPostRequest + +For POST requests with request body validation: + +```typescript +import { setupMockPostRequest } from '../api-mocking/helpers/mockHelpers'; + +await setupMockPostRequest( + mockServer, + 'https://api.example.com/validate', + { + method: 'transfer', + amount: '1000000000000000000', + to: '0x742d35cc6634c0532925a3b8d0ea4405abf5adf3' + }, // Expected request body + { result: 'validated', txId: '0x123...' }, // Mock response + { + statusCode: 200, + ignoreFields: ['timestamp', 'nonce'], // Fields to ignore in validation + }, +); +``` + +### Advanced Mock Patterns + +```typescript +// Mock with custom headers +await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: 'https://api.example.com/authenticated', + response: { data: 'secure-data' }, + responseCode: 200, + headers: { + 'Authorization': 'Bearer mock-token', + 'Content-Type': 'application/json', + }, +}); + +// Mock error responses +await setupMockRequest(mockServer, { + requestMethod: 'POST', + url: 'https://api.example.com/failing-endpoint', + response: { error: 'Internal Server Error' }, + responseCode: 500, +}); +``` + +## 🚩 Feature Flag Mocking + +### setupRemoteFeatureFlagsMock Helper + +Feature flags control application behavior and must be mocked consistently: + +```typescript +import { setupRemoteFeatureFlagsMock } from '../api-mocking/helpers/remoteFeatureFlagsHelper'; + +const testSpecificMock = async (mockServer: Mockttp) => { + await setupRemoteFeatureFlagsMock( + mockServer, + { + rewards: true, + confirmation_redesign: { + signatures: true, + transactions: false, + }, + bridgeConfig: { + support: true, + refreshRate: 5000, + }, + }, + 'main', // distribution: 'main' | 'flask' + ); +}; +``` + +### Predefined Feature Flag Configurations + +Use existing configurations from [`e2e/api-mocking/mock-responses/feature-flags-mocks.ts`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/api-mocking/mock-responses/feature-flags-mocks.ts): + +```typescript +import { + oldConfirmationsRemoteFeatureFlags, + confirmationsRedesignedFeatureFlags, +} from '../api-mocking/mock-responses/feature-flags-mocks'; + +// For legacy confirmations (old UI) +const testSpecificMock = async (mockServer: Mockttp) => { + await setupRemoteFeatureFlagsMock( + mockServer, + Object.assign({}, ...oldConfirmationsRemoteFeatureFlags), + ); +}; + +// For new confirmations UI +const testSpecificMock = async (mockServer: Mockttp) => { + await setupRemoteFeatureFlagsMock( + mockServer, + Object.assign({}, ...confirmationsRedesignedFeatureFlags), + ); +}; +``` + +### Feature Flag Override Patterns + +```typescript +// Combining predefined configs with custom overrides +await setupRemoteFeatureFlagsMock(mockServer, { + ...Object.assign({}, ...confirmationsRedesignedFeatureFlags), + rewards: true, // Override specific flags + carouselBanners: false, + perpsEnabled: true, // Add new flags +}); + +// Environment-specific flags +await setupRemoteFeatureFlagsMock(mockServer, { + devOnlyFeature: true, + prodOptimization: false, +}, 'flask'); // Flask distribution +``` + +## 🎭 Complete Mocking Examples + +### Basic Test with API Mocking + +```typescript +import { SmokeE2E } from '../../tags'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { Mockttp } from 'mockttp'; +import { setupMockRequest } from '../../api-mocking/helpers/mockHelpers'; + +describe(SmokeE2E('Token Price Display'), () => { + it('displays current token prices', async () => { + const testSpecificMock = async (mockServer: Mockttp) => { + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: 'https://price-api.metamask.io/v2/chains/1/spot-prices', + response: { + '0x0000000000000000000000000000000000000000': { // ETH + price: 2500.50, + currency: 'usd', + }, + }, + responseCode: 200, + }); + }; + + await withFixtures( + { + fixture: new FixtureBuilder().withTokensControllerERC20().build(), + testSpecificMock, + }, + async () => { + // Test implementation using mocked price API + }, + ); + }); +}); +``` + +### Advanced Test with Multiple Mocks + +```typescript +import { + SEND_ETH_SIMULATION_MOCK, + SIMULATION_ENABLED_NETWORKS_MOCK, +} from '../../api-mocking/mock-responses/simulations'; +import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationsRedesignedFeatureFlags } from '../../api-mocking/mock-responses/feature-flags-mocks'; + +const testSpecificMock = async (mockServer: Mockttp) => { + // Mock simulation API + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint, + response: SIMULATION_ENABLED_NETWORKS_MOCK.response, + responseCode: 200, + }); + + // Mock feature flags + await setupRemoteFeatureFlagsMock( + mockServer, + Object.assign({}, ...confirmationsRedesignedFeatureFlags), + ); + + // Mock transaction simulation + const { + urlEndpoint: simulationEndpoint, + requestBody, + response: simulationResponse, + ignoreFields, + } = SEND_ETH_SIMULATION_MOCK; + + await setupMockPostRequest( + mockServer, + simulationEndpoint, + requestBody, + simulationResponse, + { + statusCode: 200, + ignoreFields, + }, + ); +}; +``` + +### Organized Mock Setup + +For complex tests with many mocks: + +```typescript +const setupSwapMocks = async (mockServer: Mockttp) => { + // Price API mocks + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: 'https://price-api.metamask.io/v2/chains/1/spot-prices', + response: { /* price data */ }, + responseCode: 200, + }); + + // Swap quotes mock + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: /^https:\/\/swap-api\.metamask\.io\/networks\/1\/trades/, + response: { /* quote data */ }, + responseCode: 200, + }); + + // Gas API mock + await setupMockRequest(mockServer, { + requestMethod: 'GET', + url: 'https://gas-api.metamask.io/networks/1/gasPrices', + response: { /* gas prices */ }, + responseCode: 200, + }); +}; + +await withFixtures( + { + fixture: new FixtureBuilder().withGanacheNetwork().build(), + testSpecificMock: setupSwapMocks, + }, + async () => { + // Complex swap test implementation + }, +); +``` + +## 🚨 Mocking Best Practices + +### 1. Use Specific URL Patterns + +```typescript +// βœ… Good - specific endpoint +url: 'https://api.metamask.io/v2/chains/1/spot-prices' + +// βœ… Better - regex for dynamic parts +url: /^https:\/\/api\.metamask\.io\/v2\/chains\/\d+\/spot-prices$/ + +// ❌ Avoid - too broad, may interfere with other requests +url: 'metamask.io' +``` + +### 2. Handle Request Bodies Properly + +```typescript +// βœ… Validate important request body fields +await setupMockPostRequest( + mockServer, + 'https://api.example.com/transactions', + { + to: '0x742d35cc6634c0532925a3b8d0ea4405abf5adf3', + value: '1000000000000000000', // 1 ETH in wei + gasLimit: '21000', + }, + { txHash: '0x123...' }, + { + ignoreFields: ['timestamp', 'nonce', 'gasPrice'], // Ignore dynamic fields + }, +); +``` + +### 3. Use Appropriate HTTP Status Codes + +```typescript +// βœ… Use descriptive response codes +responseCode: 200, // OK +responseCode: 201, // Created +responseCode: 400, // Bad Request +responseCode: 404, // Not Found +responseCode: 500, // Internal Server Error +``` + +### 4. Organize Feature Flag Overrides + +```typescript +// βœ… Use Object.assign with predefined arrays +Object.assign({}, ...confirmationsRedesignedFeatureFlags) + +// ❌ Don't spread arrays directly +...confirmationsRedesignedFeatureFlags // This won't merge objects correctly +``` + +## πŸ” Debugging Mocks + +### Request Validation and Debugging + +The mock server automatically tracks and logs: + +- **Matched mock requests** with request details +- **Unmocked requests** that weren't handled +- **Request/response timing** information +- **Feature flag configurations** applied + + +### Common Debugging Steps + +1. **Check test output** for mock-related warnings +2. **Verify URL patterns** match actual requests +3. **Review request body validation** for POST requests +4. **Confirm feature flag configurations** are applied correctly +5. **Check mock precedence** (test-specific overrides defaults) + +### Debug Logging + +Enable debug logging to see mock activity: + +```typescript +// Mock helpers automatically log when mocks are triggered +// Look for output like: +// "Mocking GET request to: https://api.example.com/data" +// "Request body validation passed for: https://api.example.com/submit" +``` + +## 🚨 Common Mocking Issues + +### Mock Not Triggering + +**Cause**: URL pattern doesn't match actual request +**Solution**: Check URL pattern specificity and regex syntax + +```typescript +// ❌ Too specific - might miss query parameters +url: 'https://api.example.com/data' + +// βœ… More flexible pattern +url: /^https:\/\/api\.example\.com\/data(\?.*)?$/ +``` + +### POST Body Validation Failing + +**Cause**: Expected request body doesn't match actual request +**Solution**: Use `ignoreFields` for dynamic data + +```typescript +await setupMockPostRequest( + mockServer, + 'https://api.example.com/submit', + { method: 'transfer', amount: '1000' }, + { success: true }, + { + ignoreFields: ['timestamp', 'nonce', 'uuid'], // Ignore dynamic fields + }, +); +``` + +### Feature Flags Not Applying + +**Cause**: Incorrect merging of feature flag arrays +**Solution**: Use `Object.assign({}, ...arrays)` pattern + +```typescript +// βœ… Correct merging +Object.assign({}, ...confirmationsRedesignedFeatureFlags) + +// ❌ Incorrect - spreads array items +{ ...confirmationsRedesignedFeatureFlags } +``` + +## πŸ“š Mocking Resources + +- **Main Mocking Documentation**: [`e2e/MOCKING.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/MOCKING.md) +- **Mock Responses Directory**: [`e2e/api-mocking/mock-responses/`](https://github.com/MetaMask/metamask-mobile/tree/main/e2e/api-mocking/mock-responses) +- **Mock Helpers**: [`e2e/api-mocking/helpers/`](https://github.com/MetaMask/metamask-mobile/tree/main/e2e/api-mocking/helpers) +- **Feature Flag Mocks**: [`e2e/api-mocking/mock-responses/feature-flags-mocks.ts`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/api-mocking/mock-responses/feature-flags-mocks.ts) + +## βœ… Mocking Checklist + +Before submitting tests with custom mocks: + +- [ ] Uses `testSpecificMock` parameter in `withFixtures` +- [ ] URL patterns are specific enough to avoid conflicts +- [ ] POST request body validation configured appropriately +- [ ] Uses `ignoreFields` for dynamic request data +- [ ] Appropriate HTTP status codes used +- [ ] Feature flags configured using proper merge patterns +- [ ] Mock organization follows logical grouping +- [ ] No hardcoded values that should come from constants +- [ ] Error scenarios mocked when testing error handling + +The MetaMask Mobile API mocking system provides comprehensive control over network requests, enabling reliable and deterministic E2E tests. By following these patterns, you'll create tests that are both isolated and realistic. \ No newline at end of file diff --git a/docs/testing/e2e/mobile-e2e-framework-guide.md b/docs/testing/e2e/mobile-e2e-framework-guide.md new file mode 100644 index 00000000..7711ab45 --- /dev/null +++ b/docs/testing/e2e/mobile-e2e-framework-guide.md @@ -0,0 +1,431 @@ +# MetaMask Mobile E2E Framework Guide + +This guide covers the specific usage of MetaMask Mobile's TypeScript-based E2E testing framework. For general testing best practices, see the [General E2E Testing Guidelines](./mobile-e2e-guidelines.md). + +## πŸš€ Framework Overview + +MetaMask Mobile uses a modern TypeScript-based E2E testing framework built on Detox, featuring: + +- **Enhanced reliability** with auto-retry mechanisms +- **Type safety** with full TypeScript support +- **Configurable element state checking** (visibility, enabled, stability) +- **Comprehensive API mocking** system +- **Fixture-based test data management** + +## πŸ“‹ Quick Setup + +1. **Install E2E dependencies**: `yarn setup:e2e` +2. **Read setup documentation**: [`docs/readme/testing.md`](https://github.com/MetaMask/metamask-mobile/blob/main/docs/readme/testing.md) +3. **Configure environment files**: `.e2e.env` and `.js.env` +4. **Set up device**: iOS Simulator (iPhone 15 Pro) or Android Emulator (Pixel 5 API 34) - This is configurable + +## πŸ—οΈ Framework Architecture + +### Modern Framework + +``` +e2e/framework/ # Modern TypeScript framework +β”œβ”€β”€ Assertions.ts # Enhanced assertions with auto-retry +β”œβ”€β”€ Gestures.ts # Robust user interactions +β”œβ”€β”€ Matchers.ts # Type-safe element selectors +β”œβ”€β”€ Utilities.ts # Core utilities with retry mechanisms +β”œβ”€β”€ fixtures/ # Test data management +β”‚ β”œβ”€β”€ FixtureBuilder.ts # Builder pattern for test fixtures +β”‚ β”œβ”€β”€ FixtureHelper.ts # withFixtures implementation +β”‚ └── FixtureUtils.ts # Utility functions +└── index.ts # Main entry point - import from here +``` + +## πŸ“ Essential Patterns + +### MANDATORY: withFixtures Pattern + +Every E2E test MUST use `withFixtures` for proper setup and cleanup: + +```typescript +import { withFixtures } from '../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../viewHelper'; +import { SmokeE2E } from '../tags'; + +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(); + + // Test implementation here + }, + ); + }); +}); +``` + +### Required Framework Imports + +```typescript +// βœ… ALWAYS import from framework entry point +import { Assertions, Gestures, Matchers, Utilities } from '../framework'; + +// ❌ NEVER import from individual files +import Assertions from '../framework/Assertions'; +``` + +## 🎯 Framework Classes Usage + +### Enhanced Assertions + +```typescript +// Modern assertions with descriptions and auto-retry +await Assertions.expectElementToBeVisible(element, { + description: 'submit button should be visible', + timeout: 15000, +}); + +await Assertions.expectTextDisplayed('Success!', { + description: 'success message should appear', +}); + +await Assertions.expectElementToHaveText(input, 'Expected Value', { + description: 'input should contain entered value', +}); +``` + +### Robust Gestures with Element State Configuration + +The framework provides configurable element state checking for optimal performance and reliability: + +```typescript +// Default behavior (recommended for most cases) +// checkVisibility: true, checkEnabled: true, checkStability: false +await Gestures.tap(button, { + description: 'tap submit button', +}); + +// For animated elements - enable stability checking +await Gestures.tap(carouselItem, { + checkStability: true, + description: 'tap carousel item', +}); + +// For loading/disabled elements - skip checks +await Gestures.tap(loadingButton, { + checkEnabled: false, + description: 'tap button during loading', +}); + +// Text input with options +await Gestures.typeText(input, 'Hello World', { + clearFirst: true, + hideKeyboard: true, + description: 'enter username', +}); +``` + +### Type-Safe Element Matching + +```typescript +// Element selectors using framework Matchers +const button = Matchers.getElementByID('send-button'); +const textElement = Matchers.getElementByText('Send'); +const labelElement = Matchers.getElementByLabel('Send Button'); +const webElement = Matchers.getWebViewByID('dapp-webview'); +``` + +### Retry Mechanisms and Utilities + +```typescript +// High-level retry for complex interactions +await Utilities.executeWithRetry( + async () => { + await Gestures.tap(button, { timeout: 2000 }); + await Assertions.expectElementToBeVisible(nextScreen, { timeout: 2000 }); + }, + { + timeout: 30000, + description: 'tap button and verify navigation', + elemDescription: 'Submit Button', + } +); + +// Element state checking utilities +await Utilities.checkElementReadyState(element, { + checkVisibility: true, + checkEnabled: true, + checkStability: false, +}); +``` + +## πŸ§ͺ Test Fixtures and Data Management + +### FixtureBuilder Patterns + +```typescript +// Basic fixture +new FixtureBuilder().build() + +// With popular networks +new FixtureBuilder().withPopularNetworks().build() + +// With Ganache network for local testing +new FixtureBuilder().withGanacheNetwork().build() + +// With connected test dapp +new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp(buildPermissions(['0x539'])) + .build() + +// With pre-configured tokens and contacts +new FixtureBuilder() + .withAddressBookControllerContactBob() + .withTokensControllerERC20() + .build() +``` + +### Advanced withFixtures Configuration + +```typescript +import { DappVariants, LocalNodeType, GanacheHardfork } from '../framework/fixtures/constants'; + +await withFixtures( + { + fixture: new FixtureBuilder().withGanacheNetwork().build(), + restartDevice: true, + + // Configure test dapps + dapps: [ + { dappVariant: DappVariants.MULTICHAIN }, + { dappVariant: DappVariants.TEST_DAPP }, + ], + + // Configure local blockchain nodes + localNodeOptions: [{ + type: LocalNodeType.ganache, + options: { + hardfork: GanacheHardfork.london, + mnemonic: 'WORD1 WORD2 WORD3 WORD4 WORD5 WORD6 WORD7 WORD8 WORD9 WORD10 WORD11 WORD12' + } + }], + + // Test-specific API mocks (see API Mocking Guide) + testSpecificMock: async (mockServer) => { + // Custom mocking logic + }, + + // Additional launch arguments + launchArgs: { + fixtureServerPort: 8545, + }, + }, + async () => { + // Test implementation + }, +); +``` + +## πŸ›οΈ Page Object Integration + +The framework integrates seamlessly with the Page Object Model pattern: + +```typescript +import { Matchers, Gestures, Assertions } from '../../framework'; +import { SendViewSelectors } from './SendView.selectors'; + +class SendView { + // Getter pattern for element references + get sendButton() { + return Matchers.getElementByID(SendViewSelectors.SEND_BUTTON); + } + + get amountInput() { + return Matchers.getElementByID(SendViewSelectors.AMOUNT_INPUT); + } + + // Action methods using framework + async tapSendButton(): Promise { + await Gestures.tap(this.sendButton, { + description: 'tap send button', + }); + } + + async inputAmount(amount: string): Promise { + await Gestures.typeText(this.amountInput, amount, { + clearFirst: true, + description: `input amount: ${amount}`, + }); + } + + // Verification methods + async verifySendButtonVisible(): Promise { + await Assertions.expectElementToBeVisible(this.sendButton, { + description: 'send button should be visible', + }); + } + + // Complex interaction with retry + async sendETHWithRetry(amount: string): Promise { + await Utilities.executeWithRetry( + async () => { + await this.inputAmount(amount); + await this.tapSendButton(); + await Assertions.expectTextDisplayed('Transaction Sent', { + timeout: 2000, + description: 'transaction confirmation should appear', + }); + }, + { + timeout: 30000, + description: 'complete send ETH transaction', + } + ); + } +} + +export default new SendView(); +``` + +## ❌ Framework Anti-Patterns + +### Prohibited Patterns + +```typescript +// ❌ NEVER: Use TestHelpers.delay() +await TestHelpers.delay(5000); + +// ❌ NEVER: Use deprecated methods (marked with @deprecated) +await Assertions.checkIfVisible(element); +await Gestures.tapAndLongPress(element); + +// ❌ NEVER: Import from individual framework files +import Assertions from '../framework/Assertions'; + +// ❌ NEVER: Missing descriptions in framework calls +await Gestures.tap(button); +await Assertions.expectElementToBeVisible(element); + +// ❌ NEVER: Manual retry loops (use framework utilities) +let attempts = 0; +while (attempts < 5) { + try { + await Gestures.tap(button); + break; + } catch { + attempts++; + } +} +``` + +### Correct Framework Usage + +```typescript +// βœ… Use proper waiting with framework assertions +await Assertions.expectElementToBeVisible(element, { + description: 'element should be visible', +}); + +// βœ… Use modern framework methods with descriptions +await Gestures.tap(button, { description: 'tap submit button' }); + +// βœ… Use framework retry mechanisms +await Utilities.executeWithRetry( + async () => { /* operation */ }, + { timeout: 30000, description: 'retry operation' } +); +``` + +## 🚨 Framework Troubleshooting + +### Common Framework Issues + +#### "Element not enabled" Errors +```typescript +// Solution: Skip enabled check for temporarily disabled elements +await Gestures.tap(loadingButton, { + checkEnabled: false, + description: 'tap button during loading state', +}); +``` + +#### "Element moving/animating" Errors +```typescript +// Solution: Enable stability checking for animated elements +await Gestures.tap(animatedButton, { + checkStability: true, + description: 'tap animated carousel button', +}); +``` + +#### Framework Migration Issues +```typescript +// ❌ Old deprecated pattern +await Assertions.checkIfVisible(element, 15000); + +// βœ… New framework pattern +await Assertions.expectElementToBeVisible(element, { + timeout: 15000, + description: 'element should be visible', +}); +``` + +## πŸ”„ Migration from Legacy Framework + +### Migration Status +Current migration phases from [`e2e/framework/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/README.md): + +- βœ… Phase 0: TypeScript framework foundation +- βœ… Phase 1: ESLint for E2E tests +- ⏳ Phase 2: Legacy framework replacement +- ⏳ Phase 3: Gradual test migration + +### For New Tests +- Use TypeScript framework exclusively +- Import from `e2e/framework/index.ts` +- Follow all patterns in this guide +- Use `withFixtures` pattern + +### For Existing Tests +- Gradually migrate to TypeScript framework +- Replace deprecated methods (check `@deprecated` tags) +- Update imports to use framework entry point +- Add `description` parameters to all framework calls + +### Common Migration Patterns + +| Legacy Pattern | Modern Framework Equivalent | +|----------------|----------------------------| +| `TestHelpers.delay(5000)` | `Assertions.expectElementToBeVisible(element, {timeout: 5000})` | +| `checkIfVisible(element, 15000)` | `expectElementToBeVisible(element, {timeout: 15000, description: '...'})` | +| `waitFor(element).toBeVisible()` | `expectElementToBeVisible(element, {description: '...'})` | +| `tapAndLongPress(element)` | `longPress(element, {description: '...'})` | +| `clearField(element); typeText(element, text)` | `typeText(element, text, {clearFirst: true, description: '...'})` | + +## πŸ“š Framework Resources + +- **Framework Documentation**: [`e2e/framework/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/README.md) +- **Setup Guide**: [`docs/readme/testing.md`](https://github.com/MetaMask/metamask-mobile/blob/main/docs/readme/testing.md) +- **API Mocking Guide**: [API Mocking Documentation](./e2e-api-mocking-guide.md) +- **Testing Guidelines**: [`e2e/.cursor/rules/e2e-testing-guidelines.mdc`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/.cursor/rules/e2e-testing-guidelines.mdc) +- **Fixtures Documentation**: [`e2e/framework/fixtures/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/fixtures/README.md) + +## βœ… Framework Checklist + +Before using the framework in tests, ensure: + +- [ ] Uses `withFixtures` pattern for all test setup +- [ ] Imports from `e2e/framework/index.ts` (not individual files) +- [ ] No usage of `TestHelpers.delay()` or deprecated methods +- [ ] All framework calls include descriptive `description` parameters +- [ ] Element state configuration used appropriately (visibility, enabled, stability) +- [ ] FixtureBuilder used for test data setup +- [ ] Framework retry mechanisms used instead of manual loops +- [ ] TypeScript types used for better development experience + +The MetaMask Mobile E2E framework provides a robust, reliable foundation for writing maintainable end-to-end tests. By following these patterns and avoiding anti-patterns, you'll create tests that are both resilient and easy to understand. \ No newline at end of file diff --git a/docs/testing/e2e/mobile-e2e-guidelines.md b/docs/testing/e2e/mobile-e2e-guidelines.md index afad6d58..2c3d014a 100644 --- a/docs/testing/e2e/mobile-e2e-guidelines.md +++ b/docs/testing/e2e/mobile-e2e-guidelines.md @@ -1,10 +1,33 @@ -# MetaMask Mobile E2E Test Guidelines +# MetaMask Mobile E2E Testing Guidelines -Crafting Tests Like a Masterpiece +**Crafting Tests Like a Masterpiece** -In the realm of software testing, writing test code is akin to composing a compelling narrative. Just as a well-constructed story draws readers in with clarity and coherence, high-quality test code should engage developers with its readability and clarity. To ensure that our tests convey their purpose effectively, let's delve into some foundational principles: +In the realm of software testing, writing test code is akin to composing a compelling narrative. Just as a well-constructed story draws readers in with clarity and coherence, high-quality test code should engage developers with its readability and clarity. -## πŸ—Ί Locator Strategy +## πŸ§ͺ Test Classification and Philosophy + +### Are These E2E Tests or System Tests? + +MetaMask Mobile's "E2E" tests are technically **system tests** with controlled dependencies: + +- **System Under Test**: Complete MetaMask Mobile app running on real devices/simulators +- **External Dependencies**: Mocked APIs, controlled blockchain networks (Ganache), test dapps +- **User Interface**: Real UI interactions through Detox framework +- **Data Flow**: Complete user journeys from UI through business logic to mocked external services + +**Why We Call Them E2E**: The terminology reflects the **user perspective** - tests cover complete user workflows from end to end, even though external dependencies are controlled for reliability. + +### Benefits of This Approach + +- **Deterministic**: Mocked APIs ensure consistent test results +- **Fast**: No network latency or external service dependencies +- **Isolated**: Tests don't affect real services or depend on external state +- **Comprehensive**: Full app stack tested with realistic user interactions +- **Maintainable**: Controlled environment reduces flakiness + +To ensure that our tests convey their purpose effectively, let's delve into some foundational principles: + +## πŸ—ΊοΈ Locator Strategy The default strategy for locating elements is using Test IDs. However, in complex scenarios, employ Text or Label-based locators. @@ -35,7 +58,7 @@ device.getPlatform() === 'android' ### Locating Elements byText -βœ… Good: When locating elements by text, retrieve the corresponding text string from the `en.json` file in the `locales/languages` folder. For instance, if you need to interact with `Show Hex Data`, access it as follows: +βœ… Good: When locating elements by text, retrieve the corresponding text string from the `en.json` file in the [`locales/languages`](https://github.com/MetaMask/metamask-mobile/tree/main/locales/languages) folder. For instance, if you need to interact with `Show Hex Data`, access it as follows: ```javascript import en from '../../locales/languages/en.json'; @@ -48,7 +71,7 @@ en.app_settings.show_hex_data; const elementText = 'Show Hex Data'; // Hardcoded text ``` -## πŸ“Œ Naming Convention +## πŸ“Œ Naming Conventions Descriptive and clear names are the cornerstone of maintainable code. Vague, cryptic names leave readers puzzled. @@ -58,6 +81,7 @@ Descriptive and clear names are the cornerstone of maintainable code. Vague, cry ```javascript tapCreateWalletButton() { + // Implementation } ``` @@ -65,6 +89,7 @@ tapCreateWalletButton() { ```javascript tapNTB() { + // What does NTB mean? } ``` @@ -72,6 +97,7 @@ or ```javascript tapBtn() { + // Which button? } ``` @@ -179,66 +205,101 @@ export const AddCustomTokenViewSelectorsText = { const DELETE_WALLET_INPUT_BOX_ID = 'delete-wallet-input-box'; ``` -## Assertions +## ✨ Mobile Custom Framework -To ensure consistency and avoid redundancy in our test scripts, we've implemented an Assertion class. Whenever assertions are needed within tests, it's preferred to utilize methods provided by this class. +To ensure consistency and reliability in our test scripts, we use MetaMask Mobile's E2E framework. For detailed framework usage, see the [E2E Framework Guide](./mobile-e2e-framework-guide.md). -βœ… Good: Utilize assertion methods from the Assertion class to perform assertions in tests. For instance: +βœ… Good: Utilize framework methods with descriptions: -```javascript -await Assertions.checkIfToggleIsOn(SecurityAndPrivacy.metaMetricsToggle); +```typescript +import { Assertions, Gestures, Matchers } from '../framework'; + +await Assertions.expectElementToBeVisible(SecurityAndPrivacy.metaMetricsToggle, { + description: 'MetaMetrics toggle should be visible', +}); + +await Gestures.tap(button, { + description: 'tap create wallet button', +}); ``` -❌ Bad: Refrain from using built-in assertion methods directly within tests, as shown below: +❌ Bad: Using legacy patterns or missing descriptions: ```javascript -await expect(element(by.id(Login - button))).toHaveText(login - text); +await expect(element(by.id('login-button'))).toHaveText('login-text'); +await element(by.id('button')).tap(); // No description ``` -_NOTE: Generally speaking, you don’t need to put an assertion before a test action. You can minimize the amount of assertions by using a test action as an implied assertion on the same element._ +_NOTE: Generally speaking, you don't need to put an assertion before a test action. You can minimize the amount of assertions by using a test action as an implied assertion on the same element._ -## πŸ›  Method Design +## πŸ› οΈ Method Design βœ… Good: Embrace the "Don't Repeat Yourself" (DRY) principle. Reuse existing page-object actions to prevent code duplication and maintain code integrity. Ensure each method has a singular purpose. -❌ Bad: Cluttering methods with multiple tasks and duplicating code, like this: +```typescript +async tapCreateWalletButton(): Promise { + await Gestures.tap(this.createWalletButton, { + description: 'tap create wallet button', + }); +} -```javascript +async verifyWalletCreated(): Promise { + await Assertions.expectTextDisplayed('Wallet Created Successfully', { + description: 'wallet creation confirmation should appear', + }); +} +``` + +❌ Bad: Cluttering methods with multiple responsibilities and duplicating code: +```javascript tapNoThanksButton() { await OnboardingWizard.isVisible(); await TestHelpers.waitAndTap(NO_THANKS_BUTTON_ID); await TestHelpers.isNoThanksButtonNotVisible(); + // Too many responsibilities in one method } ``` -## πŸ“ Comments +## πŸ“ Comments and Documentation βœ… Good: Use JSDoc syntax for adding documentation to clarify complex logic or provide context. Excessive comments can be counterproductive and may signal a need for code simplification. -```javascript +```typescript +/** + * Get element by web ID for dApp interactions. + * + * @param {string} webID - The web ID of the element to locate within the webview + * @return {Promise} Resolves to the located web element + */ +async getElementByWebID(webID: string): Promise { + return Matchers.getWebViewByID(webID); +} /** - * Get element by web ID. - * - * @param {string} webID - The web ID of the element to locate - * @return {Promise} Resolves to the located element - */ - async getElementByWebID(webID) { - return web.element(by.web.id(webID)); - } + * Completes the full send ETH workflow including validation and confirmation. + * Handles network switching if required and validates gas estimation. + * + * @param {string} recipient - The recipient ETH address (must be valid) + * @param {string} amount - The amount to send in ETH (e.g., "0.1") + * @param {boolean} maxGas - Whether to use maximum gas limit + */ +async sendETHTransaction(recipient: string, amount: string, maxGas = false): Promise { + // Complex workflow implementation +} ``` -❌ Bad: Over Commenting simple and self-explanatory code, like this: +❌ Bad: Over commenting simple and self-explanatory code: ```javascript - // This function taps the button tapButton() { - // ... + // Tap the button + await Gestures.tap(this.button); +} ``` -## Creating Page Objects: +## πŸ›οΈ Creating Page Objects Page objects serve as the building blocks of our test suites, providing a clear and organized representation of the elements and interactions within our application. @@ -250,27 +311,54 @@ Establishing a well-defined structure for your page objects is crucial as it fac βœ… Good: Define a clear structure for your page objects, organizing elements and actions in a logical manner. This makes it easy to reuse elements and actions across multiple tests. For example: -##### Page object +#### Page object -```javascript -class SettingsPage { - // Element selectors +```typescript +import { Matchers, Gestures, Assertions } from '../../framework'; +import { SettingsViewSelectors } from './SettingsView.selectors'; + +class SettingsView { + // Element getters get networksButton() { - /*...*/ + return Matchers.getElementByID(SettingsViewSelectors.NETWORKS_BUTTON); } - // Actions - async tapNetworksButton() { - /*...*/ + // Action methods + async tapNetworksButton(): Promise { + await Gestures.tap(this.networksButton, { + description: 'tap networks button', + }); + } + + // Verification methods + async verifySettingsPageVisible(): Promise { + await Assertions.expectElementToBeVisible(this.networksButton, { + description: 'settings page should be visible', + }); } } + +export default new SettingsView(); ``` -##### Spec file: +#### Spec file: -```javascript -it('tap networks button', async () => { - await SettingsView.tapNetworks(); +```typescript +import { withFixtures } from '../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../framework/fixtures/FixtureBuilder'; +import SettingsView from '../pages/Settings/SettingsView'; + +it('navigates to networks settings', async () => { + await withFixtures( + { + fixture: new FixtureBuilder().build(), + restartDevice: true, + }, + async () => { + await SettingsView.verifySettingsPageVisible(); + await SettingsView.tapNetworksButton(); + }, + ); }); ``` @@ -278,39 +366,50 @@ it('tap networks button', async () => { ```javascript it('create a new wallet', async () => { - // Check that Start Exploring CTA is visible & tap it + // Direct element interaction without page objects await TestHelpers.waitAndTap('start-exploring-button'); - // Check that we are on the metametrics optIn screen await TestHelpers.checkIfVisible('metaMetrics-OptIn'); }); ``` ### 🏷️ Meaningful Naming -Good: +βœ… Good: Choose descriptive names for page object properties and methods that accurately convey their purpose: -βœ… Choose descriptive names for page object properties and methods that accurately convey their purpose. For example: - -```javascript -class SettingsPage {} +```typescript +class WalletView { + get sendButton() { /* ... */ } + get receiveButton() { /* ... */ } + + async tapSendButton(): Promise { /* ... */ } + async initiateReceiveFlow(): Promise { /* ... */ } +} ``` -❌ Bad: Unclear or ambiguous names that make it difficult to understand the purpose of the page object: +❌ Bad: Unclear or ambiguous names that make it difficult to understand the purpose: ```javascript -class Screen2 {} +class Screen2 { + get btn1() { /* ... */ } + get thing() { /* ... */ } +} ``` ### πŸ“¦ Encapsulation of Interactions By encapsulating interaction logic within methods, test code does not need to concern itself with the specifics of how interactions are performed. This allows for easier modification of interaction behavior without impacting test scripts. -βœ… Good: Encapsulate interactions with page elements within methods, abstracting away implementation details. For example: - -```javascript -class SettingsPage { - async tapNetworksButton() { - /*...*/ +βœ… Good: Encapsulate interactions with page elements within methods, abstracting away implementation details: + +```typescript +class SettingsView { + async navigateToNetworks(): Promise { + await Gestures.tap(this.networksButton, { + description: 'navigate to networks settings', + }); + await Assertions.expectElementToBeVisible(NetworkView.networkContainer, { + description: 'networks page should load', + }); } } ``` @@ -318,41 +417,51 @@ class SettingsPage { ❌ Bad: Exposing implementation details and directly interacting with elements in test code: ```javascript -// Test code -await TestHelpers.waitAndTap('start-exploring-button'); +// Test code with direct element interaction +await TestHelpers.waitAndTap('networks-button'); +await TestHelpers.checkIfVisible('network-container'); ``` -### βš™οΈ Utilization of Utility Functions +### βš™οΈ Utilization of Modern Framework -Using utility functions is a reliable way to interact with page elements consistently across various tests. By centralizing the interaction logic and removing low-level details, utility functions improve readability and enhance the scalability of tests. +Using the modern TypeScript framework is essential for consistent and reliable interactions across tests. The framework centralizes interaction logic and removes low-level details, improving readability and enhancing test scalability. -βœ… Good: Leverage utility functions to interact with page elements consistently across tests: +βœ… Good: Leverage the modern framework for all page object interactions: -```javascript -import Matchers from '../../utils/Matchers'; -import Gestures from '../../utils/Gestures'; +```typescript +import { Matchers, Gestures, Assertions } from '../../framework'; +import { NetworksViewSelectors } from './NetworksView.selectors'; -class SettingsPage { +class NetworksView { get networksButton() { - return Matchers.getElementByID('ELEMENT-STRING'); + return Matchers.getElementByID(NetworksViewSelectors.NETWORKS_BUTTON); + } + + async tapNetworksButton(): Promise { + await Gestures.tap(this.networksButton, { + description: 'tap networks button', + }); } - async tapNetworksButton() { - await Gestures.waitAndTap(this.networksButton); + async verifyNetworksVisible(): Promise { + await Assertions.expectElementToBeVisible(this.networksButton, { + description: 'networks button should be visible', + }); } } ``` -_NOTE:_ Matchers and Gestures are fundamental components of our test actions. +_NOTE:_ The modern framework components (`Matchers`, `Gestures`, `Assertions`) are fundamental to our test reliability and maintainability. -Matchers Utility Class: Handles element identification and matching logic, ensuring consistency in element location across tests. This promotes consistency and reduces code duplication. +- **Matchers**: Handles element identification with type safety and platform compatibility +- **Gestures**: Manages user interactions with configurable state checking and auto-retry +- **Assertions**: Provides enhanced verification with descriptive error messages and retry logic -Gestures Utility Class: Manages user interactions with page elements, such as tapping, swiping, or scrolling. It enhances test readability by providing descriptive methods for common user interactions. - -❌ Bad: Implementing interactions directly in test code without using utility functions. For example: +❌ Bad: Using legacy patterns or direct Detox calls without the framework: ```javascript -await element(by.id('ELEMENT-STRING')).tap(); +await element(by.id('networks-button')).tap(); +await waitFor(element(by.id('network-container'))).toBeVisible(); ``` ### πŸ—ƒοΈ Using Getters for Element Storage @@ -361,23 +470,30 @@ In our page objects, we utilize getters for element storage instead of defining βœ… Good: By encapsulating element selectors within getters, we ensure that elements are requested immediately before any action is taken on them. This approach promotes a more dynamic and responsive interaction with the page elements, enhancing the reliability and robustness of our tests. -For example: - -```javascript -import { NetworksViewSelectorsIDs } from '../../selectors/Settings/NetworksView.selectors'; +```typescript +import { NetworksViewSelectors } from '../../selectors/Settings/NetworksView.selectors'; +import { Matchers } from '../../framework'; -class SettingsPage { +class NetworksView { get networksButton() { - return Matchers.getElementByID(SettingsViewSelectorsIDs.NETWORKS); + return Matchers.getElementByID(NetworksViewSelectors.NETWORKS_BUTTON); } - async tapNetworksButton() { - await Gestures.waitAndTap(this.networksButton); + get addNetworkButton() { + return device.getPlatform() === 'ios' + ? Matchers.getElementByID(NetworksViewSelectors.ADD_NETWORK_BUTTON) + : Matchers.getElementByLabel(NetworksViewSelectors.ADD_NETWORK_BUTTON); + } + + async tapNetworksButton(): Promise { + await Gestures.tap(this.networksButton, { + description: 'tap networks button', + }); } } ``` -❌ Bad: Defining element selectors as constants and then interacting with them within test actions can lead to potential issues with element staleness and unexpected behavior. +❌ Bad: Defining element selectors as constants can lead to potential issues with element staleness and unexpected behavior: ```javascript const CONFIRM_BUTTON_ID = 'contract-name-confirm-button'; @@ -389,18 +505,18 @@ export default class ContractNickNameView { } ``` -## A Comprehensive Guide on Implementing Page Objects for Test Scenarios +## πŸ“– A Comprehensive Guide on Implementing Page Objects This guide provides a comprehensive overview of implementing the concepts discussed earlier to create a page object for use in a test scenario. By following this guide, you will gain a deeper insight into our approach to writing tests, enabling you to produce test code that is both maintainable and reusable. -#### Step 1: Define Selector Objects for NetworksView +### Step 1: Define Selector Objects for NetworksView -Start by creating selector objects. These objects store identifiers for UI elements, making your tests easier to read and maintain. We'll use two types of selectors: IDs and Text. Let's call this file: `NetworksView.selectors.js` +Start by creating selector objects. These objects store identifiers for UI elements, making your tests easier to read and maintain. We'll use two types of selectors: IDs and Text. Let's call this file: [`NetworksView.selectors.ts`](https://github.com/MetaMask/metamask-mobile/tree/main/e2e/selectors/Settings) -```javascript +```typescript import enContent from '../../../locales/languages/en.json'; -export const NetworksViewSelectorsIDs = { +export const NetworksViewSelectors = { RPC_CONTAINER: 'new-rpc-screen', ADD_NETWORKS_BUTTON: 'add-network-button', NETWORK_NAME_INPUT: 'input-network-name', @@ -421,130 +537,253 @@ export const NetworkViewSelectorsText = { }; ``` -#### Step 2: Create the Networks Page Object +### Step 2: Create the Networks Page Object -The Networks Page Object encapsulates interactions with the NetworksView. It uses the selectors defined in _Step 1_ and utility functions for actions like typing and tapping. Methods within the page object hide the complexity of direct UI interactions, offering a simpler interface for test scripts. Use Matchers for finding elements and Gestures for performing actions, enhancing code reusability. +The Networks Page Object encapsulates interactions with the NetworksView using the modern framework. It uses the selectors defined in _Step 1_ and framework utilities for enhanced reliability. -```javascript +```typescript import { - NetworksViewSelectorsIDs, + NetworksViewSelectors, NetworkViewSelectorsText, } from '../../selectors/Settings/NetworksView.selectors'; -import Matchers from '../../utils/Matchers'; -import Gestures from '../../utils/Gestures'; +import { Matchers, Gestures, Assertions } from '../../framework'; class NetworkView { get networkContainer() { - return Matchers.getElementByID(NetworksViewSelectorsIDs.NETWORK_CONTAINER); + return Matchers.getElementByID(NetworksViewSelectors.NETWORK_CONTAINER); } get addNetworkButton() { return device.getPlatform() === 'ios' - ? Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_NETWORKS_BUTTON) - : Matchers.getElementByLabel( - NetworksViewSelectorsIDs.ADD_NETWORKS_BUTTON, - ); - } - - get rpcAddButton() { - return device.getPlatform() === 'android' - ? Matchers.getElementByLabel( - NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON, - ) - : Matchers.getElementByID( - NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON, - ); + ? Matchers.getElementByID(NetworksViewSelectors.ADD_NETWORKS_BUTTON) + : Matchers.getElementByLabel(NetworksViewSelectors.ADD_NETWORKS_BUTTON); } get customNetworkTab() { - return Matchers.getElementByText( - NetworkViewSelectorsText.CUSTOM_NETWORK_TAB, - ); + return Matchers.getElementByText(NetworkViewSelectorsText.CUSTOM_NETWORK_TAB); } get rpcWarningBanner() { - return Matchers.getElementByID(NetworksViewSelectorsIDs.RPC_WARNING_BANNER); + return Matchers.getElementByID(NetworksViewSelectors.RPC_WARNING_BANNER); } get rpcURLInput() { - return Matchers.getElementByID(NetworksViewSelectorsIDs.RPC_URL_INPUT); + return Matchers.getElementByID(NetworksViewSelectors.RPC_URL_INPUT); } get networkNameInput() { - return Matchers.getElementByID(NetworksViewSelectorsIDs.NETWORK_NAME_INPUT); + return Matchers.getElementByID(NetworksViewSelectors.NETWORK_NAME_INPUT); } get chainIDInput() { - return Matchers.getElementByID(NetworksViewSelectorsIDs.CHAIN_INPUT); + return Matchers.getElementByID(NetworksViewSelectors.CHAIN_INPUT); } get networkSymbolInput() { - return Matchers.getElementByID( - NetworksViewSelectorsIDs.NETWORKS_SYMBOL_INPUT, - ); + return Matchers.getElementByID(NetworksViewSelectors.NETWORKS_SYMBOL_INPUT); } - async typeInNetworkName(networkName) { - await Gestures.typeTextAndHideKeyboard(this.networkNameInput, networkName); + async typeInNetworkName(networkName: string): Promise { + await Gestures.typeText(this.networkNameInput, networkName, { + clearFirst: true, + hideKeyboard: true, + description: `enter network name: ${networkName}`, + }); } - async typeInRpcUrl(rPCUrl) { - await Gestures.typeTextAndHideKeyboard(this.rpcURLInput, rPCUrl); + async typeInRpcUrl(rpcUrl: string): Promise { + await Gestures.typeText(this.rpcURLInput, rpcUrl, { + clearFirst: true, + hideKeyboard: true, + description: `enter RPC URL: ${rpcUrl}`, + }); } - async clearRpcInputBox() { - await Gestures.clearField(this.rpcURLInput); + async clearRpcInputBox(): Promise { + await Gestures.typeText(this.rpcURLInput, '', { + clearFirst: true, + description: 'clear RPC URL input', + }); } - async tapAddNetworkButton() { - await Gestures.waitAndTap(this.addNetworkButton); + async tapAddNetworkButton(): Promise { + await Gestures.tap(this.addNetworkButton, { + description: 'tap add network button', + }); } - async switchToCustomNetworks() { - await Gestures.waitAndTap(this.customNetworkTab); + async switchToCustomNetworks(): Promise { + await Gestures.tap(this.customNetworkTab, { + description: 'switch to custom networks tab', + }); } - async typeInChainId(chainID) { - await Gestures.typeTextAndHideKeyboard(this.chainIDInput, chainID); + async typeInChainId(chainID: string): Promise { + await Gestures.typeText(this.chainIDInput, chainID, { + clearFirst: true, + hideKeyboard: true, + description: `enter chain ID: ${chainID}`, + }); } - async typeInNetworkSymbol(networkSymbol) { - await Gestures.typeTextAndHideKeyboard( - this.networkSymbolInput, - networkSymbol, - ); + async typeInNetworkSymbol(networkSymbol: string): Promise { + await Gestures.typeText(this.networkSymbolInput, networkSymbol, { + clearFirst: true, + hideKeyboard: true, + description: `enter network symbol: ${networkSymbol}`, + }); } - async tapRpcNetworkAddButton() { - await Gestures.waitAndTap(this.rpcAddButton); + async verifyRpcWarningVisible(): Promise { + await Assertions.expectElementToBeVisible(this.rpcWarningBanner, { + description: 'RPC warning banner should be visible', + }); } } export default new NetworkView(); ``` -#### Step 3: Utilize Page Objects in Test Specifications +### Step 3: Page Objects -With the page object in place, writing test specifications becomes more straightforward. The test scripts interact with the application through the page object's interface, improving readability and maintainability. +With the page object in place and using the modern framework with `withFixtures`, writing test specifications becomes more straightforward and reliable. #### Example Test Case: Adding a Network -```javascript +```typescript +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../../viewHelper'; import NetworkView from '../../pages/Settings/NetworksView'; -import Assertions from '../../utils/Assertions'; -import Networks from '../../resources/networks.json'; - -it('add Gnosis network', async () => { - // Tap on Add Network button - await NetworkView.tapAddNetworkButton(); - await NetworkView.switchToCustomNetworks(); - await NetworkView.typeInNetworkName(Networks.Gnosis.providerConfig.nickname); - await NetworkView.typeInRpcUrl('abc'); // Negative test. Input incorrect RPC URL - await Assertions.checkIfVisible(NetworkView.rpcWarningBanner); - await NetworkView.clearRpcInputBox(); - await NetworkView.typeInRpcUrl(Networks.Gnosis.providerConfig.rpcUrl); - await NetworkView.typeInChainId(Networks.Gnosis.providerConfig.chainId); - await NetworkView.typeInNetworkSymbol(Networks.Gnosis.providerConfig.ticker); - await NetworkView.tapRpcNetworkAddButton(); +import { SmokeNetworks } from '../../tags'; + +describe(SmokeNetworks('Network Management'), () => { + beforeAll(async () => { + jest.setTimeout(150000); + }); + + it('adds custom network with validation', async () => { + await withFixtures( + { + fixture: new FixtureBuilder().build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + + // Navigate and start adding network + await NetworkView.tapAddNetworkButton(); + await NetworkView.switchToCustomNetworks(); + + // Enter network details with validation + await NetworkView.typeInNetworkName('Gnosis Test Network'); + + // Test validation with incorrect RPC URL + await NetworkView.typeInRpcUrl('invalid-url'); + await NetworkView.verifyRpcWarningVisible(); + + // Clear and enter correct RPC URL + await NetworkView.clearRpcInputBox(); + await NetworkView.typeInRpcUrl('https://rpc.gnosischain.com'); + await NetworkView.typeInChainId('100'); + await NetworkView.typeInNetworkSymbol('GNO'); + }, + ); + }); +}); +``` + +## πŸ§ͺ Test Design Philosophy + +### Test Atomicity and Coupling + +#### When to Isolate Tests: +- Testing specific functionality of a single component or feature +- When you need to pinpoint exact failure causes +- For basic unit-level behaviors + +#### When to Combine Tests: +- For multi-step user flows that represent real user behavior +- When testing how different parts of the application work together +- When the setup for multiple tests is time-consuming and identical + +#### Guidelines: +- Each test should run with a dedicated app instance and controlled environment +- Use `withFixtures` to create test prerequisites and clean up afterward +- Control application state programmatically rather than through UI interactions +- Consider the "fail-fast" philosophy - if an initial step fails, subsequent steps may not need to run + +### Controlling State + +#### Best Practices: +- Control application state through fixtures rather than UI interactions +- Use `FixtureBuilder` to set up test prerequisites instead of UI steps +- Minimize UI interactions to reduce potential breaking points +- Improve test stability by reducing timing and synchronization issues + +#### Example: +```typescript +// βœ… Good: Use fixture to set up prerequisites +const fixture = new FixtureBuilder() + .withAddressBookControllerContactBob() + .withTokensControllerERC20() + .build(); + +await withFixtures({ fixture }, async () => { + await loginToApp(); + // Test only the essential steps: + // - Navigate to send flow + // - Select contact from address book + // - Send TST token + // - Verify transaction +}); + +// ❌ Bad: Building all state through UI +await withFixtures({ fixture: new FixtureBuilder().build() }, async () => { + await loginToApp(); + // All these steps add complexity and potential failure points: + // - Navigate to contacts + // - Add contact manually + // - Navigate to dapp + // - Connect to dapp + // - Deploy token contract + // - Add token to wallet + // - Navigate to send + // - Send token }); ``` + +## 🎯 Test Quality Principles + +### Reliability +- Tests should consistently produce the same results +- Use controlled environments and mocked external dependencies +- Implement proper retry mechanisms through the framework +- Handle expected failures gracefully + +### Maintainability +- Follow Page Object Model pattern consistently +- Use descriptive naming throughout +- Keep tests focused on specific behaviors +- Minimize code duplication through reusable page objects + +### Readability +- Write tests that tell a story of user behavior +- Use meaningful descriptions in all framework calls +- Structure tests logically with clear arrange-act-assert patterns +- Document complex test scenarios with comments when necessary + +### Speed +- Use `withFixtures` for efficient test setup +- Minimize unnecessary UI interactions +- Leverage framework optimizations (like `checkStability: false` by default) +- Group related assertions to reduce redundant operations + +## πŸ“š Additional Resources + +- **Framework Usage Guide**: [E2E Framework Guide](./mobile-e2e-framework-guide.md) +- **API Mocking Guide**: [E2E API Mocking Guide](./mobile-e2e-api-mocking-guide.md) +- **Setup Documentation**: [`docs/readme/testing.md`](https://github.com/MetaMask/metamask-mobile/blob/main/docs/readme/testing.md) +- **Framework Documentation**: [`e2e/framework/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/README.md) + +By following these timeless testing principles alongside the modern MetaMask Mobile framework, you'll create test suites that are not only functional but also serve as living documentation of how the application should behave from a user's perspective. \ No newline at end of file diff --git a/docs/testing/e2e/mobile-e2e-mocking.md b/docs/testing/e2e/mobile-e2e-mocking.md deleted file mode 100644 index e824d0b6..00000000 --- a/docs/testing/e2e/mobile-e2e-mocking.md +++ /dev/null @@ -1,173 +0,0 @@ -# Mocking APIs in MetaMask Mobile for Enhanced E2E Testing - -## Introduction - -This document outlines how MetaMask Mobile uses API mocking to boost End-to-End (E2E) testing. Mocking lets us simulate different conditions that the app might face, ensuring it functions reliably across various scenarios. - -## Mocking vs. E2E Testing - -While E2E tests verify the overall user experience, mocking focuses on testing individual components. Here’s how they differ: - -- **E2E Tests**: These validate the app's functionality as a whole, interacting with real APIs and backend services. -- **Mocking**: Simulates responses from APIs, allowing us to test components in specific network conditions or edge cases. - -We use mocking to enhance our E2E testing, especially for scenarios where E2E alone might be unreliable or tricky to set up. - -## File Structure - -We keep E2E and mock tests separate with file naming conventions: - -``` -root/ -β”œβ”€β”€ e2e/ -β”‚ β”œβ”€β”€ spec/ -β”‚ β”‚ β”œβ”€β”€ Accounts/ -β”‚ β”‚ β”‚ └── spec.js - mock.spec.js# Mock test for Accounts -β”‚ β”‚ β”œβ”€β”€ Transactions/ -β”‚ β”‚ β”‚ └── spec.js - mock.spec.js # Mock test for Transactions -β”‚ β”œβ”€β”€ api-mocking/ -β”‚ β”œβ”€β”€ api-mocking/ -β”‚ β”‚ β”œβ”€β”€ mock-responses/ -β”‚ β”‚ β”‚ β”œβ”€β”€ gas-api-responses.json -``` - -This structure promotes clear organisation and makes managing tests simpler. - -## Mock Server Implementation - -### Overview - -We use Mockttp to simulate API responses, providing flexible testing across different HTTP methods. This allows us to test app behaviour even when external dependencies are unavailable or unreliable. - -### Key Features - -- Supports multiple HTTP methods (GET, POST, etc.) -- Configurable requests and responses -- Logs responses to simplify debugging - -### Naming Mock Test Files - -Mock test files are named with `.mock.spec.js` to keep things organised. For example, a test for the suggested gas API would be named: `suggestedGasApi.mock.spec.js`. - -### Setting Up the Mock Server - -The `startMockServer` function in `e2e/api-mocking/mock-server.js` starts the mock server. It takes events organised by HTTP methods, specifying the endpoint, response data, and request body (for POST requests). - -```javascript -import { mockEvents } from '../api-mocking/mock-config/mock-events'; - -mockServer = await startMockServer({ - GET: [mockEvents.GET.suggestedGasApiErrorResponse], - POST: [mockEvents.POST.suggestedGasApiPostResponse], -}); -``` - -### Defining Mock Events - -`mockEvents.js` defines mock events, including: - -- `urlEndpoint`: The API endpoint being mocked -- `response`: The mock response the server will return -- `requestBody`: Expected request body (for POST requests) - -```javascript -export const mockEvents = { - GET: { - suggestedGasApiErrorResponse: { - urlEndpoint: 'https://gas.api.cx.metamask.io/networks/1/suggestedGasFees', - response: { status: 500, message: 'Internal Server Error' }, - }, - }, - POST: { - suggestedGasApiPostResponse: { - urlEndpoint: 'https://gas.api.cx.metamask.io/networks/1/suggestedGasFees', - response: { status: 200, message: 'Success' }, - requestBody: { priorityFee: '2', maxFee: '2.000855333' }, - }, - }, -}; -``` - -### Response Structure - -Mock responses are stored in individual JSON files for each API or service within the `mock-responses` folder, making them easier to maintain and manage. Each API service has its own JSON response file, such as `gasApiResponse.json` for gas-related responses and `ethpriceResponse.json` for Ethereum price responses. This organisation enables clear separation of mock data and simplifies updates or additions. - -**Example:** `gasApiResponse.json` - -```json -{ - "suggestedGasApiResponses": { - "error": { - "message": "Internal Server Error" - } - }, - "suggestedGasFeesApiGanache": { - // ... detailed gas fee data ... - } -} -``` - -## Logging - -The mock server logs response statuses and bodies to help track mocked requests, making debugging more straightforward. - -## Using Mock Testing Effectively - -### When to Use Mocks: - -- For testing isolated features without relying on live data -- For testing edge cases that are tricky to reproduce with real data -- For deterministic test results by controlling inputs and outputs - -### When Not to Use Mocks: - -- Stable Live Environments: When APIs and services are reliable, testing live ensures production-like accuracy. -- Integration Testing: Live tests validate interactions with third-party services, capturing real-world behaviour. -- Performance Testing: Only live environments provide accurate latency and throughput metrics. -- Dynamic Data Scenarios: Features relying on user data or complex workflows may reveal issues that mocks miss. - -There should be a mix of tests that verify real-life services and some that use mocks, when applicable, to achieve comprehensive coverage. - -### Utilizing Fixtures with testSpecificMock - -For more complex mock events or criteria, you can use the `mockSpecificTest` object to define custom mock events. - -When using fixtures for E2E tests, you can leverage the `testSpecificMock` object to inject specific mocks into your test cases. This allows you to dynamically modify mock responses based on the scenario under test. For example: - -```javascript -test.use({ - testSpecificMock: { - GET: [ - { - urlEndpoint: - 'https://gas.api.cx.metamask.io/networks/1/suggestedGasFees', - response: { status: 200, message: 'Custom Success Response' }, - }, - ], - }, -}); -``` - -This approach is particularly useful for targeted scenarios where pre-defined mock events in `mockEvents.js` might not be sufficient or applicable. - -For example, see this [GitHub Reference](https://github.com/MetaMask/metamask-mobile/blob/d1946d2037399356b7b582b07e09b0528a68e0ac/e2e/specs/confirmations/advanced-gas-fees.mock.spec.js#L30). - -### When mockEvents May Not Be Suitable - -In certain cases, using `mockEvents` might not be idealβ€”for instance, when: - -- The mock responses need to be dynamically created based on the test input. -- The scenario being tested is unique and requires a one-off response that doesn’t fit the general structure of `mockEvents`. -- Reusing existing mocks may lead to inconsistencies in the expected results. - -In such cases, `testSpecificMock` is a more suitable option, allowing you to define bespoke mocks for individual tests. - -### Current Workaround for Network Request Interception - -Due to limitations in intercepting network requests directly, we currently route traffic through a proxy server. This lets us intercept and mock requests as needed. - -## Future Improvements - -We’re looking into further enhancements for our mocking setup to simplify processes and improve test coverage. From c816c59bd2666dd45ac653bc4009c08aaa8d399a Mon Sep 17 00:00:00 2001 From: cmd-ob Date: Mon, 1 Sep 2025 12:13:18 +0100 Subject: [PATCH 2/2] run prettier --- docs/testing/e2e/mobile-e2e-api-mocking.md | 50 ++++++----- .../testing/e2e/mobile-e2e-framework-guide.md | 83 +++++++++++-------- docs/testing/e2e/mobile-e2e-guidelines.md | 56 +++++++++---- 3 files changed, 121 insertions(+), 68 deletions(-) diff --git a/docs/testing/e2e/mobile-e2e-api-mocking.md b/docs/testing/e2e/mobile-e2e-api-mocking.md index 98aecd2b..5b4541bf 100644 --- a/docs/testing/e2e/mobile-e2e-api-mocking.md +++ b/docs/testing/e2e/mobile-e2e-api-mocking.md @@ -68,7 +68,9 @@ export const DEFAULT_MOCKS = { ], }; ``` + j + ### Adding New Default Mocks To add default mocks that benefit all tests: @@ -209,10 +211,10 @@ import { setupMockPostRequest } from '../api-mocking/helpers/mockHelpers'; await setupMockPostRequest( mockServer, 'https://api.example.com/validate', - { + { method: 'transfer', amount: '1000000000000000000', - to: '0x742d35cc6634c0532925a3b8d0ea4405abf5adf3' + to: '0x742d35cc6634c0532925a3b8d0ea4405abf5adf3', }, // Expected request body { result: 'validated', txId: '0x123...' }, // Mock response { @@ -232,7 +234,7 @@ await setupMockRequest(mockServer, { response: { data: 'secure-data' }, responseCode: 200, headers: { - 'Authorization': 'Bearer mock-token', + Authorization: 'Bearer mock-token', 'Content-Type': 'application/json', }, }); @@ -313,10 +315,14 @@ await setupRemoteFeatureFlagsMock(mockServer, { }); // Environment-specific flags -await setupRemoteFeatureFlagsMock(mockServer, { - devOnlyFeature: true, - prodOptimization: false, -}, 'flask'); // Flask distribution +await setupRemoteFeatureFlagsMock( + mockServer, + { + devOnlyFeature: true, + prodOptimization: false, + }, + 'flask', +); // Flask distribution ``` ## 🎭 Complete Mocking Examples @@ -337,8 +343,9 @@ describe(SmokeE2E('Token Price Display'), () => { requestMethod: 'GET', url: 'https://price-api.metamask.io/v2/chains/1/spot-prices', response: { - '0x0000000000000000000000000000000000000000': { // ETH - price: 2500.50, + '0x0000000000000000000000000000000000000000': { + // ETH + price: 2500.5, currency: 'usd', }, }, @@ -415,7 +422,9 @@ const setupSwapMocks = async (mockServer: Mockttp) => { await setupMockRequest(mockServer, { requestMethod: 'GET', url: 'https://price-api.metamask.io/v2/chains/1/spot-prices', - response: { /* price data */ }, + response: { + /* price data */ + }, responseCode: 200, }); @@ -423,7 +432,9 @@ const setupSwapMocks = async (mockServer: Mockttp) => { await setupMockRequest(mockServer, { requestMethod: 'GET', url: /^https:\/\/swap-api\.metamask\.io\/networks\/1\/trades/, - response: { /* quote data */ }, + response: { + /* quote data */ + }, responseCode: 200, }); @@ -431,7 +442,9 @@ const setupSwapMocks = async (mockServer: Mockttp) => { await setupMockRequest(mockServer, { requestMethod: 'GET', url: 'https://gas-api.metamask.io/networks/1/gasPrices', - response: { /* gas prices */ }, + response: { + /* gas prices */ + }, responseCode: 200, }); }; @@ -453,13 +466,13 @@ await withFixtures( ```typescript // βœ… Good - specific endpoint -url: 'https://api.metamask.io/v2/chains/1/spot-prices' +url: 'https://api.metamask.io/v2/chains/1/spot-prices'; // βœ… Better - regex for dynamic parts -url: /^https:\/\/api\.metamask\.io\/v2\/chains\/\d+\/spot-prices$/ +url: /^https:\/\/api\.metamask\.io\/v2\/chains\/\d+\/spot-prices$/; // ❌ Avoid - too broad, may interfere with other requests -url: 'metamask.io' +url: 'metamask.io'; ``` ### 2. Handle Request Bodies Properly @@ -513,7 +526,6 @@ The mock server automatically tracks and logs: - **Request/response timing** information - **Feature flag configurations** applied - ### Common Debugging Steps 1. **Check test output** for mock-related warnings @@ -542,10 +554,10 @@ Enable debug logging to see mock activity: ```typescript // ❌ Too specific - might miss query parameters -url: 'https://api.example.com/data' +url: 'https://api.example.com/data'; // βœ… More flexible pattern -url: /^https:\/\/api\.example\.com\/data(\?.*)?$/ +url: /^https:\/\/api\.example\.com\/data(\?.*)?$/; ``` ### POST Body Validation Failing @@ -599,4 +611,4 @@ Before submitting tests with custom mocks: - [ ] No hardcoded values that should come from constants - [ ] Error scenarios mocked when testing error handling -The MetaMask Mobile API mocking system provides comprehensive control over network requests, enabling reliable and deterministic E2E tests. By following these patterns, you'll create tests that are both isolated and realistic. \ No newline at end of file +The MetaMask Mobile API mocking system provides comprehensive control over network requests, enabling reliable and deterministic E2E tests. By following these patterns, you'll create tests that are both isolated and realistic. diff --git a/docs/testing/e2e/mobile-e2e-framework-guide.md b/docs/testing/e2e/mobile-e2e-framework-guide.md index 7711ab45..0216ffa0 100644 --- a/docs/testing/e2e/mobile-e2e-framework-guide.md +++ b/docs/testing/e2e/mobile-e2e-framework-guide.md @@ -25,7 +25,7 @@ MetaMask Mobile uses a modern TypeScript-based E2E testing framework built on De ``` e2e/framework/ # Modern TypeScript framework -β”œβ”€β”€ Assertions.ts # Enhanced assertions with auto-retry +β”œβ”€β”€ Assertions.ts # Enhanced assertions with auto-retry β”œβ”€β”€ Gestures.ts # Robust user interactions β”œβ”€β”€ Matchers.ts # Type-safe element selectors β”œβ”€β”€ Utilities.ts # Core utilities with retry mechanisms @@ -61,7 +61,7 @@ describe(SmokeE2E('Feature Name'), () => { }, async () => { await loginToApp(); - + // Test implementation here }, ); @@ -153,7 +153,7 @@ await Utilities.executeWithRetry( timeout: 30000, description: 'tap button and verify navigation', elemDescription: 'Submit Button', - } + }, ); // Element state checking utilities @@ -170,56 +170,63 @@ await Utilities.checkElementReadyState(element, { ```typescript // Basic fixture -new FixtureBuilder().build() +new FixtureBuilder().build(); // With popular networks -new FixtureBuilder().withPopularNetworks().build() +new FixtureBuilder().withPopularNetworks().build(); // With Ganache network for local testing -new FixtureBuilder().withGanacheNetwork().build() +new FixtureBuilder().withGanacheNetwork().build(); // With connected test dapp new FixtureBuilder() .withPermissionControllerConnectedToTestDapp(buildPermissions(['0x539'])) - .build() + .build(); // With pre-configured tokens and contacts new FixtureBuilder() .withAddressBookControllerContactBob() .withTokensControllerERC20() - .build() + .build(); ``` ### Advanced withFixtures Configuration ```typescript -import { DappVariants, LocalNodeType, GanacheHardfork } from '../framework/fixtures/constants'; +import { + DappVariants, + LocalNodeType, + GanacheHardfork, +} from '../framework/fixtures/constants'; await withFixtures( { fixture: new FixtureBuilder().withGanacheNetwork().build(), restartDevice: true, - + // Configure test dapps dapps: [ { dappVariant: DappVariants.MULTICHAIN }, { dappVariant: DappVariants.TEST_DAPP }, ], - + // Configure local blockchain nodes - localNodeOptions: [{ - type: LocalNodeType.ganache, - options: { - hardfork: GanacheHardfork.london, - mnemonic: 'WORD1 WORD2 WORD3 WORD4 WORD5 WORD6 WORD7 WORD8 WORD9 WORD10 WORD11 WORD12' - } - }], - + localNodeOptions: [ + { + type: LocalNodeType.ganache, + options: { + hardfork: GanacheHardfork.london, + mnemonic: + 'WORD1 WORD2 WORD3 WORD4 WORD5 WORD6 WORD7 WORD8 WORD9 WORD10 WORD11 WORD12', + }, + }, + ], + // Test-specific API mocks (see API Mocking Guide) testSpecificMock: async (mockServer) => { // Custom mocking logic }, - + // Additional launch arguments launchArgs: { fixtureServerPort: 8545, @@ -244,7 +251,7 @@ class SendView { get sendButton() { return Matchers.getElementByID(SendViewSelectors.SEND_BUTTON); } - + get amountInput() { return Matchers.getElementByID(SendViewSelectors.AMOUNT_INPUT); } @@ -269,7 +276,7 @@ class SendView { description: 'send button should be visible', }); } - + // Complex interaction with retry async sendETHWithRetry(amount: string): Promise { await Utilities.executeWithRetry( @@ -284,7 +291,7 @@ class SendView { { timeout: 30000, description: 'complete send ETH transaction', - } + }, ); } } @@ -336,8 +343,10 @@ await Gestures.tap(button, { description: 'tap submit button' }); // βœ… Use framework retry mechanisms await Utilities.executeWithRetry( - async () => { /* operation */ }, - { timeout: 30000, description: 'retry operation' } + async () => { + /* operation */ + }, + { timeout: 30000, description: 'retry operation' }, ); ``` @@ -346,6 +355,7 @@ await Utilities.executeWithRetry( ### Common Framework Issues #### "Element not enabled" Errors + ```typescript // Solution: Skip enabled check for temporarily disabled elements await Gestures.tap(loadingButton, { @@ -355,6 +365,7 @@ await Gestures.tap(loadingButton, { ``` #### "Element moving/animating" Errors + ```typescript // Solution: Enable stability checking for animated elements await Gestures.tap(animatedButton, { @@ -364,6 +375,7 @@ await Gestures.tap(animatedButton, { ``` #### Framework Migration Issues + ```typescript // ❌ Old deprecated pattern await Assertions.checkIfVisible(element, 15000); @@ -378,20 +390,23 @@ await Assertions.expectElementToBeVisible(element, { ## πŸ”„ Migration from Legacy Framework ### Migration Status + Current migration phases from [`e2e/framework/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/README.md): - βœ… Phase 0: TypeScript framework foundation -- βœ… Phase 1: ESLint for E2E tests +- βœ… Phase 1: ESLint for E2E tests - ⏳ Phase 2: Legacy framework replacement - ⏳ Phase 3: Gradual test migration ### For New Tests + - Use TypeScript framework exclusively - Import from `e2e/framework/index.ts` - Follow all patterns in this guide - Use `withFixtures` pattern ### For Existing Tests + - Gradually migrate to TypeScript framework - Replace deprecated methods (check `@deprecated` tags) - Update imports to use framework entry point @@ -399,13 +414,13 @@ Current migration phases from [`e2e/framework/README.md`](https://github.com/Met ### Common Migration Patterns -| Legacy Pattern | Modern Framework Equivalent | -|----------------|----------------------------| -| `TestHelpers.delay(5000)` | `Assertions.expectElementToBeVisible(element, {timeout: 5000})` | -| `checkIfVisible(element, 15000)` | `expectElementToBeVisible(element, {timeout: 15000, description: '...'})` | -| `waitFor(element).toBeVisible()` | `expectElementToBeVisible(element, {description: '...'})` | -| `tapAndLongPress(element)` | `longPress(element, {description: '...'})` | -| `clearField(element); typeText(element, text)` | `typeText(element, text, {clearFirst: true, description: '...'})` | +| Legacy Pattern | Modern Framework Equivalent | +| ---------------------------------------------- | ------------------------------------------------------------------------- | +| `TestHelpers.delay(5000)` | `Assertions.expectElementToBeVisible(element, {timeout: 5000})` | +| `checkIfVisible(element, 15000)` | `expectElementToBeVisible(element, {timeout: 15000, description: '...'})` | +| `waitFor(element).toBeVisible()` | `expectElementToBeVisible(element, {description: '...'})` | +| `tapAndLongPress(element)` | `longPress(element, {description: '...'})` | +| `clearField(element); typeText(element, text)` | `typeText(element, text, {clearFirst: true, description: '...'})` | ## πŸ“š Framework Resources @@ -428,4 +443,4 @@ Before using the framework in tests, ensure: - [ ] Framework retry mechanisms used instead of manual loops - [ ] TypeScript types used for better development experience -The MetaMask Mobile E2E framework provides a robust, reliable foundation for writing maintainable end-to-end tests. By following these patterns and avoiding anti-patterns, you'll create tests that are both resilient and easy to understand. \ No newline at end of file +The MetaMask Mobile E2E framework provides a robust, reliable foundation for writing maintainable end-to-end tests. By following these patterns and avoiding anti-patterns, you'll create tests that are both resilient and easy to understand. diff --git a/docs/testing/e2e/mobile-e2e-guidelines.md b/docs/testing/e2e/mobile-e2e-guidelines.md index 2c3d014a..3e957e7e 100644 --- a/docs/testing/e2e/mobile-e2e-guidelines.md +++ b/docs/testing/e2e/mobile-e2e-guidelines.md @@ -214,9 +214,12 @@ To ensure consistency and reliability in our test scripts, we use MetaMask Mobil ```typescript import { Assertions, Gestures, Matchers } from '../framework'; -await Assertions.expectElementToBeVisible(SecurityAndPrivacy.metaMetricsToggle, { - description: 'MetaMetrics toggle should be visible', -}); +await Assertions.expectElementToBeVisible( + SecurityAndPrivacy.metaMetricsToggle, + { + description: 'MetaMetrics toggle should be visible', + }, +); await Gestures.tap(button, { description: 'tap create wallet button', @@ -378,11 +381,19 @@ it('create a new wallet', async () => { ```typescript class WalletView { - get sendButton() { /* ... */ } - get receiveButton() { /* ... */ } - - async tapSendButton(): Promise { /* ... */ } - async initiateReceiveFlow(): Promise { /* ... */ } + get sendButton() { + /* ... */ + } + get receiveButton() { + /* ... */ + } + + async tapSendButton(): Promise { + /* ... */ + } + async initiateReceiveFlow(): Promise { + /* ... */ + } } ``` @@ -390,8 +401,12 @@ class WalletView { ```javascript class Screen2 { - get btn1() { /* ... */ } - get thing() { /* ... */ } + get btn1() { + /* ... */ + } + get thing() { + /* ... */ + } } ``` @@ -560,7 +575,9 @@ class NetworkView { } get customNetworkTab() { - return Matchers.getElementByText(NetworkViewSelectorsText.CUSTOM_NETWORK_TAB); + return Matchers.getElementByText( + NetworkViewSelectorsText.CUSTOM_NETWORK_TAB, + ); } get rpcWarningBanner() { @@ -674,14 +691,14 @@ describe(SmokeNetworks('Network Management'), () => { // Navigate and start adding network await NetworkView.tapAddNetworkButton(); await NetworkView.switchToCustomNetworks(); - + // Enter network details with validation await NetworkView.typeInNetworkName('Gnosis Test Network'); - + // Test validation with incorrect RPC URL await NetworkView.typeInRpcUrl('invalid-url'); await NetworkView.verifyRpcWarningVisible(); - + // Clear and enter correct RPC URL await NetworkView.clearRpcInputBox(); await NetworkView.typeInRpcUrl('https://rpc.gnosischain.com'); @@ -698,16 +715,19 @@ describe(SmokeNetworks('Network Management'), () => { ### Test Atomicity and Coupling #### When to Isolate Tests: + - Testing specific functionality of a single component or feature - When you need to pinpoint exact failure causes - For basic unit-level behaviors #### When to Combine Tests: + - For multi-step user flows that represent real user behavior - When testing how different parts of the application work together - When the setup for multiple tests is time-consuming and identical #### Guidelines: + - Each test should run with a dedicated app instance and controlled environment - Use `withFixtures` to create test prerequisites and clean up afterward - Control application state programmatically rather than through UI interactions @@ -716,12 +736,14 @@ describe(SmokeNetworks('Network Management'), () => { ### Controlling State #### Best Practices: + - Control application state through fixtures rather than UI interactions - Use `FixtureBuilder` to set up test prerequisites instead of UI steps - Minimize UI interactions to reduce potential breaking points - Improve test stability by reducing timing and synchronization issues #### Example: + ```typescript // βœ… Good: Use fixture to set up prerequisites const fixture = new FixtureBuilder() @@ -756,24 +778,28 @@ await withFixtures({ fixture: new FixtureBuilder().build() }, async () => { ## 🎯 Test Quality Principles ### Reliability + - Tests should consistently produce the same results - Use controlled environments and mocked external dependencies - Implement proper retry mechanisms through the framework - Handle expected failures gracefully ### Maintainability + - Follow Page Object Model pattern consistently - Use descriptive naming throughout - Keep tests focused on specific behaviors - Minimize code duplication through reusable page objects ### Readability + - Write tests that tell a story of user behavior - Use meaningful descriptions in all framework calls - Structure tests logically with clear arrange-act-assert patterns - Document complex test scenarios with comments when necessary ### Speed + - Use `withFixtures` for efficient test setup - Minimize unnecessary UI interactions - Leverage framework optimizations (like `checkStability: false` by default) @@ -786,4 +812,4 @@ await withFixtures({ fixture: new FixtureBuilder().build() }, async () => { - **Setup Documentation**: [`docs/readme/testing.md`](https://github.com/MetaMask/metamask-mobile/blob/main/docs/readme/testing.md) - **Framework Documentation**: [`e2e/framework/README.md`](https://github.com/MetaMask/metamask-mobile/blob/main/e2e/framework/README.md) -By following these timeless testing principles alongside the modern MetaMask Mobile framework, you'll create test suites that are not only functional but also serve as living documentation of how the application should behave from a user's perspective. \ No newline at end of file +By following these timeless testing principles alongside the modern MetaMask Mobile framework, you'll create test suites that are not only functional but also serve as living documentation of how the application should behave from a user's perspective.