Skip to content

Commit c814a01

Browse files
committed
Fix #42: Hack around React's development mode error triggering hack.
React's development mode has a funny way of triggering errors, which tries to convince that exceptions are uncaught (for their development tools to report them) even though React catches them for the "error boundaries" feature. Since our jsdom handler reports uncaught exceptions as hard failures that fail the run, this creates a very bad interaction where every caught-but-not-really exception crashes the run. We hack around this hack by detecting when our error handler is in fact called by React's own hack. In that case, we ignore the uncaught exception, and proceed with normal execution. We add a test that actually uses React's error boundaries, and makes sure that the React component can still detect the error and its error message.
1 parent 9dde29e commit c814a01

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

build.sbt

+9-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,15 @@ lazy val `scalajs-env-jsdom-nodejs`: Project = project.in(file("jsdom-nodejs-env
7575
"org.scala-js" %% "scalajs-env-nodejs" % scalaJSVersion,
7676

7777
"com.novocode" % "junit-interface" % "0.11" % "test",
78-
"org.scala-js" %% "scalajs-js-envs-test-kit" % scalaJSVersion % "test"
78+
"org.scala-js" %% "scalajs-js-envs-test-kit" % scalaJSVersion % "test",
79+
80+
/* See JSDOMNodeJSEnvTest.reactUnhandledExceptionHack.
81+
* We use intransitive() because we do not need the transitive
82+
* dependencies of these webjars, and one of them actually fails to
83+
* resolve (see https://github.com/webjars/webjars/issues/1789).
84+
*/
85+
"org.webjars.npm" % "react" % "16.13.1" % "test" intransitive(),
86+
"org.webjars.npm" % "react-dom" % "16.13.1" % "test" intransitive(),
7987
)
8088
)
8189

jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala

+9
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends JSEnv {
8484
| var virtualConsole = new jsdom.VirtualConsole()
8585
| .sendTo(console, { omitJSDOMErrors: true });
8686
| virtualConsole.on("jsdomError", function (error) {
87+
| /* #42 Counter-hack the hack that React's development mode uses
88+
| * to bypass browsers' debugging tools. If we detect that we are
89+
| * called from that hack, we do nothing.
90+
| */
91+
| var isWithinReactsInvokeGuardedCallbackDevHack_issue42 =
92+
| new Error("").stack.indexOf("invokeGuardedCallbackDev") >= 0;
93+
| if (isWithinReactsInvokeGuardedCallbackDevHack_issue42)
94+
| return;
95+
|
8796
| try {
8897
| // Display as much info about the error as possible
8998
| if (error.detail && error.detail.stack) {

jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala

+102
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package org.scalajs.jsenv.jsdomnodejs
22

3+
import java.nio.charset.StandardCharsets
4+
import java.nio.file.{Files, Path}
5+
36
import scala.concurrent.duration._
47

8+
import com.google.common.jimfs.Jimfs
9+
510
import org.junit.Test
611

12+
import org.scalajs.jsenv.Input
713
import org.scalajs.jsenv.test.kit.TestKit
814

915
class JSDOMNodeJSEnvTest {
16+
import JSDOMNodeJSEnvTest._
17+
1018
private val kit = new TestKit(new JSDOMNodeJSEnv, 1.minute)
1119

1220
@Test
@@ -21,4 +29,98 @@ class JSDOMNodeJSEnvTest {
2129
.expectOut("http://localhost/foo\n")
2230
}
2331
}
32+
33+
@Test
34+
def reactUnhandledExceptionHack_issue42: Unit = {
35+
val code =
36+
"""
37+
|const rootElement = document.createElement("div");
38+
|document.body.appendChild(rootElement);
39+
|
40+
|class ThrowingComponent extends React.Component {
41+
| render() {
42+
| throw new Error("boom");
43+
| }
44+
|}
45+
|
46+
|class ErrorBoundary extends React.Component {
47+
| constructor(props) {
48+
| super(props);
49+
| this.state = { hasError: false };
50+
| }
51+
|
52+
| componentDidCatch(error, info) {
53+
| this.setState({error: error.message, hasError: true});
54+
| }
55+
|
56+
| render() {
57+
| if (this.state.hasError) {
58+
| console.log("render-error");
59+
| return React.createElement("p", null,
60+
| `Caught error: ${this.state.error}`);
61+
| } else {
62+
| return this.props.children;
63+
| }
64+
| }
65+
|}
66+
|
67+
|class MyMainComponent extends React.Component {
68+
| render() {
69+
| console.log("two");
70+
| return React.createElement(ErrorBoundary, null,
71+
| React.createElement(ThrowingComponent)
72+
| );
73+
| }
74+
|}
75+
|
76+
|console.log("begin");
77+
|
78+
|const mounted = ReactDOM.render(
79+
| React.createElement(ErrorBoundary, null,
80+
| React.createElement(ThrowingComponent, null)
81+
| ),
82+
| rootElement
83+
|);
84+
|
85+
|console.log(document.querySelector("p").textContent);
86+
|
87+
|console.log("end");
88+
""".stripMargin
89+
90+
kit.withRun(ReactJSFiles :+ codeToInput(code)) {
91+
_.expectOut("begin\nrender-error\nCaught error: boom\nend\n")
92+
.succeeds()
93+
}
94+
}
95+
}
96+
97+
object JSDOMNodeJSEnvTest {
98+
private lazy val ReactJSFiles: List[Input] = {
99+
val fs = Jimfs.newFileSystem()
100+
val reactFile = copyResource(
101+
"/META-INF/resources/webjars/react/16.13.1/umd/react.development.js",
102+
fs.getPath("react.development.js"))
103+
val reactDOMFile = copyResource(
104+
"/META-INF/resources/webjars/react-dom/16.13.1/umd/react-dom.development.js",
105+
fs.getPath("react-dom.development.js"))
106+
List(reactFile, reactDOMFile).map(Input.Script(_))
107+
}
108+
109+
private def copyResource(name: String, out: Path): out.type = {
110+
val inputStream = getClass().getResourceAsStream(name)
111+
assert(inputStream != null, s"couldn't load $name from resources")
112+
try {
113+
Files.copy(inputStream, out)
114+
} finally {
115+
inputStream.close()
116+
}
117+
out
118+
}
119+
120+
private def codeToInput(code: String): Input = {
121+
val p = Files.write(
122+
Jimfs.newFileSystem().getPath("testScript.js"),
123+
code.getBytes(StandardCharsets.UTF_8))
124+
Input.Script(p)
125+
}
24126
}

0 commit comments

Comments
 (0)