jest-doctor 0.1.0 โ 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/createEnvMixin.d.ts +6 -6
- package/dist/createEnvMixin.js +3 -1
- package/dist/env/jsdom.d.ts +9 -9
- package/dist/env/node.d.ts +9 -9
- package/dist/patch/createAsyncHookCleaner.d.ts +1 -1
- package/dist/patch/createAsyncHookDetector.d.ts +1 -1
- package/dist/patch/fakeTimers.js +8 -8
- package/dist/patch/promiseConcurrency.js +4 -4
- package/dist/reporter.d.ts +1 -3
- package/dist/reporter.js +3 -2
- package/dist/utils/getStack.js +1 -2
- package/dist/utils/initOriginal.d.ts +6 -6
- package/package.json +15 -11
- package/readme.md +302 -165
package/dist/createEnvMixin.d.ts
CHANGED
|
@@ -38,12 +38,12 @@ declare const createEnvMixin: <EnvironmentConstructor extends JestDoctorConstruc
|
|
|
38
38
|
(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
|
|
39
39
|
};
|
|
40
40
|
console: {
|
|
41
|
-
log: (
|
|
42
|
-
info: (
|
|
43
|
-
warn: (
|
|
44
|
-
error: (
|
|
45
|
-
debug: (
|
|
46
|
-
trace: (
|
|
41
|
+
log: (...data: any[]) => void;
|
|
42
|
+
info: (...data: any[]) => void;
|
|
43
|
+
warn: (...data: any[]) => void;
|
|
44
|
+
error: (...data: any[]) => void;
|
|
45
|
+
debug: (...data: any[]) => void;
|
|
46
|
+
trace: (...data: any[]) => void;
|
|
47
47
|
};
|
|
48
48
|
};
|
|
49
49
|
readonly promiseOwner: Map<number, string>;
|
package/dist/createEnvMixin.js
CHANGED
|
@@ -55,8 +55,10 @@ const createEnvMixin = (Environment) => {
|
|
|
55
55
|
processOutputs: 0,
|
|
56
56
|
};
|
|
57
57
|
const tmpDir = (0, getReporterTmpDir_1.default)(config.projectConfig.reporters || config.globalConfig.reporters);
|
|
58
|
+
const isWorker = typeof process.send === 'function';
|
|
59
|
+
const seed = (isWorker ? process.ppid : process.pid).toString();
|
|
58
60
|
this.reporterTmpDir = tmpDir
|
|
59
|
-
? node_path_1.default.join(tmpDir,
|
|
61
|
+
? node_path_1.default.join(tmpDir, seed, this.testPath + '.json')
|
|
60
62
|
: '';
|
|
61
63
|
this.options = (0, normalizeOptions_1.default)(config.projectConfig.testEnvironmentOptions);
|
|
62
64
|
(0, initLeakRecord_1.default)(this, consts_1.MAIN_THREAD);
|
package/dist/env/jsdom.d.ts
CHANGED
|
@@ -16,17 +16,17 @@ declare const _default: {
|
|
|
16
16
|
(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
|
|
17
17
|
};
|
|
18
18
|
console: {
|
|
19
|
-
log: (
|
|
20
|
-
info: (
|
|
21
|
-
warn: (
|
|
22
|
-
error: (
|
|
23
|
-
debug: (
|
|
24
|
-
trace: (
|
|
19
|
+
log: (...data: any[]) => void;
|
|
20
|
+
info: (...data: any[]) => void;
|
|
21
|
+
warn: (...data: any[]) => void;
|
|
22
|
+
error: (...data: any[]) => void;
|
|
23
|
+
debug: (...data: any[]) => void;
|
|
24
|
+
trace: (...data: any[]) => void;
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
readonly promiseOwner: Map<number, string>;
|
|
28
|
-
readonly asyncHookDetector?: import("async_hooks").AsyncHook;
|
|
29
|
-
readonly asyncHookCleaner?: import("async_hooks").AsyncHook;
|
|
28
|
+
readonly asyncHookDetector?: import("node:async_hooks").AsyncHook;
|
|
29
|
+
readonly asyncHookCleaner?: import("node:async_hooks").AsyncHook;
|
|
30
30
|
readonly options: import("../types").NormalizedOptions;
|
|
31
31
|
seenTearDown: boolean;
|
|
32
32
|
currentAfterEachCount: number;
|
|
@@ -53,7 +53,7 @@ declare const _default: {
|
|
|
53
53
|
fakeTimers: import("@jest/fake-timers").LegacyFakeTimers<unknown> | null;
|
|
54
54
|
fakeTimersModern: import("@jest/fake-timers").ModernFakeTimers | null;
|
|
55
55
|
moduleMocker: import("jest-mock").ModuleMocker | null;
|
|
56
|
-
getVmContext: () => import("vm").Context | null;
|
|
56
|
+
getVmContext: () => import("node:vm").Context | null;
|
|
57
57
|
exportConditions: (() => Array<string>) | undefined;
|
|
58
58
|
};
|
|
59
59
|
} & import("../createEnvMixin").JestDoctorConstructor;
|
package/dist/env/node.d.ts
CHANGED
|
@@ -16,17 +16,17 @@ declare const _default: {
|
|
|
16
16
|
(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
|
|
17
17
|
};
|
|
18
18
|
console: {
|
|
19
|
-
log: (
|
|
20
|
-
info: (
|
|
21
|
-
warn: (
|
|
22
|
-
error: (
|
|
23
|
-
debug: (
|
|
24
|
-
trace: (
|
|
19
|
+
log: (...data: any[]) => void;
|
|
20
|
+
info: (...data: any[]) => void;
|
|
21
|
+
warn: (...data: any[]) => void;
|
|
22
|
+
error: (...data: any[]) => void;
|
|
23
|
+
debug: (...data: any[]) => void;
|
|
24
|
+
trace: (...data: any[]) => void;
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
readonly promiseOwner: Map<number, string>;
|
|
28
|
-
readonly asyncHookDetector?: import("async_hooks").AsyncHook;
|
|
29
|
-
readonly asyncHookCleaner?: import("async_hooks").AsyncHook;
|
|
28
|
+
readonly asyncHookDetector?: import("node:async_hooks").AsyncHook;
|
|
29
|
+
readonly asyncHookCleaner?: import("node:async_hooks").AsyncHook;
|
|
30
30
|
readonly options: import("../types").NormalizedOptions;
|
|
31
31
|
seenTearDown: boolean;
|
|
32
32
|
currentAfterEachCount: number;
|
|
@@ -53,7 +53,7 @@ declare const _default: {
|
|
|
53
53
|
fakeTimers: import("@jest/fake-timers").LegacyFakeTimers<unknown> | null;
|
|
54
54
|
fakeTimersModern: import("@jest/fake-timers").ModernFakeTimers | null;
|
|
55
55
|
moduleMocker: import("jest-mock").ModuleMocker | null;
|
|
56
|
-
getVmContext: () => import("vm").Context | null;
|
|
56
|
+
getVmContext: () => import("node:vm").Context | null;
|
|
57
57
|
exportConditions: (() => Array<string>) | undefined;
|
|
58
58
|
};
|
|
59
59
|
} & import("../createEnvMixin").JestDoctorConstructor;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { JestDoctorEnvironment } from '../types';
|
|
2
|
-
declare const createAsyncHookCleaner: (that: JestDoctorEnvironment) => import("async_hooks").AsyncHook;
|
|
2
|
+
declare const createAsyncHookCleaner: (that: JestDoctorEnvironment) => import("node:async_hooks").AsyncHook;
|
|
3
3
|
export default createAsyncHookCleaner;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { JestDoctorEnvironment } from '../types';
|
|
2
|
-
declare const createAsyncHookDetector: (that: JestDoctorEnvironment) => import("async_hooks").AsyncHook;
|
|
2
|
+
declare const createAsyncHookDetector: (that: JestDoctorEnvironment) => import("node:async_hooks").AsyncHook;
|
|
3
3
|
export default createAsyncHookDetector;
|
package/dist/patch/fakeTimers.js
CHANGED
|
@@ -16,7 +16,7 @@ const patchFakeTimers = (that) => {
|
|
|
16
16
|
const originalFakeSetInterval = env.setInterval.bind(env);
|
|
17
17
|
const originalFakeClearTimeout = env.clearTimeout.bind(env);
|
|
18
18
|
const originalFakeClearInterval = env.clearInterval.bind(env);
|
|
19
|
-
env.setTimeout = function (callback, delay) {
|
|
19
|
+
env.setTimeout = Object.assign(function (callback, delay) {
|
|
20
20
|
const fakeTimeout = that.leakRecords.get(that.currentTestName)?.fakeTimers;
|
|
21
21
|
const timerId = originalFakeSetTimeout(() => {
|
|
22
22
|
fakeTimeout?.delete(timerId);
|
|
@@ -31,8 +31,8 @@ const patchFakeTimers = (that) => {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
return timerId;
|
|
34
|
-
};
|
|
35
|
-
env.setInterval = function (callback, delay) {
|
|
34
|
+
}, env.setTimeout);
|
|
35
|
+
env.setInterval = Object.assign(function (callback, delay) {
|
|
36
36
|
const intervalId = originalFakeSetInterval(callback, delay);
|
|
37
37
|
const stack = (0, getStack_1.default)(that.global.setInterval);
|
|
38
38
|
if (!(0, isIgnored_1.default)(stack, that.options.report.fakeTimers.ignore)) {
|
|
@@ -45,17 +45,17 @@ const patchFakeTimers = (that) => {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
return intervalId;
|
|
48
|
-
};
|
|
49
|
-
env.clearTimeout = (timerId) => {
|
|
48
|
+
}, env.setInterval);
|
|
49
|
+
env.clearTimeout = Object.assign((timerId) => {
|
|
50
50
|
that.leakRecords.get(that.currentTestName)?.fakeTimers.delete(timerId);
|
|
51
51
|
originalFakeClearTimeout(timerId);
|
|
52
|
-
};
|
|
53
|
-
env.clearInterval = (intervalId) => {
|
|
52
|
+
}, env.clearTimeout);
|
|
53
|
+
env.clearInterval = Object.assign((intervalId) => {
|
|
54
54
|
that.leakRecords
|
|
55
55
|
.get(that.currentTestName)
|
|
56
56
|
?.fakeTimers.delete(intervalId);
|
|
57
57
|
originalFakeClearInterval(intervalId);
|
|
58
|
-
};
|
|
58
|
+
}, env.clearInterval);
|
|
59
59
|
};
|
|
60
60
|
const originalClearAllTimers = api.clearAllTimers.bind(api);
|
|
61
61
|
api.clearAllTimers = () => {
|
|
@@ -5,11 +5,11 @@ const patchPromiseConcurrency = (that) => {
|
|
|
5
5
|
const env = that.global;
|
|
6
6
|
const getRootIds = () => {
|
|
7
7
|
let triggerId = (0, node_async_hooks_1.executionAsyncId)();
|
|
8
|
-
const rootIds = [
|
|
9
|
-
|
|
10
|
-
triggerId = that.asyncIdToParentId.get(triggerId);
|
|
8
|
+
const rootIds = [];
|
|
9
|
+
do {
|
|
11
10
|
rootIds.push(triggerId);
|
|
12
|
-
|
|
11
|
+
triggerId = that.asyncIdToParentId.get(triggerId);
|
|
12
|
+
} while (triggerId && triggerId !== that.asyncRoot);
|
|
13
13
|
return rootIds;
|
|
14
14
|
};
|
|
15
15
|
const cleanPromise = (promises, promise, asyncId) => {
|
package/dist/reporter.d.ts
CHANGED
|
@@ -8,9 +8,7 @@ declare class JestDoctorReporter implements Reporter {
|
|
|
8
8
|
private readonly reportDir;
|
|
9
9
|
private readonly pidDir;
|
|
10
10
|
private readonly keep;
|
|
11
|
-
constructor(
|
|
12
|
-
seed: number;
|
|
13
|
-
}, options: ReporterOptions);
|
|
11
|
+
constructor(_: object, options: ReporterOptions);
|
|
14
12
|
onRunComplete(): Promise<void>;
|
|
15
13
|
}
|
|
16
14
|
export default JestDoctorReporter;
|
package/dist/reporter.js
CHANGED
|
@@ -56,12 +56,13 @@ class JestDoctorReporter {
|
|
|
56
56
|
reportDir;
|
|
57
57
|
pidDir;
|
|
58
58
|
keep;
|
|
59
|
-
constructor(
|
|
60
|
-
const seed =
|
|
59
|
+
constructor(_, options) {
|
|
60
|
+
const seed = process.pid.toString();
|
|
61
61
|
this.tmpDir = options.tmpDir || consts_1.REPORTER_TMP_DIR;
|
|
62
62
|
this.keep = options.keep || false;
|
|
63
63
|
this.reportDir = node_path_1.default.join(this.tmpDir, seed);
|
|
64
64
|
this.pidDir = node_path_1.default.join(this.tmpDir, 'pid');
|
|
65
|
+
(0, node_fs_1.rmSync)(this.reportDir, { recursive: true, force: true });
|
|
65
66
|
(0, node_fs_1.mkdirSync)(this.pidDir, { recursive: true });
|
|
66
67
|
(0, node_fs_1.mkdirSync)(this.reportDir, { recursive: true });
|
|
67
68
|
if (!this.keep) {
|
package/dist/utils/getStack.js
CHANGED
|
@@ -9,8 +9,7 @@ const getStack = (stackFrom) => {
|
|
|
9
9
|
lines.shift();
|
|
10
10
|
const finalStack = [];
|
|
11
11
|
for (const line of lines) {
|
|
12
|
-
if (/
|
|
13
|
-
!line.includes('(node:internal/') &&
|
|
12
|
+
if (!line.includes('(node:internal/') &&
|
|
14
13
|
!line.includes('node_modules/jest-runtime') &&
|
|
15
14
|
!line.includes('node_modules/jest-circus') &&
|
|
16
15
|
!line.includes('node_modules/jest-runner') &&
|
|
@@ -12,12 +12,12 @@ declare const initOriginal: () => {
|
|
|
12
12
|
(str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error | null) => void): boolean;
|
|
13
13
|
};
|
|
14
14
|
console: {
|
|
15
|
-
log: (
|
|
16
|
-
info: (
|
|
17
|
-
warn: (
|
|
18
|
-
error: (
|
|
19
|
-
debug: (
|
|
20
|
-
trace: (
|
|
15
|
+
log: (...data: any[]) => void;
|
|
16
|
+
info: (...data: any[]) => void;
|
|
17
|
+
warn: (...data: any[]) => void;
|
|
18
|
+
error: (...data: any[]) => void;
|
|
19
|
+
debug: (...data: any[]) => void;
|
|
20
|
+
trace: (...data: any[]) => void;
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
23
|
export default initOriginal;
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jest-doctor",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "jest environment for leak detection",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "jest environment for leak and issue detection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Stephan Dum",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"homepage": "https://github.com/stephan-dum/jest-doctor",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/stephan-dum/jest-doctor.git"
|
|
11
|
+
"url": "git+https://github.com/stephan-dum/jest-doctor.git",
|
|
12
|
+
"directory": "packages/jest-doctor"
|
|
12
13
|
},
|
|
13
14
|
"exports": {
|
|
14
15
|
"./package.json": "./package.json",
|
|
@@ -26,7 +27,10 @@
|
|
|
26
27
|
"jest-doctor",
|
|
27
28
|
"leak detection",
|
|
28
29
|
"open handles",
|
|
29
|
-
"
|
|
30
|
+
"issue detection",
|
|
31
|
+
"testing",
|
|
32
|
+
"jest environment",
|
|
33
|
+
"test hygien"
|
|
30
34
|
],
|
|
31
35
|
"types": "./dist/types.d.ts",
|
|
32
36
|
"devDependencies": {
|
|
@@ -37,11 +41,11 @@
|
|
|
37
41
|
"@swc/jest": "^0.2.39",
|
|
38
42
|
"@testing-library/dom": "^10.4.1",
|
|
39
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
40
|
-
"@testing-library/react": "^16.3.
|
|
44
|
+
"@testing-library/react": "^16.3.2",
|
|
41
45
|
"@types/cross-spawn": "^6.0.6",
|
|
42
46
|
"@types/jest": "30.0.0",
|
|
43
|
-
"@types/node": "
|
|
44
|
-
"@types/react": "^19.2.
|
|
47
|
+
"@types/node": "^25.0.10",
|
|
48
|
+
"@types/react": "^19.2.9",
|
|
45
49
|
"@types/react-dom": "^19.2.3",
|
|
46
50
|
"c8": "^10.1.3",
|
|
47
51
|
"cross-spawn": "^7.0.6",
|
|
@@ -49,12 +53,12 @@
|
|
|
49
53
|
"jest": "30.2.0",
|
|
50
54
|
"jest-environment-jsdom": "^30.2.0",
|
|
51
55
|
"jest-environment-node": "^30.2.0",
|
|
52
|
-
"memfs": "^4.
|
|
56
|
+
"memfs": "^4.56.10",
|
|
53
57
|
"nyc": "^17.1.0",
|
|
54
58
|
"react": "^19.2.3",
|
|
55
59
|
"react-dom": "^19.2.3",
|
|
56
60
|
"ts-jest": "^29.4.6",
|
|
57
|
-
"typescript": "5.9.
|
|
61
|
+
"typescript": "5.9.3"
|
|
58
62
|
},
|
|
59
63
|
"peerDependencies": {
|
|
60
64
|
"jest-environment-jsdom": "*",
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
},
|
|
71
75
|
"dependencies": {
|
|
72
76
|
"chalk": "4.1.2",
|
|
73
|
-
"zod": "^4.3.
|
|
77
|
+
"zod": "^4.3.6"
|
|
74
78
|
},
|
|
75
79
|
"scripts": {
|
|
76
80
|
"typecheck": "tsc",
|
|
@@ -80,6 +84,6 @@
|
|
|
80
84
|
"test:unit": "jest --coverage",
|
|
81
85
|
"test:matrix": "node ./e2e/runMatrix.mjs",
|
|
82
86
|
"test": "yarn matrix && yarn test:unit && yarn coverage:merge",
|
|
83
|
-
"coverage:merge": "nyc report
|
|
87
|
+
"coverage:merge": "nyc report"
|
|
84
88
|
}
|
|
85
89
|
}
|
package/readme.md
CHANGED
|
@@ -1,165 +1,302 @@
|
|
|
1
|
-
# jest-doctor
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
It
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
onError: '
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
# jest-doctor [](https://github.com/stephan-dum/jest-doctor/actions/workflows/main.yml) [](https://codecov.io/gh/stephan-dum/jest-doctor) [](https://www.npmjs.com/package/jest-doctor) [](./LICENSE)
|
|
2
|
+
|
|
3
|
+
jest-doctor is a custom Jest environment that fails tests deterministically
|
|
4
|
+
when [async work leaks](#what-is-an-async-leak) across test boundaries.
|
|
5
|
+
It prevents flaky tests and enforces strong test hygiene.
|
|
6
|
+
|
|
7
|
+
## โจ What problems does it catch?
|
|
8
|
+
|
|
9
|
+
It detects and reports when tests:
|
|
10
|
+
- Leave unresolved Promises
|
|
11
|
+
- Leave open real or fake timers
|
|
12
|
+
- Rely on excessive real-time delays
|
|
13
|
+
- Emit process / console outputs
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
**Docs**
|
|
17
|
+
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Reporter](#reporter)
|
|
21
|
+
- [When not to use jest-doctor](#when-not-to-use-jest-doctor)
|
|
22
|
+
- [Limitations](#limitations)
|
|
23
|
+
- [FAQ](#faq)
|
|
24
|
+
- [Migration](./docs/migration.md)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
## ๐ Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev jest-doctor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
or
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
yarn add -D jest-doctor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Add one of the provided environments to your `jest.config.js`.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
export default {
|
|
43
|
+
testEnvironment: 'jest-doctor/env/node',
|
|
44
|
+
// optional
|
|
45
|
+
reporters: ['default', 'jest-doctor/reporter'],
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Out-of-the-box jest-doctor supports node and jsdom environments. But you can also [build your own environment](./docs/build_your_own_environment.md).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
## โ๏ธ Configuration
|
|
53
|
+
|
|
54
|
+
The environment can be configured through the Jest config `testEnvironmentOptions`:
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
export default {
|
|
59
|
+
testEnvironmentOptions: {
|
|
60
|
+
report: {
|
|
61
|
+
console: {
|
|
62
|
+
onError: 'warn',
|
|
63
|
+
methods: ['log', 'warn', 'error'],
|
|
64
|
+
ignore: /Third party message/,
|
|
65
|
+
},
|
|
66
|
+
timers: {
|
|
67
|
+
onError: 'warn',
|
|
68
|
+
},
|
|
69
|
+
fakeTimers: {
|
|
70
|
+
onError: 'throw',
|
|
71
|
+
},
|
|
72
|
+
promises: false,
|
|
73
|
+
processOutputs: {
|
|
74
|
+
onError: 'warn',
|
|
75
|
+
methods: ['stderr'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
delayThreshold: 1000,
|
|
79
|
+
timerIsolation: 'afterEach',
|
|
80
|
+
clearTimers: true,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### report
|
|
86
|
+
Controls which leak types are detected and how they are reported.
|
|
87
|
+
|
|
88
|
+
Each option can be:
|
|
89
|
+
- false โ disabled
|
|
90
|
+
- object โ enabled with configuration
|
|
91
|
+
|
|
92
|
+
Common options:
|
|
93
|
+
- **onError**: `'warn' | 'throw'` (default: `'throw'`)
|
|
94
|
+
- ignore: `string | RegExp | Array<string | RegExp>` (default: `[]`)
|
|
95
|
+
If the stack trace or emitted message matches, the leak is ignored.
|
|
96
|
+
|
|
97
|
+
#### possible report options
|
|
98
|
+
|
|
99
|
+
- **timers:** track real timers
|
|
100
|
+
- **fakeTimers:** track fake timers
|
|
101
|
+
- **promises:** track not awaited promises
|
|
102
|
+
- **console:** track console output
|
|
103
|
+
- **methods:** `Array<keyof Console>` (default: all) which console methods should be tracked
|
|
104
|
+
- **processOutputs:** track process output
|
|
105
|
+
- **methods:** `Array<'stderr' | 'stdout'>` (default: both) which process output methods should be tracked
|
|
106
|
+
|
|
107
|
+
### timerIsolation
|
|
108
|
+
Controls when timers are validated and cleared.
|
|
109
|
+
|
|
110
|
+
**afterEach** (default)
|
|
111
|
+
`beforeAll`, `beforeEach` and `afterAll` are still immediate but `test` / `it` and `afterEach` block defer reporting and cleanup until the last `afterEach` block is executed (or directly after the test if there are no `afterEach` blocks).
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
beforeAll โ check
|
|
115
|
+
beforeEach โ check
|
|
116
|
+
test โ defer
|
|
117
|
+
afterEach โ defer
|
|
118
|
+
afterEach โ final check
|
|
119
|
+
afterAll โ check
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This allows easier cleanup., for example react testing framework registers an unmount function in an `afterEach` block to clean up.
|
|
123
|
+
The disadvantage of this method is that it can happen that in an afterEach block a long-running task is executed and while running it timers resolve unnoticed.
|
|
124
|
+
|
|
125
|
+
**immediate**
|
|
126
|
+
timers are checked **after** each test / hook block
|
|
127
|
+
```
|
|
128
|
+
beforeAll โ check
|
|
129
|
+
beforeEach โ check
|
|
130
|
+
test โ check
|
|
131
|
+
afterEach โ check
|
|
132
|
+
afterAll โ check
|
|
133
|
+
```
|
|
134
|
+
Use when tests should clean up immediately.
|
|
135
|
+
|
|
136
|
+
### delayThreshold
|
|
137
|
+
`number` (default: `0`)
|
|
138
|
+
|
|
139
|
+
The delay in milliseconds of all `setTimeout` and `setInterval` callback that get executed is added up.
|
|
140
|
+
If the sum is higher than the threshold, an error is thrown; otherwise a warning is logged.
|
|
141
|
+
This feature should helps to detect tests that accidentally rely on real time.
|
|
142
|
+
|
|
143
|
+
### clearTimers
|
|
144
|
+
`boolean` (default: `true`)
|
|
145
|
+
|
|
146
|
+
Whether timers should be cleared automatically based on `timerIsolation`.
|
|
147
|
+
|
|
148
|
+
### verbose
|
|
149
|
+
`boolean` (default: `false`)
|
|
150
|
+
|
|
151
|
+
Jest often hides stack traces and files are not clickable.
|
|
152
|
+
Also it is only possible to report one error type at a time.
|
|
153
|
+
This option will print all errors with the related stack traces.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
## ๐ Reporter
|
|
157
|
+
|
|
158
|
+
The reporter aggregates leaks across all test environments and prints:
|
|
159
|
+
|
|
160
|
+
- Total number of leaks
|
|
161
|
+
- Grouped by type (timers, promises, console, etc.)
|
|
162
|
+
- Ordered by severity
|
|
163
|
+
|
|
164
|
+
The environment writes temporary reports to disk and the reporter reads them.
|
|
165
|
+
|
|
166
|
+
The reporter can be configured by the standard jest reporter config syntax
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
- **tmpDir**: `string` (default: `.tmp`) Directory used to exchange data between environment and reporter.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
export default {
|
|
175
|
+
reporters: ['default', ['jest-doctor/reporter', { tmpDir: 'custom-dir' }]],
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
## โ ๏ธ Limitations
|
|
181
|
+
|
|
182
|
+
### No it.concurrent
|
|
183
|
+
Concurrent tests cannot be isolated reliably. jest-doctor replaces them with
|
|
184
|
+
a synchronous version to guarantee deterministic cleanup.
|
|
185
|
+
|
|
186
|
+
### No done callbacks or generators
|
|
187
|
+
Since this is also a legacy pattern, it is not supported to avoid unnecessary complexity.
|
|
188
|
+
|
|
189
|
+
## Results are inconsistent
|
|
190
|
+
Promises are handled differently depending on the OS and node version.
|
|
191
|
+
This means the report will always look a bit different depending on the environment.
|
|
192
|
+
|
|
193
|
+
### Microtasks resolving in same tick are not tracked
|
|
194
|
+
This is a JavaScript limitation, not specific to jest-doctor.
|
|
195
|
+
```js
|
|
196
|
+
Promise.resolve().then(() => {
|
|
197
|
+
/* i am not tracked as unresolved */
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Concurrent promise combinators with nested async are problematic
|
|
202
|
+
`Promise.race`, `Promise.any`, `Promise.all` cannot safely untrack nested async:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
const doSomething = async () => {
|
|
206
|
+
// both promises will be tracked and never released
|
|
207
|
+
await someAsyncTask();
|
|
208
|
+
return new Promise(() => {
|
|
209
|
+
setTimeout(resolve, 10);
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const p1 = Promise.resolve().then(() => {
|
|
214
|
+
/* no problem if not async */
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const p2 = Promise.resolve().then(
|
|
218
|
+
() =>
|
|
219
|
+
new Promise((resolve) => {
|
|
220
|
+
/* this promise will be also always tracked */
|
|
221
|
+
resolve();
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await Promise.race([p1, p2, doSomething()]);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Imported timers bypass tracking
|
|
229
|
+
These timers are not intercepted. This can also be used as an escape hatch.
|
|
230
|
+
```js
|
|
231
|
+
import { setTimeout, setInterval } from 'node:timers';
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
## ๐ซ When not to use jest-doctor
|
|
236
|
+
- Heavy integration tests with background workers
|
|
237
|
+
- Tests relying on long-running real timers
|
|
238
|
+
- Legacy test suites using callback-based async
|
|
239
|
+
|
|
240
|
+
In such cases, consider selectively disabling checks or using ignore rules.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
## ๐ก Recommendations
|
|
244
|
+
|
|
245
|
+
- Use ESLint to
|
|
246
|
+
- detect floating promises
|
|
247
|
+
- disallow setTimeout / setInterval in tests
|
|
248
|
+
- disallow console usage
|
|
249
|
+
- Only mock console / process output *per test* not globally, to avoid missing out on errors that are thrown in silence
|
|
250
|
+
- Enable fake timers globally in config (be aware that there might be some issues ie axe needs real timers)
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
afterEach(async () => {
|
|
254
|
+
jest.useRealTimers();
|
|
255
|
+
await axe();
|
|
256
|
+
jest.useFakeTimers();
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
## ๐งช Tested Against
|
|
262
|
+
|
|
263
|
+
This project is tested against the following combinations:
|
|
264
|
+
- **jest**: 28, 29, 30
|
|
265
|
+
- **node**: 20, 22, 24
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
# โ FAQ
|
|
269
|
+
|
|
270
|
+
### How to migrate an existing project?
|
|
271
|
+
|
|
272
|
+
Please read the [migration guide](./docs/migration.md).
|
|
273
|
+
|
|
274
|
+
### Why is jest-doctor so strict?
|
|
275
|
+
|
|
276
|
+
Because flaky tests cost more than broken builds.
|
|
277
|
+
|
|
278
|
+
### Does this slow tests down?
|
|
279
|
+
|
|
280
|
+
Slightly. Overhead is intentional and bounded.
|
|
281
|
+
|
|
282
|
+
### What is an async leak?
|
|
283
|
+
|
|
284
|
+
An async leak happens when a test starts asynchronous work but does not
|
|
285
|
+
properly wait for or clean it up. This can:
|
|
286
|
+
|
|
287
|
+
- Interfere with later tests
|
|
288
|
+
- Cause flaky failures
|
|
289
|
+
- Hide real bugs
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
### Why does console output fail tests?
|
|
293
|
+
|
|
294
|
+
In the best case it just pollutes the console.
|
|
295
|
+
In the worst case a real bug is logged but ignored.
|
|
296
|
+
Thats why tests should always spy on console and assert on the output.
|
|
297
|
+
The [react example](./e2e/fixtures/react.fixture.tsx#L20-L25) shows a common problem that can be caught by tests that mock console correctly.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
If jest-doctor helped you eliminate flaky tests, consider โญ starring the repo โ
|
|
302
|
+
it helps others discover the project and motivates continued development.
|