335 lines
9.6 KiB
Markdown
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/SaayamApp.app',
|
|
build: 'xcodebuild -workspace ios/SaayamApp.xcworkspace -scheme SaayamApp -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: 'saayam://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
|
|
``` |