From 3d2e61771770a77a1504477855c83d047608b3c9 Mon Sep 17 00:00:00 2001 From: Ian Schmitz Date: Sat, 25 May 2019 12:14:09 -0700 Subject: [PATCH] Fix memory leak warning issue (#1) --- README.md | 4 +- package.json | 20 +++++---- src/index.ts | 110 +++++++++++++++++++++++++++----------------------- tsconfig.json | 6 ++- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index ee5a1b7..213c70a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # jest-environment-jsdom-fourteen -Jest environment using JSDOM 14, which does not support Node 6 ([and will therefore not be used in Jest any time soon](https://github.com/kentcdodds/dom-testing-library/issues/115#issuecomment-428314737)). If you would like to use JSDOM 13, see https://github.com/theneva/jest-environment-jsdom-thirteen. +[Jest](https://jestjs.io) by default uses [JSDOM](https://github.com/jsdom/jsdom) 11 to support Node 6. This package uses JSDOM 14, which supports Node >= 8, and does not support Node 6 ([and will therefore not be used in Jest any time soon](https://github.com/kentcdodds/dom-testing-library/issues/115#issuecomment-428314737)). If you need a newer JSDOM than the one that ships with Jest, install this package using `npm install --save-dev jest-environment-jsdom-fourteen` or `yarn add jest-environment-jsdom-fourteen --dev`, and edit your Jest config like so: @@ -9,3 +9,5 @@ If you need a newer JSDOM than the one that ships with Jest, install this packag "testEnvironment": "jest-environment-jsdom-fourteen" } ``` + +If you would like to use JSDOM 13, see https://github.com/theneva/jest-environment-jsdom-thirteen. diff --git a/package.json b/package.json index 61c71c7..4d7439d 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,29 @@ { "name": "jest-environment-jsdom-fourteen", - "version": "0.1.0", + "description": "JSDOM environment for Jest with JSDOM 14", + "version": "1.0.0-alpha.0", + "author": "Ian Schmitz ", "repository": "https://github.com/ianschmitz/jest-environment-jsdom-fourteen", "license": "MIT", "main": "lib/index.js", "files": [ "lib" ], - "dependencies": { - "jest-mock": "^24.5.0", - "jest-util": "^24.5.0", - "jsdom": "^14.0.0" - }, "scripts": { "test": "jest", "build": "rimraf lib && tsc" }, - "description": "JSDOM environment for Jest with JSDOM 14", - "author": "Ian Schmitz ", + "dependencies": { + "@jest/environment": "^24.3.0", + "@jest/fake-timers": "^24.3.0", + "@jest/types": "^24.3.0", + "jest-mock": "^24.0.0", + "jest-util": "^24.0.0", + "jsdom": "^14.0.0" + }, "devDependencies": { "@types/jest": "^24.0.11", + "@types/jsdom": "^12.2.3", "@types/node": "^11.11.3", "husky": "^1.3.1", "jest": "^24.1.0", diff --git a/src/index.ts b/src/index.ts index ca6a21b..c5f683c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,42 +1,45 @@ import { Script } from "vm"; -import { ModuleMocker } from "jest-mock"; -import { FakeTimers, installCommonGlobals } from "jest-util"; +import { Global, Config } from "@jest/types"; +import { installCommonGlobals } from "jest-util"; +import mock, { ModuleMocker } from "jest-mock"; +import { JestFakeTimers as FakeTimers } from "@jest/fake-timers"; +import { JestEnvironment, EnvironmentContext } from "@jest/environment"; import { JSDOM, VirtualConsole } from "jsdom"; -interface EnvironmentOptions { - console?: Object; - resources?: "usable" | object; -} +// The `Window` interface does not have an `Error.stackTraceLimit` property, but +// `JSDOMEnvironment` assumes it is there. +type Win = Window & + Global.Global & { + Error: { + stackTraceLimit: number; + }; + }; -class JSDomEnvironment { - dom: any; - fakeTimers: any; - global: any; - errorEventListener?: Function; - moduleMocker?: ModuleMocker; +class JSDOMEnvironment implements JestEnvironment { + dom: JSDOM | null; + fakeTimers: FakeTimers | null; + global: Win; + errorEventListener: ((event: Event & { error: Error }) => void) | null; + moduleMocker: ModuleMocker | null; - constructor(config: jest.ProjectConfig, options: EnvironmentOptions = {}) { - this.dom = new JSDOM( - "", - Object.assign( - { - pretendToBeVisual: true, - runScripts: "dangerously", - url: config.testURL, - virtualConsole: new VirtualConsole().sendTo( - options.console || console - ), - resources: options.resources, - }, - config.testEnvironmentOptions - ) - ); + constructor(config: Config.ProjectConfig, options: EnvironmentContext = {}) { + this.dom = new JSDOM("", { + pretendToBeVisual: true, + runScripts: "dangerously", + url: config.testURL, + virtualConsole: new VirtualConsole().sendTo(options.console || console), + ...config.testEnvironmentOptions, + }); + const global = (this.global = this.dom.window.document.defaultView as Win); + + if (!global) { + throw new Error("JSDOM did not return a Window object"); + } - this.global = this.dom.window.document.defaultView; // Node's error-message stack size is limited at 10, but it's pretty useful // to see more than that when a test fails. this.global.Error.stackTraceLimit = 100; - installCommonGlobals(this.global, config.globals); + installCommonGlobals(global as any, config.globals); // Report uncaught errors. this.errorEventListener = event => { @@ -44,27 +47,31 @@ class JSDomEnvironment { process.emit("uncaughtException", event.error); } }; - this.global.addEventListener("error", this.errorEventListener); + global.addEventListener("error", this.errorEventListener); // However, don't report them as uncaught if the user listens to 'error' event. // In that case, we assume the might have custom error handling logic. - const originalAddListener = this.global.addEventListener; - const originalRemoveListener = this.global.removeEventListener; + const originalAddListener = global.addEventListener; + const originalRemoveListener = global.removeEventListener; let userErrorListenerCount = 0; - this.global.addEventListener = function(name) { - if (name === "error") { + global.addEventListener = function( + ...args: Parameters + ) { + if (args[0] === "error") { userErrorListenerCount++; } - return originalAddListener.apply(this, arguments); + return originalAddListener.apply(this, args); }; - this.global.removeEventListener = function(name) { - if (name === "error") { + global.removeEventListener = function( + ...args: Parameters + ) { + if (args[0] === "error") { userErrorListenerCount--; } - return originalRemoveListener.apply(this, arguments); + return originalRemoveListener.apply(this, args); }; - this.moduleMocker = new ModuleMocker(this.global); + this.moduleMocker = new mock.ModuleMocker(global as any); const timerConfig = { idToRef: (id: number) => id, @@ -73,17 +80,17 @@ class JSDomEnvironment { this.fakeTimers = new FakeTimers({ config, - global: this.global, + global: global as any, moduleMocker: this.moduleMocker, timerConfig, }); } - setup(): Promise { + setup() { return Promise.resolve(); } - teardown(): Promise { + teardown() { if (this.fakeTimers) { this.fakeTimers.dispose(); } @@ -92,22 +99,23 @@ class JSDomEnvironment { this.global.removeEventListener("error", this.errorEventListener); } // Dispose "document" to prevent "load" event from triggering. - Object.defineProperty(this.global, "document", { value: undefined }); + Object.defineProperty(this.global, "document", { value: null }); this.global.close(); } - this.errorEventListener = undefined; - this.global = undefined; - this.dom = undefined; - this.fakeTimers = undefined; + this.errorEventListener = null; + // @ts-ignore + this.global = null; + this.dom = null; + this.fakeTimers = null; return Promise.resolve(); } - runScript(script: Script): any { + runScript(script: Script) { if (this.dom) { - return this.dom.runVMScript(script); + return this.dom.runVMScript(script) as any; } return null; } } -module.exports = JSDomEnvironment; +export = JSDOMEnvironment; diff --git a/tsconfig.json b/tsconfig.json index b547792..68f5c73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "declaration": true, + "esModuleInterop": true, "module": "commonjs", "moduleResolution": "node", "noImplicitAny": false, @@ -8,7 +9,8 @@ "noUnusedParameters": true, "outDir": "./lib", "skipLibCheck": true, - "strict": true + "strict": true, + "target": "es5" }, "include": ["src/**/*"], "exclude": ["src/**/__mocks__/*", "src/**/__tests__/*"]