jest-doctor 0.0.3 → 0.0.5
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 +1 -0
- package/dist/createEnvMixin.js +13 -5
- package/dist/env/jsdom.d.ts +1 -0
- package/dist/env/node.d.ts +1 -0
- package/dist/patch/console.js +1 -1
- package/dist/patch/fakeTimers.js +2 -2
- package/dist/patch/promiseConcurrency.d.ts +3 -0
- package/dist/patch/promiseConcurrency.js +32 -0
- package/dist/patch/timers.js +2 -2
- package/dist/reporter.js +0 -1
- package/dist/requireEnvironment.d.ts +1 -0
- package/dist/types.d.ts +11 -12
- package/dist/utils/createAsyncHookCleaner.js +8 -3
- package/dist/utils/createAsyncHookDetector.js +9 -4
- package/dist/utils/getStack.d.ts +1 -1
- package/dist/utils/getStack.js +2 -2
- package/dist/utils/normalizeOptions.d.ts +1 -1
- package/dist/utils/normalizeOptions.js +78 -44
- package/dist/utils/reportLeaks.js +14 -2
- package/package.json +5 -4
- package/readme.md +7 -11
- package/dist/patch/promise.d.ts +0 -3
- package/dist/patch/promise.js +0 -34
package/dist/createEnvMixin.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ declare const createEnvMixin: <EnvironmentConstructor extends JestDoctorConstruc
|
|
|
55
55
|
totalDelay: number;
|
|
56
56
|
};
|
|
57
57
|
tearDownError?: Error;
|
|
58
|
+
asyncIdToPromise: Map<number, Promise<unknown>>;
|
|
58
59
|
setup(): Promise<void>;
|
|
59
60
|
handleEvent(event: unknown, state: unknown): Promise<void>;
|
|
60
61
|
handleTestEvent: JestEnvironment["handleTestEvent"];
|
package/dist/createEnvMixin.js
CHANGED
|
@@ -21,7 +21,7 @@ const hook_1 = __importDefault(require("./patch/hook"));
|
|
|
21
21
|
const getAllAfterEach_1 = __importDefault(require("./utils/getAllAfterEach"));
|
|
22
22
|
const normalizeOptions_1 = __importDefault(require("./utils/normalizeOptions"));
|
|
23
23
|
const getReporterTmpDir_1 = __importDefault(require("./utils/getReporterTmpDir"));
|
|
24
|
-
const
|
|
24
|
+
const promiseConcurrency_1 = __importDefault(require("./patch/promiseConcurrency"));
|
|
25
25
|
const createEnvMixin = (Environment) => {
|
|
26
26
|
// @ts-expect-error strange ts rule where constructor arguments should be any[] where again TypeScript complains
|
|
27
27
|
return class Base extends Environment {
|
|
@@ -38,6 +38,7 @@ const createEnvMixin = (Environment) => {
|
|
|
38
38
|
testPath;
|
|
39
39
|
aggregatedReport;
|
|
40
40
|
tearDownError;
|
|
41
|
+
asyncIdToPromise = new Map();
|
|
41
42
|
constructor(config, context) {
|
|
42
43
|
super(config, context);
|
|
43
44
|
this.testPath = context.testPath.replace(/\W/g, '_');
|
|
@@ -55,8 +56,7 @@ const createEnvMixin = (Environment) => {
|
|
|
55
56
|
: '';
|
|
56
57
|
this.options = (0, normalizeOptions_1.default)(config.projectConfig.testEnvironmentOptions);
|
|
57
58
|
(0, initLeakRecord_1.default)(this, consts_1.MAIN_THREAD);
|
|
58
|
-
if (this.options.report.promises
|
|
59
|
-
this.options.report.promises.patch === 'async_hooks') {
|
|
59
|
+
if (this.options.report.promises) {
|
|
60
60
|
this.asyncHookCleaner = (0, createAsyncHookCleaner_1.default)(this);
|
|
61
61
|
this.asyncHookDetector = (0, createAsyncHookDetector_1.default)(this);
|
|
62
62
|
}
|
|
@@ -72,8 +72,8 @@ const createEnvMixin = (Environment) => {
|
|
|
72
72
|
if (report.console) {
|
|
73
73
|
(0, console_1.default)(this, report.console);
|
|
74
74
|
}
|
|
75
|
-
if (report.promises
|
|
76
|
-
(0,
|
|
75
|
+
if (report.promises) {
|
|
76
|
+
(0, promiseConcurrency_1.default)(this);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
// unfortunately @jest/types doesn't export the necessary types,
|
|
@@ -99,6 +99,14 @@ const createEnvMixin = (Environment) => {
|
|
|
99
99
|
(0, hook_1.default)(this, 'beforeAll');
|
|
100
100
|
(0, hook_1.default)(this, 'afterEach');
|
|
101
101
|
(0, hook_1.default)(this, 'afterAll');
|
|
102
|
+
Object.assign(circusEvent.runtimeGlobals, {
|
|
103
|
+
it: this.global.it,
|
|
104
|
+
test: this.global.test,
|
|
105
|
+
beforeEach: this.global.beforeEach,
|
|
106
|
+
beforeAll: this.global.beforeAll,
|
|
107
|
+
afterEach: this.global.afterEach,
|
|
108
|
+
afterAll: this.global.afterAll,
|
|
109
|
+
});
|
|
102
110
|
}
|
|
103
111
|
await super.handleEvent?.(circusEvent, circusState);
|
|
104
112
|
}
|
package/dist/env/jsdom.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ declare const _default: {
|
|
|
33
33
|
totalDelay: number;
|
|
34
34
|
};
|
|
35
35
|
tearDownError?: Error;
|
|
36
|
+
asyncIdToPromise: Map<number, Promise<unknown>>;
|
|
36
37
|
setup(): Promise<void>;
|
|
37
38
|
handleEvent(event: unknown, state: unknown): Promise<void>;
|
|
38
39
|
handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
|
package/dist/env/node.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ declare const _default: {
|
|
|
33
33
|
totalDelay: number;
|
|
34
34
|
};
|
|
35
35
|
tearDownError?: Error;
|
|
36
|
+
asyncIdToPromise: Map<number, Promise<unknown>>;
|
|
36
37
|
setup(): Promise<void>;
|
|
37
38
|
handleEvent(event: unknown, state: unknown): Promise<void>;
|
|
38
39
|
handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
|
package/dist/patch/console.js
CHANGED
|
@@ -24,7 +24,7 @@ const patchConsole = (that, consoleOptions) => {
|
|
|
24
24
|
if (!(0, exports.isIgnored)(message, consoleOptions.ignore)) {
|
|
25
25
|
that.leakRecords.get(that.currentTestName)?.console.push({
|
|
26
26
|
method: consoleMethod,
|
|
27
|
-
stack: (0, getStack_1.default)(env.console[consoleMethod]),
|
|
27
|
+
stack: (0, getStack_1.default)(env.console[consoleMethod], 'Console.' + consoleMethod),
|
|
28
28
|
testName: that.currentTestName,
|
|
29
29
|
message,
|
|
30
30
|
});
|
package/dist/patch/fakeTimers.js
CHANGED
|
@@ -30,7 +30,7 @@ const patchFakeTimers = (that) => {
|
|
|
30
30
|
fakeTimeout?.set(timerId, {
|
|
31
31
|
type: 'fakeTimeout',
|
|
32
32
|
delay: delay || 0,
|
|
33
|
-
stack: (0, getStack_1.default)(that.global.setTimeout),
|
|
33
|
+
stack: (0, getStack_1.default)(that.global.setTimeout, 'setTimeout'),
|
|
34
34
|
testName: that.currentTestName,
|
|
35
35
|
});
|
|
36
36
|
return timerId;
|
|
@@ -40,7 +40,7 @@ const patchFakeTimers = (that) => {
|
|
|
40
40
|
that.leakRecords.get(that.currentTestName)?.fakeTimers.set(intervalId, {
|
|
41
41
|
type: 'fakeInterval',
|
|
42
42
|
delay: delay || 0,
|
|
43
|
-
stack: (0, getStack_1.default)(that.global.setInterval),
|
|
43
|
+
stack: (0, getStack_1.default)(that.global.setInterval, 'setInterval'),
|
|
44
44
|
testName: that.currentTestName,
|
|
45
45
|
});
|
|
46
46
|
return intervalId;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const patchPromiseConcurrency = (that) => {
|
|
4
|
+
const env = that.global;
|
|
5
|
+
const concurrencyFactor = (fn) => (concurrentPromises) => {
|
|
6
|
+
const promises = that.leakRecords.get(that.currentTestName)?.promises;
|
|
7
|
+
return fn(concurrentPromises).finally(() => {
|
|
8
|
+
if (promises) {
|
|
9
|
+
concurrentPromises.forEach((concurrentPromise) => {
|
|
10
|
+
const leak = promises.get(concurrentPromise);
|
|
11
|
+
if (leak) {
|
|
12
|
+
const asyncId = leak?.asyncId;
|
|
13
|
+
that.asyncIdToPromise.delete(asyncId);
|
|
14
|
+
that.promiseOwner.delete(asyncId);
|
|
15
|
+
promises.delete(concurrentPromise);
|
|
16
|
+
// Promise.race and Promise.any will create a new Promise for every entry that needs to be deleted
|
|
17
|
+
promises.forEach((childLeak, key) => {
|
|
18
|
+
if (childLeak.parentAsyncId === asyncId) {
|
|
19
|
+
that.asyncIdToPromise.delete(childLeak.asyncId);
|
|
20
|
+
that.promiseOwner.delete(childLeak.asyncId);
|
|
21
|
+
promises.delete(key);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
env.Promise.race = concurrencyFactor(env.Promise.race.bind(env.Promise));
|
|
30
|
+
env.Promise.any = concurrencyFactor(env.Promise.any.bind(env.Promise));
|
|
31
|
+
};
|
|
32
|
+
exports.default = patchPromiseConcurrency;
|
package/dist/patch/timers.js
CHANGED
|
@@ -22,7 +22,7 @@ const timers = (that) => {
|
|
|
22
22
|
leakRecord?.timers.set(timerId, {
|
|
23
23
|
type: 'timeout',
|
|
24
24
|
delay: delay || 0,
|
|
25
|
-
stack: (0, getStack_1.default)(env.setTimeout),
|
|
25
|
+
stack: (0, getStack_1.default)(env.setTimeout, 'fake setTimeout'),
|
|
26
26
|
testName: owner,
|
|
27
27
|
});
|
|
28
28
|
return timerId;
|
|
@@ -41,7 +41,7 @@ const timers = (that) => {
|
|
|
41
41
|
that.leakRecords.get(owner)?.timers.set(intervalId, {
|
|
42
42
|
type: 'interval',
|
|
43
43
|
delay: delay || 0,
|
|
44
|
-
stack: (0, getStack_1.default)(env.setInterval),
|
|
44
|
+
stack: (0, getStack_1.default)(env.setInterval, 'fake setInterval'),
|
|
45
45
|
testName: that.currentTestName,
|
|
46
46
|
});
|
|
47
47
|
return intervalId;
|
package/dist/reporter.js
CHANGED
|
@@ -34,6 +34,7 @@ declare const requireEnvironment: (envName: string) => {
|
|
|
34
34
|
totalDelay: number;
|
|
35
35
|
};
|
|
36
36
|
tearDownError?: Error;
|
|
37
|
+
asyncIdToPromise: Map<number, Promise<unknown>>;
|
|
37
38
|
setup(): Promise<void>;
|
|
38
39
|
handleEvent(event: unknown, state: unknown): Promise<void>;
|
|
39
40
|
handleTestEvent: import("@jest/environment").JestEnvironment["handleTestEvent"];
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModernFakeTimers } from '@jest/fake-timers';
|
|
2
2
|
import { JestEnvironment } from '@jest/environment';
|
|
3
3
|
import initOriginal from './utils/initOriginal';
|
|
4
|
+
import type { AsyncHook } from 'node:async_hooks';
|
|
4
5
|
export interface TimerRecord {
|
|
5
6
|
type: 'timeout' | 'interval' | 'fakeTimeout' | 'fakeInterval';
|
|
6
7
|
delay: number;
|
|
@@ -10,6 +11,8 @@ export interface TimerRecord {
|
|
|
10
11
|
export interface PromiseRecord {
|
|
11
12
|
stack: string;
|
|
12
13
|
testName: string;
|
|
14
|
+
asyncId: number;
|
|
15
|
+
parentAsyncId: number;
|
|
13
16
|
}
|
|
14
17
|
export interface ConsoleRecord {
|
|
15
18
|
method: keyof Console;
|
|
@@ -18,7 +21,7 @@ export interface ConsoleRecord {
|
|
|
18
21
|
message: string;
|
|
19
22
|
}
|
|
20
23
|
export interface LeakRecord {
|
|
21
|
-
promises: Map<
|
|
24
|
+
promises: Map<Promise<unknown>, PromiseRecord>;
|
|
22
25
|
timers: Map<NodeJS.Timeout, TimerRecord>;
|
|
23
26
|
console: ConsoleRecord[];
|
|
24
27
|
totalDelay: number;
|
|
@@ -39,9 +42,8 @@ export interface FakeTimers extends Omit<ModernFakeTimers, '_fakeTimers'> {
|
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
export type TimerIsolation = 'afterEach' | 'immediate';
|
|
42
|
-
export type OnError =
|
|
45
|
+
export type OnError = false | 'throw' | 'warn';
|
|
43
46
|
export type ThrowOrWarn = 'throw' | 'warn';
|
|
44
|
-
export type Patch = 'async_hooks' | 'promise';
|
|
45
47
|
export interface ConsoleOptions {
|
|
46
48
|
onError: ThrowOrWarn;
|
|
47
49
|
methods: Array<keyof Console>;
|
|
@@ -53,11 +55,9 @@ export interface NormalizedOptions {
|
|
|
53
55
|
console: NormalizedConsoleOptions;
|
|
54
56
|
timers: OnError;
|
|
55
57
|
fakeTimers: OnError;
|
|
56
|
-
promises:
|
|
57
|
-
onError: ThrowOrWarn;
|
|
58
|
-
patch: Patch;
|
|
59
|
-
};
|
|
58
|
+
promises: OnError;
|
|
60
59
|
};
|
|
60
|
+
verbose: boolean;
|
|
61
61
|
delayThreshold: number;
|
|
62
62
|
timerIsolation: TimerIsolation;
|
|
63
63
|
clearTimers: boolean;
|
|
@@ -67,17 +67,14 @@ export type RawConsoleOptions = boolean | {
|
|
|
67
67
|
methods?: Array<keyof Console>;
|
|
68
68
|
ignore?: string | RegExp | Array<string | RegExp>;
|
|
69
69
|
};
|
|
70
|
-
export type RawPromise = boolean | {
|
|
71
|
-
onError?: ThrowOrWarn;
|
|
72
|
-
patch?: Patch;
|
|
73
|
-
};
|
|
74
70
|
export interface RawOptions {
|
|
75
71
|
report?: {
|
|
76
72
|
console?: RawConsoleOptions;
|
|
77
73
|
timers?: OnError;
|
|
78
74
|
fakeTimers?: OnError;
|
|
79
|
-
promises?:
|
|
75
|
+
promises?: OnError;
|
|
80
76
|
};
|
|
77
|
+
verbose?: boolean;
|
|
81
78
|
delayThreshold?: number;
|
|
82
79
|
timerIsolation?: TimerIsolation;
|
|
83
80
|
clearTimers?: boolean;
|
|
@@ -100,5 +97,7 @@ export interface JestDoctorEnvironment {
|
|
|
100
97
|
currentAfterEachCount: number;
|
|
101
98
|
options: NormalizedOptions;
|
|
102
99
|
aggregatedReport: AggregatedReport;
|
|
100
|
+
asyncIdToPromise: Map<number, Promise<unknown>>;
|
|
101
|
+
asyncHookDetector?: AsyncHook;
|
|
103
102
|
}
|
|
104
103
|
export {};
|
|
@@ -5,10 +5,15 @@ const createAsyncHookCleaner = (that) => {
|
|
|
5
5
|
return (0, node_async_hooks_1.createHook)({
|
|
6
6
|
promiseResolve(asyncId) {
|
|
7
7
|
const owner = that.promiseOwner.get(asyncId);
|
|
8
|
-
if (!owner)
|
|
8
|
+
if (!owner) {
|
|
9
9
|
return;
|
|
10
|
-
|
|
11
|
-
that.
|
|
10
|
+
}
|
|
11
|
+
const promise = that.asyncIdToPromise.get(asyncId);
|
|
12
|
+
if (promise) {
|
|
13
|
+
that.leakRecords.get(owner)?.promises.delete(promise);
|
|
14
|
+
that.promiseOwner.delete(asyncId);
|
|
15
|
+
that.asyncIdToPromise.delete(asyncId);
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
18
|
});
|
|
14
19
|
};
|
|
@@ -6,14 +6,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
7
7
|
const getStack_1 = __importDefault(require("./getStack"));
|
|
8
8
|
const createAsyncHookDetector = (that) => {
|
|
9
|
-
const init = (asyncId, type) => {
|
|
10
|
-
if (type !== 'PROMISE')
|
|
9
|
+
const init = (asyncId, type, parentAsyncId, resource) => {
|
|
10
|
+
if (type !== 'PROMISE') {
|
|
11
11
|
return;
|
|
12
|
+
}
|
|
13
|
+
const promise = resource;
|
|
12
14
|
const owner = that.currentTestName;
|
|
13
15
|
that.promiseOwner.set(asyncId, owner);
|
|
14
|
-
that.
|
|
15
|
-
|
|
16
|
+
that.asyncIdToPromise.set(asyncId, promise);
|
|
17
|
+
that.leakRecords.get(owner)?.promises.set(promise, {
|
|
18
|
+
stack: (0, getStack_1.default)(init, 'Promise'),
|
|
16
19
|
testName: owner,
|
|
20
|
+
asyncId,
|
|
21
|
+
parentAsyncId,
|
|
17
22
|
});
|
|
18
23
|
};
|
|
19
24
|
return (0, node_async_hooks_1.createHook)({ init });
|
package/dist/utils/getStack.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const getStack: (stackFrom: Function) => string;
|
|
1
|
+
declare const getStack: (stackFrom: Function, prefix: string) => string;
|
|
2
2
|
export default getStack;
|
package/dist/utils/getStack.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const getStack = (stackFrom) => {
|
|
3
|
+
const getStack = (stackFrom, prefix) => {
|
|
4
4
|
const error = {
|
|
5
5
|
stack: '',
|
|
6
6
|
};
|
|
7
7
|
Error.captureStackTrace(error, stackFrom);
|
|
8
|
-
return error.stack;
|
|
8
|
+
return prefix + ' ' + error.stack.replace(/\\/g, '/');
|
|
9
9
|
};
|
|
10
10
|
exports.default = getStack;
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.normalizeOptions = normalizeOptions;
|
|
37
|
+
const zod = __importStar(require("zod"));
|
|
4
38
|
const DEFAULTS = {
|
|
5
39
|
report: {
|
|
6
40
|
console: {
|
|
@@ -10,56 +44,56 @@ const DEFAULTS = {
|
|
|
10
44
|
},
|
|
11
45
|
timers: 'throw',
|
|
12
46
|
fakeTimers: 'throw',
|
|
13
|
-
promises:
|
|
14
|
-
onError: 'throw',
|
|
15
|
-
patch: 'async_hooks',
|
|
16
|
-
},
|
|
47
|
+
promises: 'throw',
|
|
17
48
|
},
|
|
49
|
+
verbose: false,
|
|
18
50
|
delayThreshold: 0,
|
|
19
51
|
timerIsolation: 'afterEach',
|
|
20
52
|
clearTimers: true,
|
|
21
53
|
};
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
const onError = zod
|
|
55
|
+
.union([zod.literal(false), zod.enum(['throw', 'warn'])])
|
|
56
|
+
.default('throw');
|
|
57
|
+
const schema = zod
|
|
58
|
+
.object({
|
|
59
|
+
report: zod
|
|
60
|
+
.object({
|
|
61
|
+
console: zod
|
|
62
|
+
.union([
|
|
63
|
+
zod.literal(false),
|
|
64
|
+
zod.object({
|
|
65
|
+
onError: zod.enum(['throw', 'warn']).default('throw'),
|
|
66
|
+
methods: zod
|
|
67
|
+
.array(zod.enum(['log', 'warn', 'error', 'info', 'debug']))
|
|
68
|
+
.default(DEFAULTS.report.console.methods),
|
|
69
|
+
ignore: zod
|
|
70
|
+
.union([
|
|
71
|
+
zod.string(),
|
|
72
|
+
zod.instanceof(RegExp),
|
|
73
|
+
zod.array(zod.union([zod.string(), zod.instanceof(RegExp)])),
|
|
74
|
+
])
|
|
75
|
+
.transform((value) => (Array.isArray(value) ? value : [value]))
|
|
76
|
+
.default([]),
|
|
77
|
+
}),
|
|
78
|
+
])
|
|
79
|
+
.default(DEFAULTS.report.console),
|
|
80
|
+
timers: onError,
|
|
81
|
+
fakeTimers: onError,
|
|
82
|
+
promises: onError,
|
|
83
|
+
})
|
|
84
|
+
.default(DEFAULTS.report),
|
|
85
|
+
verbose: zod.boolean().default(false),
|
|
86
|
+
delayThreshold: zod.int().gte(0).default(0),
|
|
87
|
+
timerIsolation: zod.enum(['afterEach', 'immediate']).default('afterEach'),
|
|
88
|
+
clearTimers: zod.boolean().default(true),
|
|
89
|
+
})
|
|
90
|
+
.default(DEFAULTS);
|
|
91
|
+
function normalizeOptions(raw) {
|
|
92
|
+
try {
|
|
93
|
+
return schema.parse(raw || {});
|
|
42
94
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
onError: rawPromise.onError ?? DEFAULTS.report.promises.onError,
|
|
46
|
-
patch: rawPromise.patch ?? DEFAULTS.report.promises.patch,
|
|
47
|
-
};
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw zod.prettifyError(error);
|
|
48
97
|
}
|
|
49
|
-
return false;
|
|
50
|
-
};
|
|
51
|
-
function normalizeOptions(raw) {
|
|
52
|
-
const report = raw.report ?? {};
|
|
53
|
-
return {
|
|
54
|
-
report: {
|
|
55
|
-
console: normalizeConsole(report.console),
|
|
56
|
-
timers: report.timers ?? DEFAULTS.report.timers,
|
|
57
|
-
fakeTimers: report.fakeTimers ?? DEFAULTS.report.fakeTimers,
|
|
58
|
-
promises: normalizePromise(report.promises),
|
|
59
|
-
},
|
|
60
|
-
delayThreshold: raw.delayThreshold ?? DEFAULTS.delayThreshold,
|
|
61
|
-
timerIsolation: raw.timerIsolation ?? DEFAULTS.timerIsolation,
|
|
62
|
-
clearTimers: raw.clearTimers ?? DEFAULTS.clearTimers,
|
|
63
|
-
};
|
|
64
98
|
}
|
|
65
99
|
exports.default = normalizeOptions;
|
|
@@ -13,8 +13,7 @@ const reportLeaks = (that, leakRecord) => {
|
|
|
13
13
|
leakRecord[property].values().next().value?.stack,
|
|
14
14
|
].join('\n');
|
|
15
15
|
const option = that.options.report[property];
|
|
16
|
-
if (option === 'throw'
|
|
17
|
-
(typeof option === 'object' && option.onError === 'throw')) {
|
|
16
|
+
if (option === 'throw') {
|
|
18
17
|
const error = new Error();
|
|
19
18
|
error.stack = message;
|
|
20
19
|
throw error;
|
|
@@ -33,6 +32,19 @@ const reportLeaks = (that, leakRecord) => {
|
|
|
33
32
|
that.aggregatedReport.fakeTimers += leakRecord.fakeTimers.size;
|
|
34
33
|
that.aggregatedReport.totalDelay += leakRecord.totalDelay;
|
|
35
34
|
}
|
|
35
|
+
if (that.options.verbose) {
|
|
36
|
+
const messages = [];
|
|
37
|
+
const addLeak = (leak) => {
|
|
38
|
+
messages.push(leak.stack);
|
|
39
|
+
};
|
|
40
|
+
leakRecord.promises.forEach(addLeak);
|
|
41
|
+
leakRecord.timers.forEach(addLeak);
|
|
42
|
+
leakRecord.fakeTimers.forEach(addLeak);
|
|
43
|
+
leakRecord.console.forEach(addLeak);
|
|
44
|
+
if (messages.length) {
|
|
45
|
+
node_console_1.default.log(messages.join('\n'));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
try {
|
|
37
49
|
if (leakRecord.console.length) {
|
|
38
50
|
const message = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jest-doctor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "jest environment for leak detection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Stephan Dum",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"@types/react": "^19.2.8",
|
|
39
39
|
"@types/react-dom": "^19.2.3",
|
|
40
40
|
"c8": "^10.1.3",
|
|
41
|
-
"cross-env": "^10.1.0",
|
|
42
41
|
"cross-spawn": "^7.0.6",
|
|
43
42
|
"eslint": "^9.39.2",
|
|
44
|
-
"istanbul-lib-report": "^3.0.1",
|
|
45
43
|
"jest": "30.2.0",
|
|
44
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
45
|
+
"jest-environment-node": "^30.2.0",
|
|
46
46
|
"memfs": "^4.52.0",
|
|
47
47
|
"nyc": "^17.1.0",
|
|
48
48
|
"react": "^19.2.3",
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"chalk": "4.1.2"
|
|
66
|
+
"chalk": "4.1.2",
|
|
67
|
+
"zod": "^4.3.5"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
70
|
"typecheck": "tsc",
|
package/readme.md
CHANGED
|
@@ -40,12 +40,8 @@ The environment can be configured through jest config `testEnvironmentOptions`:
|
|
|
40
40
|
- **report**: an object defining which leaks should be tracked and reported
|
|
41
41
|
- **timers**: `false | 'warn' | 'trow'` (default `throw`) whether normal setTimeout and setInterval should be reported and how
|
|
42
42
|
- **fakeTimers**: `false | 'warn' | 'trow'` (default `throw`) same as timers but for fake api
|
|
43
|
-
- **promises**: `false`
|
|
44
|
-
|
|
45
|
-
- **patch**: `'async_hooks' | 'promise'` (default `async_hooks`) controls how to patch promises
|
|
46
|
-
- **async_hooks**: uses node async_hook API to detect any promise generation, robust solution but with an overhead.
|
|
47
|
-
- **promise**: overwrites the global Promise object which is faster but will not detect: internal promises, async await, native API promises!
|
|
48
|
-
- **console**: `false` or object (default object)
|
|
43
|
+
- **promises**: `false | 'warn' | 'trow'` (default `throw`) indicating if promises should be reported and how
|
|
44
|
+
- **console**: `false` or object (default object)
|
|
49
45
|
- **onError**: `'warn' | 'trow'` (default `throw`) how to handle reporting
|
|
50
46
|
- **methods**: `keyof Console` (default all methods) which console methods should be tracked
|
|
51
47
|
- **ignore**: `string | regexp | Array<string | regexp>` (default: []) allows to excluded console output from tracking
|
|
@@ -67,9 +63,7 @@ export default {
|
|
|
67
63
|
},
|
|
68
64
|
timers: 'warn',
|
|
69
65
|
fakeTimers: 'warn',
|
|
70
|
-
promises:
|
|
71
|
-
onErro: 'warn',
|
|
72
|
-
},
|
|
66
|
+
promises: 'warn',
|
|
73
67
|
},
|
|
74
68
|
delayThreshold: 1000,
|
|
75
69
|
timerIsolation: 'afterEach',
|
|
@@ -100,9 +94,11 @@ Promise.resolve().then(() => {
|
|
|
100
94
|
/* i am not tracked as unresolved */
|
|
101
95
|
});
|
|
102
96
|
```
|
|
103
|
-
-
|
|
97
|
+
- Promise.race and Promise.any can only accept unchained promises, or they will report the chained promise as open.
|
|
104
98
|
```js
|
|
105
|
-
|
|
99
|
+
const p1 = Promise.resolve().then(() => { /* not allowed */});
|
|
100
|
+
const p2 = Promise.resolve();
|
|
101
|
+
await Promise.race([p1, p2]);
|
|
106
102
|
```
|
|
107
103
|
|
|
108
104
|
- console, setTimeout / setInterval can also be imported and will not participate in leak detection in these cases, but this can also serve as exit hatch if needed.
|
package/dist/patch/promise.d.ts
DELETED
package/dist/patch/promise.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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 patchPromise = (that) => {
|
|
8
|
-
const OriginalPromise = that.global.Promise;
|
|
9
|
-
class TrackedPromise extends OriginalPromise {
|
|
10
|
-
constructor(executor) {
|
|
11
|
-
let resolveRef;
|
|
12
|
-
let rejectRef;
|
|
13
|
-
super((resolve, reject) => {
|
|
14
|
-
resolveRef = resolve;
|
|
15
|
-
rejectRef = reject;
|
|
16
|
-
});
|
|
17
|
-
const promiseLeaks = that.leakRecords.get(that.currentTestName)?.promises;
|
|
18
|
-
promiseLeaks?.set(this, {
|
|
19
|
-
testName: that.currentTestName,
|
|
20
|
-
stack: (0, getStack_1.default)(that.global.Promise),
|
|
21
|
-
});
|
|
22
|
-
executor((value) => {
|
|
23
|
-
promiseLeaks?.delete(this);
|
|
24
|
-
resolveRef(value);
|
|
25
|
-
}, (reason) => {
|
|
26
|
-
promiseLeaks?.delete(this);
|
|
27
|
-
rejectRef(reason);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
Object.assign(Promise, OriginalPromise);
|
|
32
|
-
that.global.Promise = TrackedPromise;
|
|
33
|
-
};
|
|
34
|
-
exports.default = patchPromise;
|