├── .cspell.json ├── .editorconfig ├── .github └── funding.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── main │ └── SingleContextNodeEnvironment.ts └── test │ ├── SingleContextNodeEnvironment.test.ts │ └── fakeTimers.test.ts └── tsconfig.json /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", 4 | "files": [ 5 | "**/*" 6 | ], 7 | "enabledLanguageIds": [ 8 | "markdown", 9 | "typescript" 10 | ], 11 | "ignorePaths": [ 12 | "node_modules", 13 | "package-lock.json", 14 | "git", 15 | "lib", 16 | "*.log", 17 | "*.tgz" 18 | ], 19 | "words": [ 20 | "instanceof", 21 | "Reimer" 22 | ], 23 | "language": "en" 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: kayahr 2 | custom: https://paypal.me/kayaahr/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | *.tgz 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.exclude": { 4 | "lib": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Compile and watch project", 6 | "type": "typescript", 7 | "tsconfig": "tsconfig.json", 8 | "option": "watch", 9 | "problemMatcher": [ 10 | "$tsc-watch" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "runOptions": { 17 | "runOn": "folderOpen" 18 | } 19 | 20 | }, 21 | { 22 | "label": "Run tests", 23 | "type": "npm", 24 | "script": "test", 25 | "problemMatcher": [], 26 | "group": { 27 | "kind": "test", 28 | "isDefault": true 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | =========== 3 | 4 | Copyright (C) 2020 Klaus Reimer, k@ailis.de 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including without limitation the 9 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Single-context Node.js environment for Jest 2 | =========================================== 3 | 4 | | :warning: This project is no longer maintained because the author switched from Jest to [Vitest](https://vitest.dev/) :warning: | 5 | | ------------------------------------------------------------------------------------------------------------------------------- | 6 | 7 | One of Jest's key features is context isolation so tests can't have side-effects on other tests by manipulating the global context. In theory that's a good idea but in practice the current implementation messes around with global types in a way which breaks pretty much all instanceof checks in tests against standard types like Uint8Array for example. 8 | 9 | See [Jest issue #2549](https://github.com/facebook/jest/issues/2549) for details. 10 | 11 | This small project provides a single-context Node.js environment which effectively sacrifices the context isolation feature by using a single context for all tests so instanceof checks works again as expected. 12 | 13 | Alternatively you may want to try [jest-light-runner] which tackles the problem right at the base with a new test runner implementation instead of hacking the standard test runner of Jest. 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | * Install dependency: 20 | 21 | ``` 22 | npm install -D jest-environment-node-single-context 23 | ``` 24 | 25 | * Add this property to your Jest config: 26 | 27 | ``` 28 | testEnvironment: "jest-environment-node-single-context" 29 | ``` 30 | 31 | [jest-light-runner]: https://www.npmjs.com/package/jest-light-runner 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/package", 3 | "name": "jest-environment-node-single-context", 4 | "version": "29.4.0", 5 | "description": "Jest environment for Node with single context", 6 | "keywords": [ 7 | "jest", 8 | "environment" 9 | ], 10 | "license": "MIT", 11 | "main": "lib/main/SingleContextNodeEnvironment.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/kayahr/jest-environment-node-single-context" 15 | }, 16 | "scripts": { 17 | "prepare": "rimraf lib && tsc", 18 | "test": "cspell && jest" 19 | }, 20 | "files": [ 21 | "src/main/", 22 | "lib/main/" 23 | ], 24 | "funding": "https://github.com/kayahr/jest-environment-node-single-context?sponsor=1", 25 | "jest": { 26 | "testEnvironment": ".", 27 | "testMatch": [ 28 | "/lib/test/**/*.test.js" 29 | ], 30 | "transformIgnorePatterns": [ 31 | "" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "29.5.12", 36 | "cspell": "8.11.0", 37 | "jest": "29.7.0", 38 | "rimraf": "6.0.1", 39 | "typescript": "5.5.3" 40 | }, 41 | "dependencies": { 42 | "jest-environment-node": "^29.7.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/SingleContextNodeEnvironment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Klaus Reimer 3 | * See LICENSE.md for licensing information. 4 | */ 5 | 6 | import { createContext, Context, Script } from "vm"; 7 | import NodeEnvironment from "jest-environment-node"; 8 | import type { Global } from "@jest/types"; 9 | import type { JestEnvironmentConfig, EnvironmentContext } from "@jest/environment"; 10 | import { LegacyFakeTimers, ModernFakeTimers } from "@jest/fake-timers"; 11 | import { ModuleMocker } from "jest-mock"; 12 | 13 | /** Special context which is handled specially in the hacked runInContext method below */ 14 | const singleContexts = new WeakSet(); 15 | 16 | /** Remembered original runInContext method. */ 17 | const origRunInContext = Script.prototype.runInContext; 18 | 19 | /** 20 | * Ugly hack to allow Jest to just use a single Node VM context. The Jest code in question is in a large private 21 | * method of the standard Jest runtime and it would be a lot of code-copying to create a custom runtime which 22 | * replaces the script run code. So we hack into the `script.runInContext` method instead to redirect it to 23 | * `script.runInThisContext` for vm contexts recorded in `singleContexts` set. 24 | */ 25 | Script.prototype.runInContext = function(context, options) { 26 | if (singleContexts.has(context)) { 27 | return this.runInThisContext(options); 28 | } else { 29 | return origRunInContext.call(this, context, options); 30 | } 31 | } 32 | 33 | // Copy from jest-environment-node 34 | type Timer = { 35 | id: number; 36 | ref: () => Timer; 37 | unref: () => Timer; 38 | }; 39 | const timerIdToRef = (id: number) => ({ 40 | id, 41 | ref() { 42 | return this; 43 | }, 44 | unref() { 45 | return this; 46 | }, 47 | }); 48 | const timerRefToId = (timer: Timer): number | undefined => timer?.id; 49 | 50 | class SingleContextNodeEnvironment extends NodeEnvironment { 51 | constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { 52 | super(config, context); 53 | 54 | // Use shared global environment for all tests 55 | this.global = global as unknown as Global.Global; 56 | 57 | // Recreate context using the shared global environment. This fixes the environment for ESM module stuff of Jest (which doesn't just use the context 58 | // for `script.runInContext`) but a few type mismatches are still there unfortunately. For CJS mode this is irrelevant because calls for 59 | // `script.runInContext` using this context are always redirected to `script.runInThisContext`. All this would not be necessary if we could just get 60 | // the CURRENT vm context. But Node API doesn't allow that. 61 | this.context = createContext(global); 62 | 63 | if (this.context != null) { 64 | // Record the VM context of this environment so the hacked `script.runInContext` redirects the call to `script.runInThisContext` for this context. 65 | singleContexts.add(this.context); 66 | } 67 | 68 | // Install fake timers again, this time with shared global environment 69 | this.fakeTimers = new LegacyFakeTimers({ 70 | config: config.projectConfig, 71 | global, 72 | moduleMocker: this.moduleMocker as ModuleMocker, 73 | timerConfig: { 74 | idToRef: timerIdToRef, 75 | refToId: timerRefToId 76 | } 77 | }); 78 | this.fakeTimersModern = new ModernFakeTimers({ 79 | config: config.projectConfig, 80 | global 81 | }); 82 | } 83 | } 84 | 85 | export = SingleContextNodeEnvironment; 86 | -------------------------------------------------------------------------------- /src/test/SingleContextNodeEnvironment.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Klaus Reimer 3 | * See LICENSE.md for licensing information. 4 | */ 5 | 6 | import fs from "fs"; 7 | import util from "util"; 8 | import v8 from "v8"; 9 | 10 | const readFile = util.promisify(fs.readFile); 11 | 12 | function getSuperClass(cls: Function): Function { 13 | const prototype = Object.getPrototypeOf(cls.prototype); 14 | return prototype ? prototype.constructor : null; 15 | } 16 | 17 | describe("instanceof", () => { 18 | const buffers = fs.readdirSync(__dirname); 19 | const buffer = fs.readFileSync(__filename); 20 | const error = (() => { 21 | try { 22 | fs.readFileSync("/"); 23 | return null; 24 | } catch (e) { 25 | return e; 26 | } 27 | })() as Error; 28 | const promise = readFile(__filename); 29 | 30 | const nodeArrayType = buffers.constructor; 31 | const nodeErrorType = error.constructor; 32 | const nodePromiseType = promise.constructor; 33 | const nodeUint8ArrayType = getSuperClass(buffer.constructor); 34 | const nodeTypedArrayType = getSuperClass(nodeUint8ArrayType); 35 | const nodeObjectType = getSuperClass(nodeTypedArrayType); 36 | 37 | const globalTypedArrayType = getSuperClass(Uint8Array); 38 | 39 | it("works with node array type", () => { 40 | expect(buffers instanceof Array).toBe(true); 41 | expect(buffers instanceof Object).toBe(true); 42 | expect([] instanceof nodeArrayType).toBe(true); 43 | expect([] instanceof nodeObjectType).toBe(true); 44 | }); 45 | 46 | it("works with node error type", () => { 47 | expect(error instanceof Error).toBe(true); 48 | expect(error instanceof Object).toBe(true); 49 | expect(new Error() instanceof nodeErrorType).toBe(true); 50 | expect(new Error() instanceof nodeObjectType).toBe(true); 51 | }); 52 | 53 | it("works with node promise type", () => { 54 | expect(promise instanceof Promise).toBe(true); 55 | expect(promise instanceof Object).toBe(true); 56 | expect(new Promise(resolve => resolve()) instanceof nodePromiseType).toBe(true); 57 | expect(new Promise(resolve => resolve()) instanceof nodeObjectType).toBe(true); 58 | }); 59 | 60 | it("works with node Uint8Array type", () => { 61 | expect(buffer instanceof Buffer).toBe(true); 62 | expect(buffer instanceof Uint8Array).toBe(true); 63 | expect(buffer instanceof globalTypedArrayType).toBe(true); 64 | expect(buffer instanceof Object).toBe(true); 65 | expect(new Uint8Array([]) instanceof nodeUint8ArrayType).toBe(true); 66 | expect(new Uint8Array([]) instanceof nodeTypedArrayType).toBe(true); 67 | expect(new Uint8Array([]) instanceof nodeObjectType).toBe(true); 68 | }); 69 | 70 | it("recognizes typed arrays as objects", () => { 71 | expect(new Uint8Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 72 | expect(new Uint8ClampedArray([ 1, 2, 3 ]) instanceof Object).toBe(true); 73 | expect(new Uint16Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 74 | expect(new Uint32Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 75 | expect(new BigUint64Array([]) instanceof Object).toBe(true); 76 | expect(new Int8Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 77 | expect(new Int16Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 78 | expect(new Int32Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 79 | expect(new BigInt64Array([]) instanceof Object).toBe(true); 80 | expect(new Float32Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 81 | expect(new Float64Array([ 1, 2, 3 ]) instanceof Object).toBe(true); 82 | }); 83 | 84 | it("recognizes typed arrays as instances of TypedArray", () => { 85 | expect(new Uint8Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 86 | expect(new Uint8ClampedArray([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 87 | expect(new Uint16Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 88 | expect(new Uint32Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 89 | expect(new BigUint64Array([]) instanceof globalTypedArrayType).toBe(true); 90 | expect(new Int8Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 91 | expect(new Int16Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 92 | expect(new Int32Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 93 | expect(new BigInt64Array([]) instanceof globalTypedArrayType).toBe(true); 94 | expect(new Float32Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 95 | expect(new Float64Array([ 1, 2, 3 ]) instanceof globalTypedArrayType).toBe(true); 96 | }); 97 | 98 | it("works with v8 serialize/deserialize", () => { 99 | const m1 = new Map(); 100 | const m2 = v8.deserialize(v8.serialize(m1)); 101 | expect(m1).toEqual(m2); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/test/fakeTimers.test.ts: -------------------------------------------------------------------------------- 1 | describe("fake timers", () => { 2 | beforeEach(() => { 3 | jest.useFakeTimers(); 4 | }); 5 | afterEach(() => { 6 | jest.useRealTimers(); 7 | }); 8 | it("can be used with single context node env", () => { 9 | jest.useFakeTimers(); 10 | jest.spyOn(global, "setTimeout"); 11 | const callback = jest.fn(); 12 | setTimeout(callback, 1000); 13 | expect(setTimeout).toHaveBeenCalledTimes(1); 14 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); 15 | jest.runAllTimers(); 16 | expect(callback).toHaveBeenCalledTimes(1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "rootDir": "src", 8 | "outDir": "lib", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": false, 18 | "esModuleInterop": true, 19 | "skipLibCheck": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------