ποΈ Architecture
This document explains how jest-doctor integrates with Jest and enforces test isolation.
π« Non-goals
- Not a performance profiler
- Not a linter
- Not a replacement for Jestβs
--detectOpenHandles
π§© High-level design
jest-doctor works by augmenting the Jest test environment, not by modifying test code.
Core ideas:
- Each test owns its async resources
- Upon test end, async resources must fully clean
- Any leftover resource is a hard failure
This design prioritizes deterministic failures over permissive behavior.
π Integration points
jest-doctor provides custom environments:
jest-doctor/env/nodejest-doctor/env/jsdom
These environments:
- Extend Jestβs default environments
- Patch global APIs
- Track async lifecycle per test
π Execution lifecycle
- On test suite setup
- Initialize empty global leak records
- Patch globals (timers, console, test functions)
- Start promise tracking
- Before each test
- Initialize empty leak records
- During each test
- Attribute async resources to current test
- Capture creation stack traces
- After each test
- Detect leftover async resources
- Report leaks
- Clean up timers
- On test suite teardown
- Detect global leftover async resources
- Report leaks
- Clean up timers
- Cleanup globals and hooks
π΅οΈ Leak Detection Internals
This section describes how jest-doctor detects leaks.
Leak categories
jest-doctor currently detects:
| Category | Detection mechanism |
|---|---|
| Promises | v8.promiseHooks or subclassing Promise |
| Timers | Global API patching |
| Fake timers | Jest fake timer patching |
| Console output | Console method patching |
| Process output | process method patching |
| window listeners | window.(add/remove)-EventListener patching |
| excessive timer usage | add up all delays inside interval and timeout callbacks |
Promise detection
- Uses
node:v8.promiseHooks - Or subclasses
Promise - Records:
- stack trace
- asyncId
- parentAsyncId
- To support concurrent promises
Promise.race,Promise.anyandPromise.allare patched as well.- promiseConcurrency.ts
- handles untracking of losing promises
- (!) cannot handle nested promises, see known limitations
Real timers
Global functions are patched timers.ts:
setTimeoutsetIntervalclearTimeoutclearIntervalsetImmediateclearImmediate
Records:
- stack trace
- type: which of the patched method created the leak
- isAllowed: if the leak should be reported. It is still necessary to track all timers if the option
clearTimersistruebutreport.timersisfalse, to be able to clear them.
The legacy fake timer global useRealTimers function is also patched to restore patches once applied.
Fake timers
Used when Jest fake timers are enabled fakeTimers.ts:
- patches global
useFakeTimersfor legacy and modern fake timers to know when to patch the timers - patches same methods as real timers with similar records
- it uses
Object.assignto preserve existing mocking
Console detection
Console methods are patched console.ts
Console output is treated as a leak.
Records:
- stack trace
- method
Rationale:
Treating console output as a leak is a deliberate strictness choice. This enforces explicit assertions and prevents silent failures in CI. The react example shows a common problem that can be caught by tests that mock console correctly.
Process outputs
process.(stderr/stdout).write are patched processOutput.ts:
- Process output is treated as a leak.
- Same records as console.
- This will not fire if already caught by console.
DOM Listeners
window.(add/remove)EventListener are patched domListeners.ts
Attached DOM listeners after a test are treated as a leak.
Records:
- stack trace
- event
- handler
- options
Ownership attribution
All resources are associated using:
currentTestName- defined by
AsyncLocalStorage.run - becomes
main-threadif not associated with a test
- defined by
- Patches
it,test, and lifecycle hooks
This ensures:
- Correct attribution
- Accurate error reporting
- all leaks attributed with
main-threadwill be reported at the end of the test suite- usually this is a sign that the
beforeAll,afterAllor other global code has leaks.
- usually this is a sign that the
π§Ή Cleanup
jest-doctor will clear open timers to avoid cascading failures and ensure test isolation. Clean up happens after each test and will not interfere with reporting.
But it will still throw or warn based on configuration. Warnings and errors never suppress cleanup or attribution.
π Error reporting
Leaks are reported with:
- Leak type
- stack
A single error is thrown per test for clarity.
All reports are summed up and sent to the reporter at the end of each test file.