Skip to content
This repository has been archived by the owner on Nov 3, 2020. It is now read-only.

Commit

Permalink
Fix memory leak warning issue (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianschmitz committed May 25, 2019
1 parent 9069f61 commit 3d2e617
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 62 deletions.
4 changes: 3 additions & 1 deletion 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:

Expand All @@ -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.
20 changes: 12 additions & 8 deletions 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 <ianschmitz@gmail.com>",
"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 <ianschmitz@gmail.com>",
"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",
Expand Down
110 changes: 59 additions & 51 deletions src/index.ts
@@ -1,70 +1,77 @@
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<number> | null;
global: Win;
errorEventListener: ((event: Event & { error: Error }) => void) | null;
moduleMocker: ModuleMocker | null;

constructor(config: jest.ProjectConfig, options: EnvironmentOptions = {}) {
this.dom = new JSDOM(
"<!DOCTYPE html>",
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("<!DOCTYPE html>", {
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 => {
if (userErrorListenerCount === 0 && event.error) {
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<typeof originalAddListener>
) {
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<typeof originalRemoveListener>
) {
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,
Expand All @@ -73,17 +80,17 @@ class JSDomEnvironment {

this.fakeTimers = new FakeTimers({
config,
global: this.global,
global: global as any,
moduleMocker: this.moduleMocker,
timerConfig,
});
}

setup(): Promise<void> {
setup() {
return Promise.resolve();
}

teardown(): Promise<void> {
teardown() {
if (this.fakeTimers) {
this.fakeTimers.dispose();
}
Expand All @@ -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;
6 changes: 4 additions & 2 deletions tsconfig.json
@@ -1,14 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"declaration": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./lib",
"skipLibCheck": true,
"strict": true
"strict": true,
"target": "es5"
},
"include": ["src/**/*"],
"exclude": ["src/**/__mocks__/*", "src/**/__tests__/*"]
Expand Down

0 comments on commit 3d2e617

Please sign in to comment.