Current Performance: 38.8 seconds (1231 tests, 61 test suites) Tests per second: 31.7 tests/sec Primary bottleneck: Jest startup overhead + async operations not properly cleaned up
Total time: 5 minutes 38 seconds (338 seconds) Actual test time: 38.8 seconds Overhead time: 299 seconds (89% of total!)
| Metric | Value |
|---|---|
| Test suites | 61 total (57 passed, 3 failed, 1 skipped) |
| Individual tests | 1,231 total (1,203 passed, 16 failed, 12 skipped) |
| Execution time | 38.8s |
| Estimated time | 49s (Jest prediction) |
| Average per test | 31.5ms |
| Average per suite | 636ms |
Critical Issue Detected:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't
stopped in your tests.
Impact: Adds ~5 minutes of "waiting for cleanup" time!
Problem: Jest hangs for 5+ minutes after tests complete Root cause: Asynchronous operations (timers, promises, event listeners) not properly cleaned up Impact: Makes test runs 8.7x slower than they should be
Affected areas (likely culprits):
-
WebView tests -
TrackerAuthWebView.test.jsx(678 lines)- Uses
setTimeoutin mock implementation - May not be properly cleaning up timers
- Uses
-
React Query hooks - Multiple files use
@tanstack/react-queryuseSyncTracker.test.js(512 lines)useConnectedTrackers.test.js(301 lines)useDisconnectTracker.test.js(428 lines)- Query client may have active subscriptions
-
Mock implementations -
jest.setup.js- WebView mock uses
setTimeoutwithout cleanup - May create dangling promises
- WebView mock uses
Evidence from jest.setup.js (lines 171-177):
React.useEffect(() => {
if (onLoadStart) onLoadStart();
const timer = setTimeout(() => {
if (onLoadEnd) onLoadEnd();
}, 0);
return () => clearTimeout(timer); // Cleanup exists but may not run
}, []);Fix: Add proper cleanup in afterEach hooks
Largest test files by line count:
| File | Lines | Type | Impact |
|---|---|---|---|
productCatalog.test.js |
2,237 | Utility | Many test cases |
voice-recording-registry-integration.test.js |
1,301 | Integration | Complex mocking |
NewProductCapture.flow.test.js |
1,151 | Component | 7.6s runtime |
mealPatterns.test.js |
841 | Utility | Pattern matching |
photo-supplement-followup.test.js |
784 | Integration | Photo analysis |
Slowest running tests (from verbose output):
real-image-analysis.test.js- 9.9 secondsNewProductCapture.flow.test.js- 7.6 secondsprofile.modal-flow.test.jsx- 6.9 seconds (FAILING)history.test.jsx- 6.5 seconds (FAILING)
Component tests (React Testing Library):
- Slower: 500-7000ms per suite
- Heavy rendering, DOM queries
- Examples:
home.test.jsx,profile.test.jsx,history.test.jsx
Unit tests (Pure functions):
- Faster: 50-500ms per suite
- No rendering overhead
- Examples: All
transformers/*.test.jsfiles
Breakdown:
- Component/Integration tests: ~15 suites (~45% of time)
- Utility/Unit tests: ~45 suites (~20% of time)
- Cleanup overhead: ~35% of test time (the 5-minute hang)
What: Async operations not cleaned up Where:
- WebView mocks (setTimeout)
- React Query subscriptions
- Event listeners
- Network requests (fetch mocks)
How to detect:
npm test -- --detectOpenHandlesHow to fix:
// In jest.setup.js or individual test files
afterEach(() => {
jest.clearAllTimers();
jest.clearAllMocks();
// Clean up any subscriptions
});Component tests are inherently slower:
- Each test mounts/unmounts React components
- React Testing Library queries (getByText, findByRole) can be slow
- Mock implementations add overhead
Example - Slow component test:
// home.test.jsx (346 lines)
// Renders full home screen with multiple components
render(<HomeScreen />);
// Heavy DOM queries
const button = screen.getByRole('button', { name: /submit/i });Optimization:
- Use simpler queries (getByTestId faster than getByRole)
- Render smaller component slices instead of full screens
- Use shallow rendering where possible
Current: All tests run every time Problem: No separation of fast vs slow tests
Suggested structure:
__tests__/
├── unit/ # Pure functions (fast - 5-10s)
├── integration/ # API mocking (medium - 10-20s)
└── component/ # React components (slow - 20-40s)
1. Add global cleanup in jest.setup.js:
// jest.setup.js - Add at end
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
jest.restoreAllMocks();
});
afterAll(() => {
jest.clearAllTimers();
});2. Fix WebView mock to avoid timer leaks:
// jest.setup.js - Update WebView mock
jest.mock('react-native-webview', () => {
const React = require('react');
const { View } = require('react-native');
return {
WebView: React.forwardRef(({ source, onMessage, onLoadStart, onLoadEnd, testID, ...props }, ref) => {
const timerRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
injectJavaScript: jest.fn(),
postMessage: (message) => {
if (onMessage) {
onMessage({ nativeEvent: { data: message } });
}
},
}));
React.useEffect(() => {
if (onLoadStart) onLoadStart();
// Use setImmediate instead of setTimeout (faster and safer)
timerRef.current = setImmediate(() => {
if (onLoadEnd) onLoadEnd();
});
// CRITICAL: Cleanup timer
return () => {
if (timerRef.current) {
clearImmediate(timerRef.current);
}
};
}, [onLoadStart, onLoadEnd]);
return React.createElement(View, { testID: testID || 'webview', source, onMessage, ...props });
}),
};
});3. Add React Query cleanup:
// In test files using React Query
import { QueryClient } from '@tanstack/react-query';
let queryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
});
afterEach(() => {
queryClient.clear();
queryClient.cancelQueries();
});Expected impact: Reduce total time from 5m38s to ~40s (88% faster)
1. Skip component tests if no component files changed:
# .husky/pre-commit
CHANGED_FILES=$(git diff --cached --name-only)
if echo "$CHANGED_FILES" | grep -q "mobile/src/components/"; then
npm test -- --testPathPattern="components"
else
npm test -- --testPathIgnorePatterns="components"
fi2. Use Jest's --onlyChanged flag:
# package.json
"scripts": {
"test:changed": "jest --onlyChanged --no-coverage"
}Expected impact: Run only 5-10 test suites instead of all 61 (80-90% faster for small changes)
1. Reduce rendering in component tests:
// BEFORE (slow)
render(<HomeScreen />); // Renders entire screen
// AFTER (faster)
render(<VoiceRecordingButton onPress={mockFn} />); // Just the button2. Use getByTestId instead of getByRole:
// BEFORE (slow)
const button = screen.getByRole('button', { name: /submit voice/i });
// AFTER (faster)
const button = screen.getByTestId('voice-submit-button');3. Mock heavy modules:
// Mock image analysis (heavy computation)
jest.mock('@/utils/photoAnalysis', () => ({
analyzeImage: jest.fn(() => Promise.resolve({ /* mock result */ })),
}));Expected impact: Reduce slowest tests from 7-10s to 3-5s each
Current: --maxWorkers=2 (running 2 suites in parallel)
Optimization: Increase to --maxWorkers=4 on powerful machines
// package.json
{
"scripts": {
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test": "jest --maxWorkers=4" // More parallelism for local dev
}
}Note: Only helps after fixing open handles issue
Expected impact: 20-30% faster (only if open handles fixed first)
// package.json
{
"scripts": {
"test": "jest",
"test:fast": "jest --onlyChanged --no-coverage --maxWorkers=4",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:debug": "jest --detectOpenHandles --verbose"
}
}#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Get changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
# Only run mobile tests if mobile/ files changed
if echo "$CHANGED_FILES" | grep -q "^mobile/"; then
cd mobile
# Run only tests for changed files (much faster)
npm run test:fast
cd ..
fi
# ... other checks| Scenario | Before | After | Savings |
|---|---|---|---|
| Fix open handles only | 5m38s | 40s | 88% |
| + Use --onlyChanged | 5m38s | 5-10s | 95-98% |
| + Optimize slow tests | 5m38s | 3-8s | 96-99% |
| Full suite (all optimizations) | 5m38s | 25-30s | 90% |
Realistic pre-commit time (after all optimizations):
- No mobile changes: 0s (skipped)
- Few mobile files changed: 5-10s (only affected tests)
- Many mobile files changed: 25-30s (full suite, but much faster)
1. ConnectedDeviceCard.test.jsx - Schema mismatch
// Expected: trackerId
// Received: deviceId
// FIX: Update test to use deviceId (matches schema migration)2. history.test.jsx - 6.5s runtime, multiple failures
- Likely integration test with heavy mocking
- May need updated mocks
3. profile.modal-flow.test.jsx - 6.9s runtime, multiple failures
- Modal flow testing
- Async operations not resolving
Recommendation: Fix these before optimizing (ensure tests actually work)
- ✅ Add global cleanup in jest.setup.js (1 hour)
- ✅ Fix WebView mock timer leaks (30 min)
- ✅ Add React Query cleanup (1 hour)
- ✅ Run
--detectOpenHandlesto find remaining issues (30 min) - ✅ Fix failing tests (2-3 hours)
Expected result: 5m38s → 40s (88% faster)
- ✅ Add
test:fastscript with--onlyChanged(15 min) - ✅ Update .husky/pre-commit to use conditional testing (30 min)
- ✅ Test on real commits (1 hour)
Expected result: 40s → 5-10s for typical commits (75-87% faster)
- ⏸️ Refactor slow component tests (2-4 hours)
- ⏸️ Add testID attributes for faster queries (1-2 hours)
- ⏸️ Mock heavy modules (1 hour)
Expected result: 40s → 25-30s for full suite (25-37% faster)
npm test 2>&1 | grep "Time:"
# Output: Time: 38.827 s, estimated 49 s
# + 5 minutes of hangingnpm test 2>&1 | grep "Time:"
# Output: Time: 25-30s
# + No hanging (immediate exit)- Total test time
- Tests per second
- Number of open handles
- Pass rate (should stay 100% after fixing failures)
| Metric | Mobile Jest | Edge Function Deno |
|---|---|---|
| Total tests | 1,231 | 86 |
| Total time | 38.8s (+ 5min overhead) | 0.3s |
| Per-test average | 31.5ms | 3.5ms |
| Startup overhead | 5+ minutes | Negligible |
| Open handles issue | Yes ❌ | No ✅ |
| Parallelization | maxWorkers=2 | Single-threaded |
| Framework | Jest + React Native | Deno native |
Why Deno is 90x faster per test:
- No React Native rendering overhead
- No heavy mocking framework
- Native TypeScript support (no compilation)
- Lightweight test runner
- No cleanup issues
Lesson for Jest optimization:
- Minimize rendering (test smaller components)
- Reduce mocking complexity
- Ensure proper cleanup
- Use simpler queries
# Find open handles
npm test -- --detectOpenHandles
# Run single test file
npm test -- src/app/(tabs)/__tests__/home.test.jsx
# Debug slow tests
npm test -- --verbose --maxWorkers=1
# Check for memory leaks
npm test -- --logHeapUsageThe mobile Jest test suite is slow due to:
- Open handles causing 5+ minutes of hanging (89% of time) - CRITICAL
- Heavy React Native component rendering
- Large test suites without smart filtering
- Some genuinely slow tests (image analysis, etc.)
Primary recommendation: Fix open handles first (will save 5 minutes immediately), then add smart test filtering (will save another 20-30s on typical commits).
Expected final performance:
- Pre-commit for typical changes: 5-10 seconds (vs current 5m38s)
- Full test suite: 25-30 seconds (vs current 5m38s)
- Overall improvement: 95-98% faster
Last Updated: February 3, 2026 Analysis Date: February 3, 2026 Current Status: 1,231 tests in 38.8s + 5min overhead = 5m38s total Target Status: 1,231 tests in 25-30s + no overhead = 30s total