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

335 lines
9.6 KiB
Markdown

# E2E Testing with Detox
## Installation
```bash
yarn add -D detox
npx detox init
```
## Configuration
**detox.config.js:**
```javascript
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:**
```json
{
"maxWorkers": 1,
"testTimeout": 120000,
"retries": 2,
"bail": false,
"verbose": true,
"setupFilesAfterEnv": ["./init.js"]
}
```
## Test Setup
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```bash
# 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
```