badeball / karma-jsdom-launcher

A Karma plugin. Launcher for jsdom.
MIT License
37 stars 16 forks source link

Karma hangs with jsdom #27

Open fluky opened 5 years ago

fluky commented 5 years ago

I'm trying to add jsdom to an Angular CLI project and when I switch the browser to jsdom it just hangs indefinitely after building.

Steps to reproduce:

  1. generate a new Angular cli project "ng new testJsdom" Note: using Angular Cli version 7.1.4
  2. add karma-jsdom-launcher and jsdom 43 "jsdom": "~13.2.0", 44 "karma-jsdom-launcher": "~7.0.0",
  3. set browser to jsdom in src/karma.conf.js
  4. ng test

At this point the project will compile and then go to a blank screen where it sits until killed with Ctrl+C.

badeball commented 5 years ago

This is an interesting one. So, what's happening here is that something in your bundle attempts to perform a synchronous XHR. jsdom implements this by using child_process.spawnSync. This will spawn a node process running xhr-sync-worker.js, which in turn will request the subject.

This request is usually handled by Karma's server. In this case however, Node is blocking in spawnSync and won't respond to the request. In other words, there's a deadlock going on here.

There's unfortunately not much for karma-jsdom-launcher to do here. An option would be for it to spawn its own process. This would however prohibit users of passing all possible options to jsdom, as they would have to be serialized somehow. In a proper language one would simply fork (syscall), but we don't have the luxury of that in Node.

I don't have time to dig further into Angular og Webpack for you, but if you can find a way for it to not perform synchronous XHR, then this will hopefully work.

badeball commented 5 years ago

Leaving it open for a while, any tips as to how make Webpack cooperate are welcome.

badeball commented 5 years ago

I couldn't let this one go and did some more digging. It turns out that the synchronous requests are made by source-map-support.

There's an issue in angular-cli that prevents me from disabling it, which I've tried to outline below.

  1. Even though I specify "sourceMap": false in angular.json

  2. Config is normalized

  3. sourceMap option is further normalized

  4. Normalization of sourceMap always ends up with an object

  5. source-map-support is conditionally included, but the condition is always true

I suspect that https://github.com/angular/angular-cli/pull/13062 introduced this behavior.

The good news is that source-map-support will ignore any errors thrown by XMLHttpRequest. Hence, as a temporary measure, you can apply the following patch to unclog your setup.

--- node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js
+++ node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js
@@ -425,6 +425,9 @@
     }

     open(method, uri, asynchronous, user, password) {
+      if (typeof asynchronous !== "undefined" && !asynchronous) {
+        throw new Error("Synchronous request prohibited.");
+      }
       if (!this._ownerDocument) {
         throw new DOMException("The object is in an invalid state.", "InvalidStateError");
       }

Sidenote: Having a proper way of patching 3rd party libraries is convenient when you can't be bothered to submit a patch yourself, either because you can't wait for it or because your request is too obscure. I've never been on a real project without ever having needed this. One approach I've found to work is having a postinstall script in package.json and a directory containing patches as that shown above.

// package.json
  "scripts": {
    "postinstall": "patches/apply.sh"
  }

// patches/apply.sh
#!/usr/bin/env bash

for f in patches/*.patch
do
  patch --reverse --dry-run --force --strip 0 < $f &>/dev/null

  if [ $? -ne 0 ]; then
    patch --forward --force --strip 0 < $f
  fi
done

// patches/01-jsdom-prohibit-synchronous-requests.patch
--- node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js
+++ node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js
@@ -425,6 +425,9 @@
     }

     open(method, uri, asynchronous, user, password) {
+      if (typeof asynchronous !== "undefined" && !asynchronous) {
+        throw new Error("Synchronous request prohibited.");
+      }
       if (!this._ownerDocument) {
         throw new DOMException("The object is in an invalid state.", "InvalidStateError");
       }
fluky commented 5 years ago

Brilliant! Thanks for the help. Glad you couldn't let it go 😄 .

As a suggestion you may want to add this to your README.md given the popularity of the platform.

filipesilva commented 5 years ago

@badeball regarding the patching of libs, just wanted to mention that https://github.com/ds300/patch-package helps with that problem as well. Exactly same concept though. It just sorta manages the patches and is cross platform.

badeball commented 5 years ago

I've tried creating a fresh Angular project and configured "sourceMap": false in angular.json (in the test part). It now seems to run fine. @fluky, can you update your dependencies and confirm that it now works?

Edit: To specify further - it seems like the fix was published with v7.3.1, as shown by git tag --contains acc31ba.

badeball commented 5 years ago

I will furthermore see if I can warn users of synchronous requests to the Karma server, as it will never really work.

tommyc38 commented 2 years ago

I was having issues with coverage reports with source maps set to false in an angular repo. Thanks @badeball for your workaround. I also had to increase NODE_OPTIONS=--max-old-space-size=8192 but it all works.