Learn comprehensive testing strategies and debugging techniques for React Native apps.
Learn comprehensive testing strategies and debugging techniques for React Native apps.
Learn to write unit tests for React Native components and functions using Jest testing framework
Content by: Jigar Solanki
React Native Developer
Set up Jest testing environment for React Native applications with proper configuration and mocking capabilities.
Write comprehensive unit tests for components, functions, and utilities with proper assertions and test coverage.
// utils/helpers.js
export const formatCurrency = (amount, currency = 'USD') => {
if (typeof amount !== 'number') {
throw new Error('Amount must be a number');
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount);
};
export const validateEmail = (email) => {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email);
};
export const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(null, args), delay);
};
};
// components/Button.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
export const Button = ({ title, onPress, disabled = false, variant = 'primary' }) => {
return (
<TouchableOpacity
style={[styles.button, styles[variant], disabled && styles.disabled]}
onPress={onPress}
disabled={disabled}
testID="button"
>
<Text style={[styles.text, disabled && styles.disabledText]}>
{title}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
},
primary: {
backgroundColor: '#2196F3',
},
secondary: {
backgroundColor: '#6c757d',
},
disabled: {
backgroundColor: '#e9ecef',
},
text: {
color: 'white',
fontWeight: 'bold',
},
disabledText: {
color: '#6c757d',
},
});
// __tests__/utils/helpers.test.js
import { formatCurrency, validateEmail, debounce } from '../utils/helpers';
describe('formatCurrency', () => {
it('should format currency correctly for USD', () => {
expect(formatCurrency(100)).toBe('$100.00');
expect(formatCurrency(1000)).toBe('$1,000.00');
expect(formatCurrency(99.99)).toBe('$99.99');
});
it('should format currency for different currencies', () => {
expect(formatCurrency(100, 'EUR')).toBe('โฌ100.00');
expect(formatCurrency(100, 'GBP')).toBe('ยฃ100.00');
});
it('should throw error for invalid input', () => {
expect(() => formatCurrency('invalid')).toThrow('Amount must be a number');
expect(() => formatCurrency(null)).toThrow('Amount must be a number');
expect(() => formatCurrency(undefined)).toThrow('Amount must be a number');
});
it('should handle edge cases', () => {
expect(formatCurrency(0)).toBe('$0.00');
expect(formatCurrency(-100)).toBe('-$100.00');
});
});
describe('validateEmail', () => {
it('should validate correct email addresses', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('user.name@domain.co.uk')).toBe(true);
expect(validateEmail('user+tag@example.org')).toBe(true);
});
it('should reject invalid email addresses', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
expect(validateEmail('test@')).toBe(false);
expect(validateEmail('')).toBe(false);
expect(validateEmail(null)).toBe(false);
expect(validateEmail(undefined)).toBe(false);
});
});
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should debounce function calls', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 100);
debouncedFn('arg1');
debouncedFn('arg2');
debouncedFn('arg3');
expect(mockFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg3');
});
it('should reset timer on new calls', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 100);
debouncedFn('first');
jest.advanceTimersByTime(50);
debouncedFn('second');
jest.advanceTimersByTime(50);
expect(mockFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(50);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('second');
});
});
// __tests__/components/Button.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Button } from '../components/Button';
describe('Button Component', () => {
it('should render correctly with title', () => {
const { getByText, getByTestId } = render(
<Button title="Test Button" onPress={() => {}} />
);
expect(getByText('Test Button')).toBeTruthy();
expect(getByTestId('button')).toBeTruthy();
});
it('should call onPress when pressed', () => {
const mockOnPress = jest.fn();
const { getByTestId } = render(
<Button title="Test Button" onPress={mockOnPress} />
);
fireEvent.press(getByTestId('button'));
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
it('should not call onPress when disabled', () => {
const mockOnPress = jest.fn();
const { getByTestId } = render(
<Button title="Test Button" onPress={mockOnPress} disabled={true} />
);
fireEvent.press(getByTestId('button'));
expect(mockOnPress).not.toHaveBeenCalled();
});
it('should apply correct styles for different variants', () => {
const { getByTestId, rerender } = render(
<Button title="Primary" onPress={() => {}} variant="primary" />
);
let button = getByTestId('button');
expect(button.props.style).toContainEqual(
expect.objectContaining({ backgroundColor: '#2196F3' })
);
rerender(<Button title="Secondary" onPress={() => {}} variant="secondary" />);
button = getByTestId('button');
expect(button.props.style).toContainEqual(
expect.objectContaining({ backgroundColor: '#6c757d' })
);
});
it('should apply disabled styles when disabled', () => {
const { getByTestId } = render(
<Button title="Disabled" onPress={() => {}} disabled={true} />
);
const button = getByTestId('button');
expect(button.props.style).toContainEqual(
expect.objectContaining({ backgroundColor: '#e9ecef' })
);
});
it('should handle missing onPress gracefully', () => {
const { getByTestId } = render(
<Button title="No Press" />
);
expect(() => {
fireEvent.press(getByTestId('button'));
}).not.toThrow();
});
});
// jest.config.js
module.exports = {
preset: 'react-native',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|react-native-.*|@react-navigation|@react-native-community|@react-native-picker|@react-native-async-storage|@react-native-vector-icons)/)',
],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/node_modules/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
testEnvironment: 'jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
// jest.setup.js
import 'react-native-gesture-handler/jestSetup';
// Mock react-native-reanimated
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
return Reanimated;
});
// Mock react-native-vector-icons
jest.mock('react-native-vector-icons/MaterialIcons', () => 'Icon');
// Silence the warning: Animated: `useNativeDriver` is not supported
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');Test your understanding of this topic:
Test React Native components using React Testing Library for better component behavior testing
Content by: Jigar Solanki
React Native Developer
Use React Testing Library to test components in a way that resembles how users interact with your app.
Test component behavior, user interactions, and state changes to ensure components work as expected.
// components/UserProfile.js
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
const UserProfile = ({ userId, onSave, onCancel }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
});
const [errors, setErrors] = useState({});
useEffect(() => {
loadUser();
}, [userId]);
const loadUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
setFormData({
name: userData.name || '',
email: userData.email || '',
phone: userData.phone || '',
});
} catch (error) {
Alert.alert('Error', 'Failed to load user data');
} finally {
setLoading(false);
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = async () => {
if (!validateForm()) return;
try {
setSaving(true);
await onSave(userId, formData);
Alert.alert('Success', 'Profile updated successfully');
} catch (error) {
Alert.alert('Error', 'Failed to save profile');
} finally {
setSaving(false);
}
};
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: null }));
}
};
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#2196F3" testID="loading-indicator" />
<Text style={styles.loadingText}>Loading user profile...</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>User Profile</Text>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Name</Text>
<TextInput
style={[styles.input, errors.name && styles.inputError]}
value={formData.name}
onChangeText={(value) => handleInputChange('name', value)}
placeholder="Enter your name"
testID="name-input"
/>
{errors.name && (
<Text style={styles.errorText} testID="name-error">
{errors.name}
</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={[styles.input, errors.email && styles.inputError]}
value={formData.email}
onChangeText={(value) => handleInputChange('email', value)}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
testID="email-input"
/>
{errors.email && (
<Text style={styles.errorText} testID="email-error">
{errors.email}
</Text>
)}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Phone</Text>
<TextInput
style={[styles.input, errors.phone && styles.inputError]}
value={formData.phone}
onChangeText={(value) => handleInputChange('phone', value)}
placeholder="Enter your phone number"
keyboardType="phone-pad"
testID="phone-input"
/>
{errors.phone && (
<Text style={styles.errorText} testID="phone-error">
{errors.phone}
</Text>
)}
</View>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={onCancel}
testID="cancel-button"
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.saveButton, saving && styles.disabledButton]}
onPress={handleSave}
disabled={saving}
testID="save-button"
>
{saving ? (
<ActivityIndicator size="small" color="white" testID="saving-indicator" />
) : (
<Text style={styles.saveButtonText}>Save</Text>
)}
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
color: '#333',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
form: {
backgroundColor: 'white',
borderRadius: 8,
padding: 20,
marginBottom: 20,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 16,
fontWeight: '500',
color: '#333',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: 'white',
},
inputError: {
borderColor: '#f44336',
},
errorText: {
color: '#f44336',
fontSize: 14,
marginTop: 5,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
button: {
flex: 1,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
marginHorizontal: 5,
},
cancelButton: {
backgroundColor: '#6c757d',
},
saveButton: {
backgroundColor: '#2196F3',
},
disabledButton: {
backgroundColor: '#ccc',
},
cancelButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
saveButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default UserProfile;
// __tests__/components/UserProfile.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native';
import UserProfile from '../components/UserProfile';
// Mock fetch
global.fetch = jest.fn();
// Mock Alert
jest.spyOn(Alert, 'alert');
describe('UserProfile Component', () => {
const mockOnSave = jest.fn();
const mockOnCancel = jest.fn();
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
};
beforeEach(() => {
jest.clearAllMocks();
fetch.mockClear();
Alert.alert.mockClear();
});
it('should render loading state initially', () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByText, getByTestId } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
expect(getByText('Loading user profile...')).toBeTruthy();
expect(getByTestId('loading-indicator')).toBeTruthy();
});
it('should load and display user data', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByDisplayValue } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByDisplayValue('John Doe')).toBeTruthy();
expect(getByDisplayValue('john@example.com')).toBeTruthy();
expect(getByDisplayValue('123-456-7890')).toBeTruthy();
});
});
it('should handle form validation', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const { getByTestId, getByText } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByTestId('name-input')).toBeTruthy();
});
// Clear name field to trigger validation
fireEvent.changeText(getByTestId('name-input'), '');
fireEvent.press(getByTestId('save-button'));
await waitFor(() => {
expect(getByTestId('name-error')).toBeTruthy();
expect(getByText('Name is required')).toBeTruthy();
});
});
it('should call onSave with correct data when form is valid', async () => {
fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
mockOnSave.mockResolvedValueOnce();
const { getByTestId } = render(
<UserProfile userId={1} onSave={mockOnSave} onCancel={mockOnCancel} />
);
await waitFor(() => {
expect(getByTestId('save-button')).toBeTruthy();
});
fireEvent.press(getByTestId('save-button'));
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(1, {
name: 'John Doe',
email: 'john@example.com',
phone: '123-456-7890',
});
});
});
});Test your understanding of this topic:
Implement end-to-end testing for React Native applications using Detox testing framework
Content by: Jigar Solanki
React Native Developer
Set up Detox for E2E testing with proper configuration for both iOS and Android platforms.
Write comprehensive E2E tests that cover complete user journeys and critical app functionality.
// e2e/firstTest.e2e.js
import { device, expect, element, by, waitFor } from 'detox';
describe('React Native App E2E Tests', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
describe('Authentication Flow', () => {
it('should show login screen on app launch', async () => {
await expect(element(by.id('login-screen'))).toBeVisible();
await expect(element(by.id('email-input'))).toBeVisible();
await expect(element(by.id('password-input'))).toBeVisible();
await expect(element(by.id('login-button'))).toBeVisible();
});
it('should validate login form', async () => {
// Test empty form validation
await element(by.id('login-button')).tap();
await expect(element(by.text('Email is required'))).toBeVisible();
await expect(element(by.text('Password is required'))).toBeVisible();
// Test invalid email format
await element(by.id('email-input')).typeText('invalid-email');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.text('Invalid email format'))).toBeVisible();
});
it('should successfully login with valid credentials', async () => {
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
// Wait for navigation to home screen
await waitFor(element(by.id('home-screen')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('welcome-text'))).toBeVisible();
await expect(element(by.text('Welcome, test@example.com'))).toBeVisible();
});
});
describe('Navigation Flow', () => {
beforeEach(async () => {
// Login first
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await waitFor(element(by.id('home-screen'))).toBeVisible().withTimeout(5000);
});
it('should navigate to profile screen', async () => {
await element(by.id('profile-tab')).tap();
await expect(element(by.id('profile-screen'))).toBeVisible();
await expect(element(by.id('user-name'))).toBeVisible();
await expect(element(by.id('user-email'))).toBeVisible();
});
it('should navigate to settings screen', async () => {
await element(by.id('settings-tab')).tap();
await expect(element(by.id('settings-screen'))).toBeVisible();
await expect(element(by.id('theme-toggle'))).toBeVisible();
await expect(element(by.id('notifications-toggle'))).toBeVisible();
});
});
describe('User Profile Management', () => {
beforeEach(async () => {
// Login and navigate to profile
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await waitFor(element(by.id('home-screen'))).toBeVisible().withTimeout(5000);
await element(by.id('profile-tab')).tap();
});
it('should display current user information', async () => {
await expect(element(by.id('user-name'))).toHaveText('John Doe');
await expect(element(by.id('user-email'))).toHaveText('test@example.com');
await expect(element(by.id('user-phone'))).toHaveText('123-456-7890');
});
it('should edit user profile', async () => {
await element(by.id('edit-profile-button')).tap();
// Update name
await element(by.id('name-input')).clearText();
await element(by.id('name-input')).typeText('Jane Smith');
// Update phone
await element(by.id('phone-input')).clearText();
await element(by.id('phone-input')).typeText('987-654-3210');
await element(by.id('save-button')).tap();
// Verify changes
await expect(element(by.text('Profile updated successfully'))).toBeVisible();
await expect(element(by.id('user-name'))).toHaveText('Jane Smith');
await expect(element(by.id('user-phone'))).toHaveText('987-654-3210');
});
});
describe('Error Handling', () => {
it('should handle network errors gracefully', async () => {
// Simulate network error by using invalid credentials
await element(by.id('email-input')).typeText('invalid@example.com');
await element(by.id('password-input')).typeText('wrongpassword');
await element(by.id('login-button')).tap();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
});
});
// detox.config.js
module.exports = {
testRunner: 'jest',
runnerConfig: 'e2e/config.json',
configurations: {
'ios.sim.debug': {
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YourApp.app',
build: 'xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
'android.emu.debug': {
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
type: 'android.emulator',
device: {
avdName: 'Pixel_4_API_30',
},
},
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YourApp.app',
build: 'xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
attached: {
type: 'android.attached',
device: {
adbName: '.*',
},
},
},
artifacts: {
rootDir: './e2e/artifacts',
pathBuilder: './e2e/artifacts/pathBuilder.js',
screenshot: 'manual',
video: 'manual',
instruments: 'off',
},
behavior: {
init: {
reinstallApp: true,
},
cleanup: {
shutdownDevice: false,
},
},
session: {
server: 'ws://localhost:8099',
sessionId: 'detox',
},
};Test your understanding of this topic:
Master debugging tools and techniques for React Native development including Flipper, React DevTools, and native debugging
Content by: Jigar Solanki
React Native Developer
Use Flipper, React DevTools, and native debugging tools to identify and fix issues in React Native applications.
Implement effective debugging strategies including logging, breakpoints, and performance profiling.
// utils/debugger.js
import { Platform, Alert } from 'react-native';
import crashlytics from '@react-native-firebase/crashlytics';
import analytics from '@react-native-firebase/analytics';
class Debugger {
constructor() {
this.isDebugMode = __DEV__;
this.logLevel = this.isDebugMode ? 'debug' : 'error';
this.logs = [];
}
// Logging methods
log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
data,
platform: Platform.OS,
version: Platform.Version,
};
// Store logs in memory for debugging
this.logs.push(logEntry);
// Keep only last 100 logs
if (this.logs.length > 100) {
this.logs.shift();
}
// Console logging in debug mode
if (this.isDebugMode) {
console.log(`[${level.toUpperCase()}] ${message}`, data);
}
// Send to crashlytics in production
if (!this.isDebugMode && level === 'error') {
crashlytics().log(`[${level}] ${message}`);
if (data) {
crashlytics().recordError(new Error(JSON.stringify(data)));
}
}
// Send to analytics
analytics().logEvent('debug_log', {
level,
message: message.substring(0, 100), // Limit message length
});
}
debug(message, data = null) {
this.log('debug', message, data);
}
info(message, data = null) {
this.log('info', message, data);
}
warn(message, data = null) {
this.log('warn', message, data);
}
error(message, data = null) {
this.log('error', message, data);
}
// Performance monitoring
startTimer(label) {
const startTime = Date.now();
return {
end: () => {
const duration = Date.now() - startTime;
this.debug(`Timer ${label} completed`, { duration });
return duration;
}
};
}
// Memory monitoring
checkMemoryUsage() {
if (this.isDebugMode) {
const memoryInfo = {
usedJSHeapSize: performance.memory?.usedJSHeapSize || 0,
totalJSHeapSize: performance.memory?.totalJSHeapSize || 0,
jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit || 0,
};
this.debug('Memory usage', memoryInfo);
return memoryInfo;
}
}
// Network monitoring
logNetworkRequest(url, method, status, duration, error = null) {
const networkLog = {
url,
method,
status,
duration,
error,
timestamp: new Date().toISOString(),
};
this.debug('Network request', networkLog);
if (error) {
this.error('Network error', networkLog);
}
}
// Error boundary integration
logError(error, errorInfo) {
this.error('React Error Boundary', {
error: error.toString(),
errorInfo,
stack: error.stack,
});
// Send to crashlytics
if (!this.isDebugMode) {
crashlytics().recordError(error);
}
}
// User action tracking
trackUserAction(action, data = {}) {
this.debug('User action', { action, data });
analytics().logEvent('user_action', {
action,
...data,
});
}
// Get all logs for debugging
getAllLogs() {
return this.logs;
}
// Clear logs
clearLogs() {
this.logs = [];
}
// Export logs for debugging
exportLogs() {
const logsString = JSON.stringify(this.logs, null, 2);
if (this.isDebugMode) {
console.log('=== EXPORTED LOGS ===');
console.log(logsString);
console.log('=== END LOGS ===');
}
return logsString;
}
// Show debug panel in development
showDebugPanel() {
if (!this.isDebugMode) return;
const logs = this.getAllLogs();
const recentErrors = logs.filter(log => log.level === 'error').slice(-5);
Alert.alert(
'Debug Panel',
`Total Logs: ${logs.length}\nRecent Errors: ${recentErrors.length}\nMemory: ${this.checkMemoryUsage()?.usedJSHeapSize || 'N/A'} bytes`,
[
{ text: 'Export Logs', onPress: () => this.exportLogs() },
{ text: 'Clear Logs', onPress: () => this.clearLogs() },
{ text: 'Close', style: 'cancel' },
]
);
}
}
// Create singleton instance
const debugger = new Debugger();
// React Native Debugger integration
if (__DEV__) {
// Add debugger to global scope for easy access
global.debugger = debugger;
// Add shake gesture listener for debug panel
import { DeviceEventEmitter } from 'react-native';
DeviceEventEmitter.addListener('shake', () => {
debugger.showDebugPanel();
});
}
export default debugger;Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 11