Skip to content

Commit 3a0afa0

Browse files
committed
#15638 Change default behavior of 'node' test environment to 'soft' delete globals at teardown
This commit adds three configurable options to the globals cleanup at teardown: - 'off': no cleanup whatsoever - 'soft': "deleted" properties will emit a deprecation warning if accessed - 'hard': actually delete the properties The default behavior is 'soft'.
1 parent f878de6 commit 3a0afa0

File tree

14 files changed

+162
-35
lines changed

14 files changed

+162
-35
lines changed

e2e/console-debugging/jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ require('./stdout-spy');
1010

1111
module.exports = {
1212
testEnvironment: 'node',
13+
testEnvironmentOptions: {
14+
globalsCleanupMode: 'hard',
15+
},
1316
verbose: true,
1417
};

e2e/esm-config/cjs/jest.config.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
module.exports = {
99
displayName: 'Config from cjs file',
1010
testEnvironment: 'node',
11+
testEnvironmentOptions: {
12+
globalsCleanupMode: 'hard',
13+
},
1114
};

e2e/esm-config/js/jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ const displayName = await Promise.resolve('Config from js file');
1010
export default {
1111
displayName,
1212
testEnvironment: 'node',
13+
testEnvironmentOptions: {
14+
globalsCleanupMode: 'hard',
15+
},
1316
};

e2e/esm-config/mjs/jest.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ const displayName = await Promise.resolve('Config from mjs file');
1010
export default {
1111
displayName,
1212
testEnvironment: 'node',
13+
testEnvironmentOptions: {
14+
globalsCleanupMode: 'hard',
15+
},
1316
};

e2e/esm-config/ts/jest.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@
1010
type DummyConfig = {
1111
displayName: string;
1212
testEnvironment: string;
13+
testEnvironmentOptions?: {
14+
globalsCleanupMode: 'hard' | 'soft' | 'off';
15+
};
1316
};
1417

1518
const config: DummyConfig = {
1619
displayName: 'Config from ts file',
1720
testEnvironment: 'node',
21+
testEnvironmentOptions: {
22+
globalsCleanupMode: 'hard',
23+
},
1824
};
1925

2026
export default () => config;

e2e/typescript-config/modern-module-resolution/jest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@
88
const config = {
99
displayName: 'Config from modern ts file',
1010
testEnvironment: 'node',
11+
testEnvironmentOptions: {
12+
globalsCleanupMode: 'hard',
13+
},
1114
};
1215
export default config;

examples/react-native/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const {resolve} = require('path');
33
module.exports = {
44
preset: 'react-native',
55
testEnvironmentOptions: {
6-
disableGlobalsCleanup: 'true',
6+
globalsCleanupMode: 'soft',
77
},
88
// this is specific to the Jest repo, not generally needed (the files we ignore will be in node_modules which is ignored by default)
99
transformIgnorePatterns: [resolve(__dirname, '../../packages')],

jest.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export default {
3737
printBasicPrototype: true,
3838
},
3939
snapshotSerializers: [require.resolve('jest-serializer-ansi-escapes')],
40+
testEnvironmentOptions: {
41+
globalsCleanupMode: 'hard',
42+
},
4043
testPathIgnorePatterns: [
4144
'/__arbitraries__/',
4245
'/__benchmarks__/',

packages/jest-environment-node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"@jest/types": "workspace:*",
2525
"@types/node": "*",
2626
"jest-mock": "workspace:*",
27-
"jest-util": "workspace:*"
27+
"jest-util": "workspace:*",
28+
"jest-validate": "workspace:*"
2829
},
2930
"devDependencies": {
3031
"@jest/test-utils": "workspace:*",

packages/jest-environment-node/src/index.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
1515
import type {Global} from '@jest/types';
1616
import {ModuleMocker} from 'jest-mock';
1717
import {
18+
type DeletionMode,
1819
canDeleteProperties,
1920
deleteProperties,
2021
installCommonGlobals,
2122
protectProperties,
2223
} from 'jest-util';
24+
import {logValidationWarning} from 'jest-validate';
2325

2426
type Timer = {
2527
id: number;
@@ -86,7 +88,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
8688
customExportConditions = ['node', 'node-addons'];
8789
private readonly _configuredExportConditions?: Array<string>;
8890
private _globalProxy: GlobalProxy;
89-
private _clearGlobalsAtShutdown: boolean;
91+
private _globalsCleanupMode: 'off' | DeletionMode;
9092

9193
// while `context` is unused, it should always be passed
9294
constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) {
@@ -204,9 +206,27 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
204206
});
205207

206208
this._globalProxy.envSetupCompleted();
207-
this._clearGlobalsAtShutdown = ![true, 'true'].includes(
208-
projectConfig.testEnvironmentOptions['disableGlobalsCleanup'] as any,
209-
);
209+
this._globalsCleanupMode = (() => {
210+
const rawConfig =
211+
projectConfig.testEnvironmentOptions['globalsCleanupMode'];
212+
const config = rawConfig?.toString()?.toLowerCase();
213+
switch (config) {
214+
case 'hard':
215+
case 'soft':
216+
case 'off':
217+
return config;
218+
default: {
219+
if (config !== undefined) {
220+
logValidationWarning(
221+
'testEnvironmentOptions.globalsCleanupMode',
222+
`Unknown value given: ${rawConfig}`,
223+
'Available options are: [hard, soft, off]',
224+
);
225+
}
226+
return 'soft';
227+
}
228+
}
229+
})();
210230
}
211231

212232
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -222,8 +242,8 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
222242
this.context = null;
223243
this.fakeTimers = null;
224244
this.fakeTimersModern = null;
225-
if (this._clearGlobalsAtShutdown) {
226-
this._globalProxy.clear();
245+
if (this._globalsCleanupMode !== 'off') {
246+
this._globalProxy.clear(this._globalsCleanupMode);
227247
}
228248
}
229249

@@ -274,16 +294,18 @@ class GlobalProxy implements ProxyHandler<typeof globalThis> {
274294
* Deletes any property that was set on the global object, except for:
275295
* 1. Properties that were set before {@link #envSetupCompleted} was invoked.
276296
* 2. Properties protected by {@link #protectProperties}.
297+
*
298+
* @param mode determines whether to soft or hard delete the properties.
277299
*/
278-
clear(): void {
300+
clear(mode: DeletionMode): void {
279301
for (const {value} of [
280302
...[...this.propertyToValue.entries()].map(([property, value]) => ({
281303
property,
282304
value,
283305
})),
284306
...this.leftovers,
285307
]) {
286-
deleteProperties(value);
308+
deleteProperties(value, mode);
287309
}
288310
this.propertyToValue.clear();
289311
this.leftovers = [];

0 commit comments

Comments
 (0)