fix(node-env): do not warn on localStorage unless actually accessed (#16086)

This commit is contained in:
Simen Bekkhus
2026-05-03 09:58:48 +02:00
committed by GitHub
parent a579f4d8d9
commit 8d3edd8ed7
6 changed files with 93 additions and 3 deletions
+16 -1
View File
@@ -129,7 +129,13 @@ Located in `e2e/` with ~190 integration test directories:
```bash
cd e2e/<test-directory>
node ../../packages/jest-cli/bin/jest.js
node ../../packages/jest-cli/bin/jest.js --no-cache
```
Pass Node flags via the environment, e.g.:
```bash
NODE_OPTIONS='--localstorage-file=/tmp/ls.json' node ../../packages/jest-cli/bin/jest.js --no-cache
```
**Note**: Some tests may require Mercurial (`hg`) to be installed. On macOS: `brew install hg`
@@ -147,6 +153,15 @@ yarn lint
- Uses ESLint 9.x with flat config (`eslint.config.mjs`)
- Caches results for faster subsequent runs
- Takes 1-2 minutes
- **Run this only as a final check.** During development, lint only changed files (see below).
**During verification — lint only changed files first**:
```bash
yarn eslint --cache --fix <changed-file-1> <changed-file-2> ...
```
Run `yarn eslint --fix` on each file you edited before running the full `yarn lint`. This catches and auto-fixes issues (import order, formatting) without waiting 1-2 minutes for the full suite.
**Lint specific files/directories**:
+1
View File
@@ -16,6 +16,7 @@
- `[fake-timers]` Convert `Date` to milliseconds before passing to `@sinonjs/fake-timers` ([#16029](https://github.com/jestjs/jest/pull/16029))
- `[jest-circus]` Prevent crash when `asyncError` is undefined for non-Error throws ([#16003](https://github.com/jestjs/jest/pull/16003))
- `[jest-circus, jest-jasmine2]` Include `Error.cause` in JSON `failureMessages` output ([#15949](https://github.com/jestjs/jest/issues/15949))
- `[jest-environment-node]` Fix `--localstorage-file` warning on Node 25+ ([#16086](https://github.com/jestjs/jest/pull/16086))
- `[jest-runtime]` Improve CJS-from-ESM interop: `__esModule`/Babel default unwrap, broader named-export coverage, and shared CJS singleton across importers ([#16050](https://github.com/jestjs/jest/pull/16050))
- `[jest-runtime]` Load `.js` files with ESM syntax but no `"type":"module"` marker as native ESM ([#16050](https://github.com/jestjs/jest/pull/16050))
- `[jest-runtime]` Extend the `.js`-with-ESM-syntax fallback to `require()` on Node v24.9+ - falls back to `require(esm)` when the CJS parser rejects ESM syntax ([#16078](https://github.com/jestjs/jest/pull/16078))
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import tempy from 'tempy';
import {onNodeVersions} from '@jest/test-utils';
import runJest from '../runJest';
onNodeVersions('>=25', () => {
it('localStorage CRUD works inside a Jest test', async () => {
await tempy.file.task(storageFile => {
const result = runJest('local-storage', [], {
nodeOptions: `--localstorage-file=${storageFile}`,
});
expect(result.exitCode).toBe(0);
expect(result.stderr).not.toContain('--localstorage-file');
});
});
});
@@ -0,0 +1,25 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
test('localStorage CRUD', () => {
expect(localStorage).toHaveLength(0);
localStorage.setItem('a', '1');
localStorage.setItem('b', '2');
expect(localStorage.getItem('a')).toBe('1');
expect(localStorage.getItem('b')).toBe('2');
expect(localStorage).toHaveLength(2);
localStorage.removeItem('a');
expect(localStorage.getItem('a')).toBeNull();
expect(localStorage).toHaveLength(1);
localStorage.clear();
expect(localStorage).toHaveLength(0);
});
+5
View File
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
+24 -2
View File
@@ -45,6 +45,12 @@ const denyList = new Set([
type GlobalProperties = Array<keyof typeof globalThis>;
// Storage proxies (Node 25+) emit a warning on any Reflect.get access when
// --localstorage-file is not set. We install them as non-caching passthrough
// getters so they never land in GlobalProxy.propertyToValue and are never
// inspected by deleteProperties at teardown.
const storageGlobals = new Set(['localStorage', 'sessionStorage']);
const nodeGlobals = new Map(
(Object.getOwnPropertyNames(globalThis) as GlobalProperties)
.filter(global => !denyList.has(global as string))
@@ -109,9 +115,25 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
Object.getOwnPropertyNames(global) as GlobalProperties,
);
for (const [nodeGlobalsKey, descriptor] of nodeGlobals) {
protectProperties(globalThis[nodeGlobalsKey]);
if (!storageGlobals.has(nodeGlobalsKey as string)) {
protectProperties(globalThis[nodeGlobalsKey]);
}
if (!contextGlobals.has(nodeGlobalsKey)) {
if (descriptor.configurable) {
if (storageGlobals.has(nodeGlobalsKey as string)) {
Object.defineProperty(global, nodeGlobalsKey, {
configurable: true,
enumerable: descriptor.enumerable,
get: () => globalThis[nodeGlobalsKey],
set(value) {
Object.defineProperty(global, nodeGlobalsKey, {
configurable: true,
enumerable: descriptor.enumerable,
value,
writable: true,
});
},
});
} else if (descriptor.configurable) {
Object.defineProperty(global, nodeGlobalsKey, {
configurable: true,
enumerable: descriptor.enumerable,