Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate jest-environment-jsdom to TypeScript #8003

Merged
merged 31 commits into from Feb 28, 2019

Conversation

@lirbank
Copy link
Contributor

lirbank commented Feb 27, 2019

@SimenB I have a bunch of questions. I'll make a few at the time to see if it clears to me as we go (see inline review comments in a sec).

Built diff:

diff --git c/packages/jest-environment-jsdom/build/index.js w/packages/jest-environment-jsdom/build/index.js
index 8cd500cf4..718039095 100644
--- c/packages/jest-environment-jsdom/build/index.js
+++ w/packages/jest-environment-jsdom/build/index.js
@@ -1,29 +1,29 @@
 'use strict';
 
-function _fakeTimers() {
-  const data = require('@jest/fake-timers');
+function _jestUtil() {
+  const data = require('jest-util');
 
-  _fakeTimers = function _fakeTimers() {
+  _jestUtil = function _jestUtil() {
     return data;
   };
 
   return data;
 }
 
-function _jestUtil() {
-  const data = require('jest-util');
+function _jestMock() {
+  const data = _interopRequireDefault(require('jest-mock'));
 
-  _jestUtil = function _jestUtil() {
+  _jestMock = function _jestMock() {
     return data;
   };
 
   return data;
 }
 
-function _jestMock() {
-  const data = _interopRequireDefault(require('jest-mock'));
+function _fakeTimers() {
+  const data = require('@jest/fake-timers');
 
-  _jestMock = function _jestMock() {
+  _fakeTimers = function _fakeTimers() {
     return data;
   };
 
@@ -76,7 +76,13 @@ function _defineProperty(obj, key, value) {
   return obj;
 }
 
+function isWin(globals) {
+  return globals.document !== undefined;
+} // A lot of the globals expected by other APIs are `NodeJS.Global` and not
+// `Window`, so we need to cast here and there
+
 class JSDOMEnvironment {
+  // @ts-ignore
   constructor(config, options = {}) {
     this.dom = new (_jsdom()).JSDOM(
       '<!DOCTYPE html>',
@@ -92,7 +98,11 @@ class JSDOMEnvironment {
         config.testEnvironmentOptions
       )
     );
-    const global = (this.global = this.dom.window.document.defaultView); // Node's error-message stack size is limited at 10, but it's pretty useful
+    const global = (this.global = this.dom.window.document.defaultView);
+
+    if (!global) {
+      throw new Error('JSDOM did not return a Window object');
+    } // 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;
@@ -111,20 +121,20 @@ class JSDOMEnvironment {
     const originalRemoveListener = global.removeEventListener;
     let userErrorListenerCount = 0;
 
-    global.addEventListener = function(name) {
-      if (name === 'error') {
+    global.addEventListener = function(...args) {
+      if (args[0] === 'error') {
         userErrorListenerCount++;
       }
 
-      return originalAddListener.apply(this, arguments);
+      return originalAddListener.apply(this, args);
     };
 
-    global.removeEventListener = function(name) {
-      if (name === 'error') {
+    global.removeEventListener = function(...args) {
+      if (args[0] === 'error') {
         userErrorListenerCount--;
       }
 
-      return originalRemoveListener.apply(this, arguments);
+      return originalRemoveListener.apply(this, args);
     };
 
     this.moduleMocker = new (_jestMock()).default.ModuleMocker(global);
@@ -134,7 +144,7 @@ class JSDOMEnvironment {
     };
     this.fakeTimers = new (_fakeTimers()).JestFakeTimers({
       config,
-      global,
+      global: global,
       moduleMocker: this.moduleMocker,
       timerConfig
     });
@@ -150,14 +160,17 @@ class JSDOMEnvironment {
     }
 
     if (this.global) {
-      if (this.errorEventListener) {
+      if (this.errorEventListener && isWin(this.global)) {
         this.global.removeEventListener('error', this.errorEventListener);
       } // Dispose "document" to prevent "load" event from triggering.
 
       Object.defineProperty(this.global, 'document', {
         value: null
       });
-      this.global.close();
+
+      if (isWin(this.global)) {
+        this.global.close();
+      }
     }
 
     this.errorEventListener = null;
@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented Feb 27, 2019

(see inline review comments in a sec).

👀

I'll hold off on commenting just yet then 😀

@lirbank

This comment has been minimized.

Copy link
Contributor Author

lirbank commented Feb 27, 2019

(see inline review comments in a sec).

👀

I'll hold off on commenting just yet then 😀

Yes, please, the code is FULL or errors. At this point I just need some guidance.

@lirbank

This comment has been minimized.

Copy link
Contributor Author

lirbank commented Feb 27, 2019

Q1: defaultView returns Window | null which is not compatible with Global.Global. It looks like we're overwriting this.global with a window object, which seems odd to me?

const global = (this.global = this.dom.window.document.defaultView);

@lirbank

This comment has been minimized.

Copy link
Contributor Author

lirbank commented Feb 27, 2019

Q2: To be able to reset this.global, we need to change it's type to accept null. This is currently not compatible with JestEnvironment. Should I update JestEnvironment?

Q2b: Also, should we also set this.global to null on teardown in jest-environment-node?

@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented Feb 27, 2019

Q1: defaultView returns Window | null which is not compatible with Global.Global. It looks like we're overwriting this.global with a window object, which seems odd to me?

Yeah, we have a TODO it it should accept Window there. It probably should, but that will break other stuff. Just add a @ts-ignore above the assignment for now

Q2: To be able to reset this.global, we need to change it's type to accept null. This is currently not compatible with JestEnvironment. Should I update JestEnvironment?

Yeah, feel free to make it nullable

Q2b: Also, should we also set this.global to null on teardown in jest-environment-node?

Yeah, I think that makes sense - easier for GC to pick it up then

lirbank added 3 commits Feb 27, 2019
@lirbank

This comment has been minimized.

Copy link
Contributor Author

lirbank commented Feb 27, 2019

Q2b: Also, should we also set this.global to null on teardown in jest-environment-node?

Yeah, I think that makes sense - easier for GC to pick it up then

Do you prefer if this goes into a separate PR, since it's another package? Or doesn't matter?

@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented Feb 27, 2019

Do you prefer if this goes into a separate PR, since it's another package? Or doesn't matter?

Doesn't matter

@lirbank

This comment has been minimized.

Copy link
Contributor Author

lirbank commented Feb 27, 2019

@SimenB please take a look when you have the time, and let me know what you think.

There are quite a few discrepancies between types that technically should match. I have solved these with any types and @ts-ignore. Not a big fan of hacking the type system, but wanted to get a working version quickly. Happy to update as you see fit.

I've changed the return type of runScript to unknown. While I like the idea of using unknown over any I am not sure this is in the right direction, LMK what you think.

@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented on packages/jest-environment/src/index.ts in 1f9081a Feb 27, 2019

I'd keep this as it was, and move the check into jest-environment-jsdom. It's an error from the environments if they don't return {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null, and it would be great if the typings told developers that instead of failing at runtime

packages/jest-environment-jsdom/src/index.ts Outdated Show resolved Hide resolved
packages/jest-environment-jsdom/src/index.ts Outdated Show resolved Hide resolved
if (this.dom) {
// Explicitly returning `unknown` since `runVMScript` currently returns
// `void`, which is wrong
return this.dom.runVMScript(script) as unknown;

This comment has been minimized.

Copy link
@SimenB

SimenB Feb 28, 2019

Collaborator
Suggested change
return this.dom.runVMScript(script) as unknown;
return this.dom.runVMScript(script) as any;

We know what the type here will be, no need to assert it.

(it'll have the EVAL_RESULT_VARIABLE object thing due to

return (
'({"' +
ScriptTransformer.EVAL_RESULT_VARIABLE +
`":function(${Array.from(globals).join(',')}){` +
content +
'\n}});'
);
)

This comment has been minimized.

Copy link
@lirbank

lirbank Feb 28, 2019

Author Contributor

We do need to assert it since runVMScript() returns void, which is not true, and runScript() is also expected to receive {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null (via the JestEnvironment interface). I have a new solution for this, committing in a sec.

This comment has been minimized.

Copy link
@lirbank

lirbank Feb 28, 2019

Author Contributor

We know what the type here will be

This contrasts with #8003 (comment) - we can't both set the return type to the EVAL_RESULT_VARIABLE thing, AND the return value of the provided script... Sorry, I'm a bit confused here.

If we want the return type of runScript to be the the same as the return type of the provided script, we could use a generic to let the user of jest-environment-jsdom set the return type? Just an idea. But I feel there is something I am missing here... :)

This comment has been minimized.

Copy link
@SimenB

SimenB Feb 28, 2019

Collaborator

The return value of the provided script is, 100%, the EVAL_RESULT_VARIABLE thing (unless the environment has been torn down, in which cse it will be null). We construct the script ourselves. The function assigned to EVAL_RESULT_VARIABLE, we do not know what will return when invoked (that's the unknown part), but we don't care about that result

If we want the return type of runScript to be the the same as the return type of the provided script, we could use a generic to let the user of jest-environment-jsdom set the return type? Just an idea. But I feel there is something I am missing here... :)

I don't think that's needed - the script we get is constructed from a string source we've read (and potentially transformed) from disk. So we cannot statically know what it will return

packages/jest-environment/src/index.ts Outdated Show resolved Hide resolved
packages/jest-runtime/src/index.ts Outdated Show resolved Hide resolved
lirbank and others added 6 commits Feb 28, 2019
@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented Feb 28, 2019

@lirbank I pushed a commit that removes some of the comments to reduce the diff - this allows git to see it's a file rename and retain blame history

@SimenB
SimenB approved these changes Feb 28, 2019
Copy link
Collaborator

SimenB left a comment

This was a tough one! I think it's good to go now, though 🙂 I added a diff to the OP which looks good. The generated typings file also looks good. Will merge if CI is happy.

Thank you so much for sticking with me through this review!

@SimenB

This comment has been minimized.

Copy link
Collaborator

SimenB commented Feb 28, 2019

Also, I've opened up a PR to definitelytyped to fix the JSDOM type: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/33476/files


CI is passing! :🎉@lirbank could you mark this PR ready for review? I'm not allowed to merge when it's a draft 🙂

@lirbank lirbank marked this pull request as ready for review Feb 28, 2019
@SimenB SimenB merged commit de52b48 into facebook:master Feb 28, 2019
9 of 11 checks passed
9 of 11 checks passed
ci/circleci: test-jest-circus Your tests failed on CircleCI
Details
continuous-integration/appveyor/pr Waiting for AppVeyor build to complete
Details
ci/circleci: lint-and-typecheck Your tests passed on CircleCI!
Details
ci/circleci: test-browser Your tests passed on CircleCI!
Details
ci/circleci: test-node-10 Your tests passed on CircleCI!
Details
ci/circleci: test-node-11 Your tests passed on CircleCI!
Details
ci/circleci: test-node-6 Your tests passed on CircleCI!
Details
ci/circleci: test-node-8 Your tests passed on CircleCI!
Details
ci/circleci: test-or-deploy-website Your tests passed on CircleCI!
Details
deploy/netlify Deploy preview ready!
Details
facebook.jest #20190228.24 succeeded
Details
@lirbank lirbank deleted the lirbank:jest-environment branch Feb 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.