Skip to content

Commit b60ddba

Browse files
committed
Assert Js{,Fn}Component arguments are valid
1 parent 0d2afd2 commit b60ddba

File tree

6 files changed

+106
-2
lines changed

6 files changed

+106
-2
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package japgolly.scalajs.react.component
2+
3+
import scala.annotation.elidable
4+
import scala.scalajs.js
5+
import japgolly.scalajs.react.internal.JsUtil
6+
7+
object InspectRaw {
8+
9+
def isComponent(a: js.Any): Boolean =
10+
a match {
11+
case _: js.Function => true
12+
case _ => false
13+
}
14+
15+
def invalidComponentDesc(a: js.Any): String =
16+
a.asInstanceOf[Any] match {
17+
case s: String => '"' + s + "\". Strings are no longer supported; either create a facade or use js.Dynamic. See docs for detail."
18+
case o: js.Object => JsUtil.inspectObject(o)
19+
case () => a.toString
20+
case _ => s"$a (type=${js.typeOf(a)})"
21+
}
22+
23+
@elidable(elidable.ASSERTION)
24+
@inline
25+
def assertIsComponent(aa: => js.Any, name: => String): Unit = {
26+
val a = aa
27+
assert(isComponent(a), s"Invalid $name: ${invalidComponentDesc(a)}")
28+
}
29+
30+
}

core/src/main/scala/japgolly/scalajs/react/component/Js.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import japgolly.scalajs.react.{Callback, Children, CtorType, PropsChildren, vdom
77
object Js extends JsBaseComponentTemplate[RAW.ReactClassUntyped] {
88

99
def apply[P <: js.Object, C <: Children, S <: js.Object](raw: js.Any)(implicit s: CtorType.Summoner[P, C]): Component[P, S, s.CT] = {
10-
assert(raw.isInstanceOf[js.Object], s"Invalid JsComponent: $raw")
10+
InspectRaw.assertIsComponent(raw, "JsComponent")
11+
force[P, C, S](raw)(s)
12+
}
13+
14+
def force[P <: js.Object, C <: Children, S <: js.Object](raw: js.Any)(implicit s: CtorType.Summoner[P, C]): Component[P, S, s.CT] = {
1115
val rc = raw.asInstanceOf[RAW.ReactClass[P, S]]
1216
component[P, C, S](rc)(s)
1317
}

core/src/main/scala/japgolly/scalajs/react/component/JsFn.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ object JsFn extends JsBaseComponentTemplate[RAW.ReactFunctionalComponent] {
1111
type Mounted = Unit
1212

1313
def apply[P <: js.Object, C <: Children](raw: js.Any)(implicit s: CtorType.Summoner[P, C]): Component[P, s.CT] = {
14-
assert(raw.isInstanceOf[js.Object], s"Invalid JsFnComponent: $raw")
14+
InspectRaw.assertIsComponent(raw, "JsFnComponent")
15+
force[P, C](raw)(s)
16+
}
17+
18+
def force[P <: js.Object, C <: Children](raw: js.Any)(implicit s: CtorType.Summoner[P, C]): Component[P, s.CT] = {
1519
val rc = raw.asInstanceOf[RAW.ReactFunctionalComponent]
1620
componentRoot[P, s.CT, Unmounted[P]](rc, s.pf.rmap(s.summon(rc))(unmountedRoot))(s.pf)
1721
}

doc/changelog/1.0.0-RC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ See https://github.com/japgolly/scalajs-react/blob/v1.0.0-RC1/doc/changelog/1.0.
1515
* Allow `JsComponent#withRawType` to expose the raw ES6 class
1616
* Add `WebpackRequire` objects for webpack/scalajs-bundler users
1717
* Replace all overloaded `Js{,Fn}Component` `.apply` methods with `.apply(js.Any)` which expects a raw value.
18+
* Provide useful debug info when a `Js{,Fn}Component` is constructed with an invalid value.
1819
* There's now a Cats module, analogous to the Scalaz module.
1920

2021
#### Migration:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package japgolly.scalajs.react.core
2+
3+
import scala.scalajs.js
4+
import utest._
5+
import japgolly.scalajs.react._
6+
import japgolly.scalajs.react.test.TestUtil._
7+
8+
object JsLikeComponentTest extends TestSuite {
9+
10+
def RawJs3 = JsComponentPTest.RawComp
11+
def RawJs6a = JsComponentEs6PTest.RawComp // nullary ctor
12+
def RawJs6b = JsComponentEs6STest.RawComp // unary ctor
13+
def RawJsFn = JsFnComponentTest.RawComp
14+
15+
def assertNoError[A](a: => A): Unit = {
16+
a
17+
()
18+
}
19+
20+
val o = js.Dynamic.literal("hello" -> 123)
21+
22+
override def tests = TestSuite {
23+
24+
'js {
25+
'undefined - expectErrorContaining("undefined")(JsComponent[Null, Children.None, Null](js.undefined))
26+
'string - expectErrorContaining("tring")(JsComponent[Null, Children.None, Null]("what"))
27+
'num - expectErrorContaining("123")(JsComponent[Null, Children.None, Null](123))
28+
'obj - expectErrorContaining("hello")(JsComponent[Null, Children.None, Null](o))
29+
// 'fn - expectErrorContaining("a raw JsFnComponent")(JsComponent[Null, Children.None, Null](RawJsFn))
30+
'es3 - assertNoError(JsComponent[Null, Children.None, Null](RawJs3))
31+
'es60 - assertNoError(JsComponent[Null, Children.None, Null](RawJs6a))
32+
'es61 - assertNoError(JsComponent[Null, Children.None, Null](RawJs6b))
33+
}
34+
35+
'jsFn {
36+
'undefined - expectErrorContaining("undefined")(JsFnComponent[Null, Children.None](js.undefined))
37+
'string - expectErrorContaining("tring")(JsFnComponent[Null, Children.None]("what"))
38+
'num - expectErrorContaining("123")(JsFnComponent[Null, Children.None](123))
39+
'obj - expectErrorContaining("hello")(JsFnComponent[Null, Children.None](o))
40+
'fn - assertNoError(JsFnComponent[Null, Children.None](RawJsFn))
41+
// 'es3 - expectErrorContaining("a raw JsComponent")(JsFnComponent[Null, Children.None](RawJs3))
42+
// 'es60 - expectErrorContaining("a raw JsComponent")(JsFnComponent[Null, Children.None](RawJs6a))
43+
// 'es61 - expectErrorContaining("a raw JsComponent")(JsFnComponent[Null, Children.None](RawJs6b))
44+
}
45+
46+
}
47+
}

test/src/test/scala/japgolly/scalajs/react/test/TestUtil.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,23 @@ trait TestUtil
135135

136136
def yesItsMounted: Option[Boolean] = Some(true)
137137
def nopeNotMounted: Option[Boolean] = Some(false)
138+
139+
def catchError[A](a: => A): Option[Throwable] =
140+
try {
141+
a
142+
None
143+
}
144+
catch {
145+
case t: Throwable => Some(t)
146+
}
147+
148+
def expectError[A](a: => A): Throwable =
149+
catchError(a).getOrElse(fail("Error expected but code succeeded."))
150+
151+
def expectErrorContaining[A](errFrag: String)(a: => A): String = {
152+
val err = expectError(a).getMessage
153+
assertContains(err, errFrag)
154+
err
155+
}
138156
}
139157

0 commit comments

Comments
 (0)