jest-doctor 2.0.0 → 2.0.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.js +9 -2
- package/dist/patch/promise.d.ts +3 -0
- package/dist/patch/promise.js +74 -0
- package/dist/types.d.ts +5 -2
- package/dist/utils/analyzeCallback.js +4 -1
- package/dist/utils/normalizeOptions.js +3 -1
- package/package.json +97 -97
- package/readme.md +2 -2
package/dist/createEnvMixin.js
CHANGED
|
@@ -24,6 +24,7 @@ const getReporterTmpDir_1 = __importDefault(require("./utils/getReporterTmpDir")
|
|
|
24
24
|
const promiseConcurrency_1 = __importDefault(require("./patch/promiseConcurrency"));
|
|
25
25
|
const processOutput_1 = __importDefault(require("./patch/processOutput"));
|
|
26
26
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
27
|
+
const promise_1 = __importDefault(require("./patch/promise"));
|
|
27
28
|
const createEnvMixin = (Environment) => {
|
|
28
29
|
// @ts-expect-error strange ts rule where constructor arguments should be any[] where again TypeScript complains
|
|
29
30
|
return class Base extends Environment {
|
|
@@ -72,7 +73,8 @@ const createEnvMixin = (Environment) => {
|
|
|
72
73
|
: '';
|
|
73
74
|
this.options = (0, normalizeOptions_1.default)(config.projectConfig.testEnvironmentOptions);
|
|
74
75
|
(0, initLeakRecord_1.default)(this, consts_1.MAIN_THREAD);
|
|
75
|
-
|
|
76
|
+
const promiseOptions = this.options.report.promises;
|
|
77
|
+
if (promiseOptions && promiseOptions.mode === 'async_hooks') {
|
|
76
78
|
this.asyncHookCleaner = (0, createAsyncHookCleaner_1.default)(this);
|
|
77
79
|
this.asyncHookDetector = (0, createAsyncHookDetector_1.default)(this);
|
|
78
80
|
}
|
|
@@ -92,7 +94,12 @@ const createEnvMixin = (Environment) => {
|
|
|
92
94
|
(0, processOutput_1.default)(this, report.processOutputs);
|
|
93
95
|
}
|
|
94
96
|
if (report.promises) {
|
|
95
|
-
(
|
|
97
|
+
if (report.promises.mode === 'async_hooks') {
|
|
98
|
+
(0, promiseConcurrency_1.default)(this);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
(0, promise_1.default)(this);
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
// unfortunately @jest/types doesn't export the necessary types,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const getStack_1 = __importDefault(require("../utils/getStack"));
|
|
7
|
+
const isIgnored_1 = __importDefault(require("../utils/isIgnored"));
|
|
8
|
+
const patchPromise = (that) => {
|
|
9
|
+
const global = that.global;
|
|
10
|
+
const OriginalPromise = global.Promise;
|
|
11
|
+
const ignoreStack = that.options.report.promises
|
|
12
|
+
.ignoreStack;
|
|
13
|
+
const track = (promise, stackFrom) => {
|
|
14
|
+
const stack = (0, getStack_1.default)(stackFrom);
|
|
15
|
+
if (!(0, isIgnored_1.default)(stack, ignoreStack)) {
|
|
16
|
+
const promises = that.leakRecords.get(that.currentTestName)?.promises;
|
|
17
|
+
promises?.set(promise, {
|
|
18
|
+
stack,
|
|
19
|
+
asyncId: 0,
|
|
20
|
+
parentAsyncId: 0,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const untrack = (promise) => {
|
|
25
|
+
that.leakRecords.get(that.currentTestName)?.promises.delete(promise);
|
|
26
|
+
};
|
|
27
|
+
const concurrencyFactory = (promises, method) => {
|
|
28
|
+
promises.forEach((promise) => {
|
|
29
|
+
promise.untrack = true;
|
|
30
|
+
});
|
|
31
|
+
return OriginalPromise[method](promises).finally(() => {
|
|
32
|
+
promises.forEach((promise) => {
|
|
33
|
+
untrack(promise);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
class TrackedPromise extends OriginalPromise {
|
|
38
|
+
untrack;
|
|
39
|
+
constructor(executor) {
|
|
40
|
+
const promises = that.leakRecords.get(that.currentTestName)?.promises;
|
|
41
|
+
const innerExecutor = (resolve, reject) => {
|
|
42
|
+
const wrappedResolve = (value) => {
|
|
43
|
+
promises?.delete(this);
|
|
44
|
+
resolve(value);
|
|
45
|
+
};
|
|
46
|
+
const wrappedReject = (reason) => {
|
|
47
|
+
promises?.delete(this);
|
|
48
|
+
reject(reason);
|
|
49
|
+
};
|
|
50
|
+
return executor(wrappedResolve, wrappedReject);
|
|
51
|
+
};
|
|
52
|
+
super(innerExecutor);
|
|
53
|
+
track(this, TrackedPromise);
|
|
54
|
+
}
|
|
55
|
+
then(onFulfilled, onRejected) {
|
|
56
|
+
const promise = super.then(onFulfilled, onRejected);
|
|
57
|
+
if (this.untrack) {
|
|
58
|
+
untrack(promise);
|
|
59
|
+
}
|
|
60
|
+
return promise;
|
|
61
|
+
}
|
|
62
|
+
static any(promises) {
|
|
63
|
+
return concurrencyFactory(promises, 'any');
|
|
64
|
+
}
|
|
65
|
+
static race(promises) {
|
|
66
|
+
return concurrencyFactory(promises, 'race');
|
|
67
|
+
}
|
|
68
|
+
static all(promises) {
|
|
69
|
+
return concurrencyFactory(promises, 'all');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
global.Promise = TrackedPromise;
|
|
73
|
+
};
|
|
74
|
+
exports.default = patchPromise;
|
package/dist/types.d.ts
CHANGED
|
@@ -61,6 +61,9 @@ type OutputAddition = {
|
|
|
61
61
|
methods: Array<'stderr' | 'stdout'>;
|
|
62
62
|
ignoreMessage: IsIgnored;
|
|
63
63
|
};
|
|
64
|
+
type PromiseAddition = {
|
|
65
|
+
mode: 'async_hooks' | 'subclass';
|
|
66
|
+
};
|
|
64
67
|
export type ReportOptions<Type = object> = {
|
|
65
68
|
onError: ThrowOrWarn;
|
|
66
69
|
ignoreStack: IsIgnored;
|
|
@@ -74,7 +77,7 @@ export interface NormalizedOptions {
|
|
|
74
77
|
processOutputs: NormalizedReportOptions<OutputAddition>;
|
|
75
78
|
timers: NormalizedReportOptions;
|
|
76
79
|
fakeTimers: NormalizedReportOptions;
|
|
77
|
-
promises: NormalizedReportOptions
|
|
80
|
+
promises: NormalizedReportOptions<PromiseAddition>;
|
|
78
81
|
domListeners: NormalizedReportOptions;
|
|
79
82
|
};
|
|
80
83
|
verbose: boolean;
|
|
@@ -100,7 +103,7 @@ export interface RawOptions {
|
|
|
100
103
|
processOutputs?: RawReportOptions<RawOutputAddition>;
|
|
101
104
|
timers?: RawReportOptions;
|
|
102
105
|
fakeTimers?: RawReportOptions;
|
|
103
|
-
promises?: RawReportOptions
|
|
106
|
+
promises?: RawReportOptions<Partial<PromiseAddition>>;
|
|
104
107
|
domListeners?: RawReportOptions;
|
|
105
108
|
};
|
|
106
109
|
verbose?: boolean;
|
|
@@ -20,7 +20,10 @@ const analyzeCallback = async (that, callback, testContext, timeout, isHook) =>
|
|
|
20
20
|
const testName = that.global.expect.getState().currentTestName ||
|
|
21
21
|
'unknown';
|
|
22
22
|
const leakRecord = (0, initLeakRecord_1.default)(that, testName);
|
|
23
|
-
|
|
23
|
+
const option = that.options.report.promises;
|
|
24
|
+
if (option && option.mode === 'async_hooks') {
|
|
25
|
+
that.asyncRoot = (0, node_async_hooks_1.executionAsyncId)();
|
|
26
|
+
}
|
|
24
27
|
let timerId;
|
|
25
28
|
return that.asyncStorage.run('ignored', () => {
|
|
26
29
|
return new Promise((resolve, reject) => {
|
|
@@ -91,7 +91,9 @@ const schema = zod
|
|
|
91
91
|
}).default(DEFAULTS.report.console),
|
|
92
92
|
timers: createReportHandler().default(DEFAULTS.report.timers),
|
|
93
93
|
fakeTimers: createReportHandler().default(DEFAULTS.report.fakeTimers),
|
|
94
|
-
promises: createReportHandler(
|
|
94
|
+
promises: createReportHandler({
|
|
95
|
+
mode: zod.enum(['async_hooks', 'subclass']).default('async_hooks'),
|
|
96
|
+
}).default(DEFAULTS.report.promises),
|
|
95
97
|
domListeners: createReportHandler().default(DEFAULTS.report.domListeners),
|
|
96
98
|
processOutputs: createReportHandler({
|
|
97
99
|
methods: zod
|
package/package.json
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "jest-doctor",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Custom Jest environment that detects async leaks between tests, enforces test isolation, and prevents flaky failures in CI.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Stephan Dum",
|
|
7
|
-
"type": "commonjs",
|
|
8
|
-
"homepage": "https://stephan-dum.github.io/jest-doctor/",
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/stephan-dum/jest-doctor.git",
|
|
12
|
-
"directory": "packages/jest-doctor"
|
|
13
|
-
},
|
|
14
|
-
"exports": {
|
|
15
|
-
"./package.json": "./package.json",
|
|
16
|
-
"./createEnvMixin": "./dist/createEnvMixin.js",
|
|
17
|
-
"./env/node": "./dist/env/node.js",
|
|
18
|
-
"./env/jsdom": "./dist/env/jsdom.js",
|
|
19
|
-
"./reporter": "./dist/reporter.js"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"keywords": [
|
|
25
|
-
"jest",
|
|
26
|
-
"jest-doctor",
|
|
27
|
-
"leak detection",
|
|
28
|
-
"open handles",
|
|
29
|
-
"issue detection",
|
|
30
|
-
"testing",
|
|
31
|
-
"jest environment",
|
|
32
|
-
"test hygiene",
|
|
33
|
-
"flaky-tests",
|
|
34
|
-
"test-isolation",
|
|
35
|
-
"async-leaks",
|
|
36
|
-
"developer-tools",
|
|
37
|
-
"ci",
|
|
38
|
-
"test diagnostics"
|
|
39
|
-
],
|
|
40
|
-
"types": "./dist/types.d.ts",
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@dev/eslint": "1.0.0",
|
|
43
|
-
"@dev/tsconfig": "1.0.0",
|
|
44
|
-
"@jest/reporters": "30.2.0",
|
|
45
|
-
"@swc/core": "^1.15.11",
|
|
46
|
-
"@swc/jest": "^0.2.39",
|
|
47
|
-
"@testing-library/dom": "^10.4.1",
|
|
48
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
-
"@testing-library/react": "^16.3.2",
|
|
50
|
-
"@types/cross-spawn": "^6.0.6",
|
|
51
|
-
"@types/jest": "^30.0.0",
|
|
52
|
-
"@types/node": "^25.2.1",
|
|
53
|
-
"@types/react": "^19.2.13",
|
|
54
|
-
"@types/react-dom": "^19.2.3",
|
|
55
|
-
"c8": "^10.1.3",
|
|
56
|
-
"cross-spawn": "^7.0.6",
|
|
57
|
-
"eslint": "^9.39.2",
|
|
58
|
-
"jest": "30.2.0",
|
|
59
|
-
"jest-environment-jsdom": "^30.2.0",
|
|
60
|
-
"jest-environment-node": "^30.2.0",
|
|
61
|
-
"memfs": "^4.56.10",
|
|
62
|
-
"nyc": "^17.1.0",
|
|
63
|
-
"react": "^19.2.4",
|
|
64
|
-
"react-dom": "^19.2.4",
|
|
65
|
-
"shx": "^0.4.0",
|
|
66
|
-
"ts-jest": "^29.4.6",
|
|
67
|
-
"typescript": "5.9.3"
|
|
68
|
-
},
|
|
69
|
-
"peerDependencies": {
|
|
70
|
-
"jest": ">=28",
|
|
71
|
-
"jest-environment-jsdom": "*",
|
|
72
|
-
"jest-environment-node": "*"
|
|
73
|
-
},
|
|
74
|
-
"peerDependenciesMeta": {
|
|
75
|
-
"jest-environment-jsdom": {
|
|
76
|
-
"optional": true
|
|
77
|
-
},
|
|
78
|
-
"jest-environment-node": {
|
|
79
|
-
"optional": true
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"dependencies": {
|
|
83
|
-
"chalk": "4.1.2",
|
|
84
|
-
"zod": "^4.3.6"
|
|
85
|
-
},
|
|
86
|
-
"scripts": {
|
|
87
|
-
"prepare:publish": "shx rm -fR dist && yarn build && npm login && npm publish",
|
|
88
|
-
"typecheck": "tsc",
|
|
89
|
-
"build": "tsc --project tsconfig.build.json",
|
|
90
|
-
"dev": "yarn typecheck --watch & yarn build --watch",
|
|
91
|
-
"lint": "eslint .",
|
|
92
|
-
"test:unit": "jest --coverage",
|
|
93
|
-
"test:matrix": "node ./e2e/runMatrix.mjs",
|
|
94
|
-
"test": "yarn matrix && yarn test:unit && yarn coverage:merge",
|
|
95
|
-
"coverage:merge": "nyc report"
|
|
96
|
-
}
|
|
97
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "jest-doctor",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Custom Jest environment that detects async leaks between tests, enforces test isolation, and prevents flaky failures in CI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Stephan Dum",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"homepage": "https://stephan-dum.github.io/jest-doctor/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/stephan-dum/jest-doctor.git",
|
|
12
|
+
"directory": "packages/jest-doctor"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./createEnvMixin": "./dist/createEnvMixin.js",
|
|
17
|
+
"./env/node": "./dist/env/node.js",
|
|
18
|
+
"./env/jsdom": "./dist/env/jsdom.js",
|
|
19
|
+
"./reporter": "./dist/reporter.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"jest",
|
|
26
|
+
"jest-doctor",
|
|
27
|
+
"leak detection",
|
|
28
|
+
"open handles",
|
|
29
|
+
"issue detection",
|
|
30
|
+
"testing",
|
|
31
|
+
"jest environment",
|
|
32
|
+
"test hygiene",
|
|
33
|
+
"flaky-tests",
|
|
34
|
+
"test-isolation",
|
|
35
|
+
"async-leaks",
|
|
36
|
+
"developer-tools",
|
|
37
|
+
"ci",
|
|
38
|
+
"test diagnostics"
|
|
39
|
+
],
|
|
40
|
+
"types": "./dist/types.d.ts",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@dev/eslint": "1.0.0",
|
|
43
|
+
"@dev/tsconfig": "1.0.0",
|
|
44
|
+
"@jest/reporters": "30.2.0",
|
|
45
|
+
"@swc/core": "^1.15.11",
|
|
46
|
+
"@swc/jest": "^0.2.39",
|
|
47
|
+
"@testing-library/dom": "^10.4.1",
|
|
48
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
50
|
+
"@types/cross-spawn": "^6.0.6",
|
|
51
|
+
"@types/jest": "^30.0.0",
|
|
52
|
+
"@types/node": "^25.2.1",
|
|
53
|
+
"@types/react": "^19.2.13",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"c8": "^10.1.3",
|
|
56
|
+
"cross-spawn": "^7.0.6",
|
|
57
|
+
"eslint": "^9.39.2",
|
|
58
|
+
"jest": "30.2.0",
|
|
59
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
60
|
+
"jest-environment-node": "^30.2.0",
|
|
61
|
+
"memfs": "^4.56.10",
|
|
62
|
+
"nyc": "^17.1.0",
|
|
63
|
+
"react": "^19.2.4",
|
|
64
|
+
"react-dom": "^19.2.4",
|
|
65
|
+
"shx": "^0.4.0",
|
|
66
|
+
"ts-jest": "^29.4.6",
|
|
67
|
+
"typescript": "5.9.3"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"jest": ">=28",
|
|
71
|
+
"jest-environment-jsdom": "*",
|
|
72
|
+
"jest-environment-node": "*"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"jest-environment-jsdom": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"jest-environment-node": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"chalk": "4.1.2",
|
|
84
|
+
"zod": "^4.3.6"
|
|
85
|
+
},
|
|
86
|
+
"scripts": {
|
|
87
|
+
"prepare:publish": "shx rm -fR dist && yarn build && npm login && npm publish",
|
|
88
|
+
"typecheck": "tsc",
|
|
89
|
+
"build": "tsc --project tsconfig.build.json",
|
|
90
|
+
"dev": "yarn typecheck --watch & yarn build --watch",
|
|
91
|
+
"lint": "eslint .",
|
|
92
|
+
"test:unit": "jest --coverage",
|
|
93
|
+
"test:matrix": "node ./e2e/runMatrix.mjs",
|
|
94
|
+
"test": "yarn matrix && yarn test:unit && yarn coverage:merge",
|
|
95
|
+
"coverage:merge": "nyc report"
|
|
96
|
+
}
|
|
97
|
+
}
|
package/readme.md
CHANGED
|
@@ -11,9 +11,9 @@ Add one of the environments to your Jest config:
|
|
|
11
11
|
|
|
12
12
|
```js
|
|
13
13
|
export default {
|
|
14
|
-
testEnvironment:
|
|
14
|
+
testEnvironment: 'jest-doctor/env/node',
|
|
15
15
|
// optional
|
|
16
|
-
reporters: [
|
|
16
|
+
reporters: ['default', 'jest-doctor/reporter'],
|
|
17
17
|
};
|
|
18
18
|
```
|
|
19
19
|
|