react-native-sop-docs/06-testing-quality-assurance/e2e-testing.md

9.6 KiB

E2E Testing with Detox

Installation

yarn add -D detox
npx detox init

Configuration

detox.config.js:

module.exports = {
  testRunner: 'jest',
  runnerConfig: 'e2e/config.json',
  configurations: {
    'ios.sim.debug': {
      device: {
        type: 'ios.simulator',
        device: {
          type: 'iPhone 12',
        },
      },
      app: {
        type: 'ios.app',
        binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
        build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
      },
    },
    'android.emu.debug': {
      device: {
        type: 'android.emulator',
        device: {
          avdName: 'Pixel_4_API_30',
        },
      },
      app: {
        type: 'android.apk',
        binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
        build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
        reversePorts: [8081],
      },
    },
  },
};

e2e/config.json:

{
  "maxWorkers": 1,
  "testTimeout": 120000,
  "retries": 2,
  "bail": false,
  "verbose": true,
  "setupFilesAfterEnv": ["./init.js"]
}

Test Setup

// e2e/init.js
const detox = require('detox');
const config = require('../detox.config.js');
const adapter = require('detox/runners/jest/adapter');

jest.setTimeout(300000);
jasmine.getEnv().addReporter(adapter);

beforeAll(async () => {
  await detox.init(config);
});

beforeEach(async () => {
  await adapter.beforeEach();
});

afterAll(async () => {
  await adapter.afterAll();
  await detox.cleanup();
});

Login Flow Test

// e2e/loginFlow.e2e.js
describe('Login Flow', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should show login screen on app launch', async () => {
    await expect(element(by.id('loginScreen'))).toBeVisible();
    await expect(element(by.id('emailInput'))).toBeVisible();
    await expect(element(by.id('passwordInput'))).toBeVisible();
    await expect(element(by.id('loginButton'))).toBeVisible();
  });

  it('should show validation errors for empty fields', async () => {
    await element(by.id('loginButton')).tap();
    
    await expect(element(by.text('Email is required'))).toBeVisible();
    await expect(element(by.text('Password is required'))).toBeVisible();
  });

  it('should login successfully with valid credentials', async () => {
    await element(by.id('emailInput')).typeText('test@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();

    // Wait for navigation to home screen
    await waitFor(element(by.id('homeScreen')))
      .toBeVisible()
      .withTimeout(10000);
  });

  it('should show error for invalid credentials', async () => {
    await element(by.id('emailInput')).typeText('invalid@example.com');
    await element(by.id('passwordInput')).typeText('wrongpassword');
    await element(by.id('loginButton')).tap();

    await expect(element(by.text('Invalid credentials'))).toBeVisible();
  });
});

Navigation Test

// e2e/navigation.e2e.js
describe('Navigation', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
    // Login first
    await element(by.id('emailInput')).typeText('test@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();
    await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000);
  });

  it('should navigate between tabs', async () => {
    // Test Home tab
    await expect(element(by.id('homeScreen'))).toBeVisible();

    // Navigate to Profile tab
    await element(by.id('profileTab')).tap();
    await expect(element(by.id('profileScreen'))).toBeVisible();

    // Navigate to Settings tab
    await element(by.id('settingsTab')).tap();
    await expect(element(by.id('settingsScreen'))).toBeVisible();

    // Navigate back to Home tab
    await element(by.id('homeTab')).tap();
    await expect(element(by.id('homeScreen'))).toBeVisible();
  });

  it('should navigate to detail screen and back', async () => {
    await element(by.id('firstRequestItem')).tap();
    await expect(element(by.id('requestDetailScreen'))).toBeVisible();

    await element(by.id('backButton')).tap();
    await expect(element(by.id('homeScreen'))).toBeVisible();
  });
});

Form Testing

// e2e/createRequest.e2e.js
describe('Create Request Flow', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
    // Login and navigate to create request
    await element(by.id('emailInput')).typeText('test@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();
    await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000);
    await element(by.id('createRequestButton')).tap();
  });

  it('should create a new request successfully', async () => {
    await element(by.id('titleInput')).typeText('Help for Medical Treatment');
    await element(by.id('descriptionInput')).typeText('Need urgent help for medical treatment');
    await element(by.id('targetAmountInput')).typeText('50000');
    
    // Select category
    await element(by.id('categoryDropdown')).tap();
    await element(by.text('Health')).tap();
    
    // Add image
    await element(by.id('addImageButton')).tap();
    await element(by.text('Camera')).tap();
    
    // Submit form
    await element(by.id('submitButton')).tap();
    
    await waitFor(element(by.text('Request created successfully')))
      .toBeVisible()
      .withTimeout(10000);
  });

  it('should show validation errors for incomplete form', async () => {
    await element(by.id('submitButton')).tap();
    
    await expect(element(by.text('Title is required'))).toBeVisible();
    await expect(element(by.text('Description is required'))).toBeVisible();
    await expect(element(by.text('Target amount is required'))).toBeVisible();
  });
});

Scroll and List Testing

// e2e/requestList.e2e.js
describe('Request List', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
    // Login first
    await element(by.id('emailInput')).typeText('test@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();
    await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000);
  });

  it('should load and display request list', async () => {
    await expect(element(by.id('requestList'))).toBeVisible();
    await expect(element(by.id('requestItem-0'))).toBeVisible();
  });

  it('should scroll through request list', async () => {
    await element(by.id('requestList')).scroll(300, 'down');
    await expect(element(by.id('requestItem-5'))).toBeVisible();
  });

  it('should pull to refresh', async () => {
    await element(by.id('requestList')).swipe('down', 'slow', 0.8);
    await waitFor(element(by.id('refreshIndicator')))
      .toBeVisible()
      .withTimeout(2000);
  });

  it('should filter requests by category', async () => {
    await element(by.id('filterButton')).tap();
    await element(by.text('Health')).tap();
    await element(by.id('applyFilterButton')).tap();
    
    await waitFor(element(by.id('requestList')))
      .toBeVisible()
      .withTimeout(5000);
  });
});

Device Interactions

// e2e/deviceInteractions.e2e.js
describe('Device Interactions', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should handle device rotation', async () => {
    await device.setOrientation('landscape');
    await expect(element(by.id('loginScreen'))).toBeVisible();
    
    await device.setOrientation('portrait');
    await expect(element(by.id('loginScreen'))).toBeVisible();
  });

  it('should handle app backgrounding and foregrounding', async () => {
    await device.sendToHome();
    await device.launchApp({newInstance: false});
    await expect(element(by.id('loginScreen'))).toBeVisible();
  });

  it('should handle deep links', async () => {
    await device.openURL({url: 'myapp://request/123'});
    await waitFor(element(by.id('requestDetailScreen')))
      .toBeVisible()
      .withTimeout(10000);
  });
});

Test Utilities

// e2e/utils/helpers.js
export const loginUser = async (email = 'test@example.com', password = 'password123') => {
  await element(by.id('emailInput')).typeText(email);
  await element(by.id('passwordInput')).typeText(password);
  await element(by.id('loginButton')).tap();
  await waitFor(element(by.id('homeScreen'))).toBeVisible().withTimeout(10000);
};

export const navigateToScreen = async (screenId) => {
  await element(by.id(screenId)).tap();
  await waitFor(element(by.id(`${screenId}Screen`))).toBeVisible().withTimeout(5000);
};

export const fillForm = async (formData) => {
  for (const [fieldId, value] of Object.entries(formData)) {
    await element(by.id(fieldId)).typeText(value);
  }
};

export const waitForElementToDisappear = async (elementId, timeout = 5000) => {
  await waitFor(element(by.id(elementId))).not.toBeVisible().withTimeout(timeout);
};

Running Tests

# Build and run iOS tests
detox build --configuration ios.sim.debug
detox test --configuration ios.sim.debug

# Build and run Android tests
detox build --configuration android.emu.debug
detox test --configuration android.emu.debug

# Run specific test file
detox test --configuration ios.sim.debug e2e/loginFlow.e2e.js

# Run tests with verbose output
detox test --configuration ios.sim.debug --loglevel verbose