jest-doctor 2.0.7 → 2.0.9

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.
@@ -43,10 +43,6 @@ const createEnvMixin = (Environment) => {
43
43
  testPath;
44
44
  aggregatedReport;
45
45
  tearDownError;
46
- promiseToAsyncId = new Map();
47
- asyncRoot = 0;
48
- asyncIdToParentId = new Map();
49
- asyncIdToPromise = new Map();
50
46
  asyncStorage = new node_async_hooks_1.AsyncLocalStorage();
51
47
  testTimeout;
52
48
  constructor(config, context) {
@@ -103,11 +99,16 @@ const createEnvMixin = (Environment) => {
103
99
  async handleEvent(event, state) {
104
100
  const circusEvent = event;
105
101
  const circusState = state;
106
- if (circusEvent.name === 'test_fn_start') {
102
+ if (circusEvent.name === 'test_start') {
107
103
  if (this.options.timerIsolation === 'afterEach') {
108
104
  this.currentAfterEachCount = (0, getAllAfterEach_1.default)(circusEvent.test.parent);
109
105
  }
110
106
  }
107
+ else if (circusEvent.name === 'test_fn_start') {
108
+ if (this.options.timerIsolation === 'beforeEach') {
109
+ this.currentAfterEachCount = (0, getAllAfterEach_1.default)(circusEvent.test.parent);
110
+ }
111
+ }
111
112
  else if (circusEvent.name === 'teardown') {
112
113
  // the detector needs to be disabled here to avoid the teardown promise polluting the report
113
114
  this.asyncHookDetector?.();
@@ -22,6 +22,9 @@ const patchDOMListeners = (that, listenerOptions) => {
22
22
  recordCapture === capture);
23
23
  });
24
24
  if (index !== -1) {
25
+ if (options && options.signal) {
26
+ options.signal.removeEventListener('abort', leak.domListeners[index].abort);
27
+ }
25
28
  leak.domListeners.splice(index, 1);
26
29
  }
27
30
  }
@@ -31,20 +34,29 @@ const patchDOMListeners = (that, listenerOptions) => {
31
34
  if (leak) {
32
35
  const stack = (0, getStack_1.default)(window.addEventListener);
33
36
  if (!(0, isIgnored_1.default)(stack, listenerOptions.ignoreStack)) {
37
+ const abort = () => {
38
+ removeEventLeak(event, listener, options);
39
+ };
34
40
  leak.domListeners.push({
35
41
  event: event,
36
42
  listener,
37
43
  stack,
38
44
  options,
45
+ abort,
39
46
  });
47
+ if (typeof options === 'object') {
48
+ if (options.signal) {
49
+ options.signal.addEventListener('abort', abort);
50
+ }
51
+ if (options.once) {
52
+ return originalWindowAddEventListener(event, function (...args) {
53
+ removeEventLeak(event, listener, options);
54
+ listener.call(this, ...args);
55
+ }, options);
56
+ }
57
+ }
40
58
  }
41
59
  }
42
- if (typeof options === 'object' && options.once) {
43
- return originalWindowAddEventListener(event, function (...args) {
44
- removeEventLeak(event, listener, options);
45
- listener.call(this, ...args);
46
- }, options);
47
- }
48
60
  return originalWindowAddEventListener(event, listener, options);
49
61
  };
50
62
  const originalWindowRemoveEventListner = window.removeEventListener.bind(window);
@@ -23,7 +23,7 @@ const patchTimers = (that) => {
23
23
  callback();
24
24
  });
25
25
  leakRecord?.timers.set(timerId, {
26
- type: 'setImmediate',
26
+ type: 'immediate',
27
27
  stack,
28
28
  isAllowed,
29
29
  });
package/dist/types.d.ts CHANGED
@@ -37,6 +37,7 @@ interface DOMListenerRecord {
37
37
  capture?: boolean;
38
38
  } | false | undefined;
39
39
  stack: string;
40
+ abort: () => void;
40
41
  }
41
42
  export interface LeakRecord {
42
43
  promises: Map<Promise<unknown>, PromiseRecord>;
@@ -47,7 +48,7 @@ export interface LeakRecord {
47
48
  fakeTimers: Map<number | NodeJS.Timeout | NodeJS.Immediate, TimerRecord>;
48
49
  domListeners: DOMListenerRecord[];
49
50
  }
50
- export type TimerIsolation = 'afterEach' | 'immediate';
51
+ export type TimerIsolation = 'afterEach' | 'immediate' | 'beforeEach';
51
52
  export type ThrowOrWarn = 'throw' | 'warn';
52
53
  export type IsIgnored = Array<string | RegExp>;
53
54
  type RawIgnore = string | RegExp | Array<string | RegExp>;
@@ -129,7 +130,6 @@ export interface JestDoctorEnvironment extends JestEnvironment {
129
130
  currentAfterEachCount: number;
130
131
  options: NormalizedOptions;
131
132
  aggregatedReport: AggregatedReport;
132
- asyncRoot: number;
133
133
  asyncStorage: AsyncLocalStorage<string>;
134
134
  testTimeout: number;
135
135
  }
@@ -22,28 +22,25 @@ const analyzeCallback = async (that, callback, testContext, timeout, isHook, tra
22
22
  const leakRecord = (0, initLeakRecord_1.default)(that, testName);
23
23
  let isRejected = false;
24
24
  let timerId;
25
- return that.asyncStorage.run('ignored', () => {
26
- return new Promise((resolve, reject) => {
27
- timerId = setTimeout(() => {
28
- isRejected = true;
29
- reject(getTimeoutError(timeout, isHook, trace));
30
- }, timeout);
31
- void Promise.resolve(that.asyncStorage.run(testName, () => callback.call(testContext)))
32
- .then((returnValue) => {
33
- if (!isRejected) {
34
- (0, reportLeaks_1.default)(that, leakRecord);
35
- resolve(returnValue);
36
- }
37
- }, (reason) => {
38
- isRejected = true;
39
- reject(reason);
40
- })
41
- .catch(reject);
42
- }).finally(() => {
43
- clearTimeout(timerId);
44
- that.asyncRoot = 0;
45
- (0, cleanupAfterTest_1.default)(that, leakRecord, testName, isRejected);
46
- });
25
+ return new Promise((resolve, reject) => {
26
+ timerId = setTimeout(() => {
27
+ isRejected = true;
28
+ reject(getTimeoutError(timeout, isHook, trace));
29
+ }, timeout);
30
+ void Promise.resolve(that.asyncStorage.run(testName, () => callback.call(testContext)))
31
+ .then((returnValue) => {
32
+ if (!isRejected) {
33
+ (0, reportLeaks_1.default)(that, leakRecord);
34
+ resolve(returnValue);
35
+ }
36
+ }, (reason) => {
37
+ isRejected = true;
38
+ reject(reason);
39
+ })
40
+ .catch(reject);
41
+ }).finally(() => {
42
+ clearTimeout(timerId);
43
+ (0, cleanupAfterTest_1.default)(that, leakRecord, testName, isRejected);
47
44
  });
48
45
  };
49
46
  exports.default = analyzeCallback;
@@ -15,9 +15,12 @@ const cleanupAfterTest = (that, leakRecord, testName, ignoreAfterEach = false) =
15
15
  if (record.type === 'timeout') {
16
16
  that.original.timer.clearTimeout(timerId);
17
17
  }
18
- else {
18
+ else if (record.type === 'interval') {
19
19
  that.original.timer.clearInterval(timerId);
20
20
  }
21
+ else {
22
+ that.original.timer.clearImmediate(timerId);
23
+ }
21
24
  }
22
25
  }
23
26
  that.leakRecords.delete(testName);
@@ -105,7 +105,9 @@ const schema = zod
105
105
  .default(DEFAULTS.report),
106
106
  verbose: zod.boolean().default(false),
107
107
  delayThreshold: zod.int().gte(0).default(0),
108
- timerIsolation: zod.enum(['afterEach', 'immediate']).default('afterEach'),
108
+ timerIsolation: zod
109
+ .enum(['afterEach', 'immediate', 'beforeEach'])
110
+ .default('afterEach'),
109
111
  clearTimers: zod.boolean().default(true),
110
112
  })
111
113
  .default(DEFAULTS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-doctor",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Custom Jest environment that detects async leaks between tests, enforces test isolation, and prevents flaky failures in CI.",
5
5
  "license": "MIT",
6
6
  "author": "Stephan Dum",
@@ -49,12 +49,12 @@
49
49
  "@testing-library/react": "^16.3.2",
50
50
  "@types/cross-spawn": "^6.0.6",
51
51
  "@types/jest": "^30.0.0",
52
- "@types/node": "^25.2.3",
52
+ "@types/node": "^25.3.0",
53
53
  "@types/react": "^19.2.14",
54
54
  "@types/react-dom": "^19.2.3",
55
55
  "c8": "^10.1.3",
56
56
  "cross-spawn": "^7.0.6",
57
- "eslint": "^9.39.2",
57
+ "eslint": "^10.0.0",
58
58
  "jest": "30.2.0",
59
59
  "jest-environment-jsdom": "^30.2.0",
60
60
  "jest-environment-node": "^30.2.0",