This file describes testing functionality provided by React.JS and scalajs-react.
It is plenty for simple and small unit tests.
For larger and/or complicated tests, it is highly recommended to use
Scala Test-State.
See this example
for how to write tests for real-world scalajs-react applications.
- Setup
React.addons.TestUtils
Simulate
andSimulation
ComponentTester
ReactTestVar
WithExternalCompStateAccess
DebugJs
Test Scripts
-
Install PhantomJS.
-
Add the following to SBT:
// scalajs-react test module libraryDependencies += "com.github.japgolly.scalajs-react" %%% "test" % "0.11.3" % "test" // React JS itself. // NOTE: Requires react-with-addons.js instead of just react.js jsDependencies ++= Seq( "org.webjars.bower" % "react" % "15.3.2" % "test" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React", "org.webjars.bower" % "react" % "15.3.2" % "test" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM", "org.webjars.bower" % "react" % "15.3.2" % "test" / "react-dom-server.js" minified "react-dom-server.min.js" dependsOn "react-dom.js" commonJSName "ReactDOMServer"), // Indicate that unit tests will access the DOM requiresDOM := true // Compile tests to JS using fast-optimisation scalaJSStage in Test := FastOptStage
-
To workaround a PhantomJS bug that causes tests to crash if they write to stderr, copy
PhantomJS2Env.scala
to yourproject
directory and add this to SBT:jsEnv in Test := new PhantomJS2Env(scalaJSPhantomJSClassLoader.value)
React.addons.TestUtils is wrapped in Scala and available as ReactTestUtils
. Usage is unchanged from JS.
To make event simulation easier, certain event types have dedicated, strongly-typed case classes to wrap event data. For example, JS like
// JavaScript
React.addons.TestUtils.Simulate.change(t, {target: {value: "Hi"}})
becomes
// Scala
ReactTestUtils.Simulate.change(t, ChangeEventData(value = "Hi"))
// Or shorter
ChangeEventData("Hi") simulate t
Simulations can also be created and composed without a target, using Simulation
. Example:
val a = Simulation.focus
val b = Simulation.change(ChangeEventData(value = "hi"))
val c = Simulation.blur
val s = a andThen b andThen c
// Or shorter
val s = Simulation.focus >> ChangeEventData("hi").simulation >> Simulation.blur
// Or even shorter again, using a convenience method
val s = Simulation.focusChangeBlur("hi")
// Then run it later
s run component
A helper that renders a component into the document so that
- you can easily change props and/or state.
- it is unmounted when the test is over.
If you don't need to test props changes, you can actually just use ReactTestUtils.withRenderedIntoDocument
.
This might accrue more helpful features over time but currently changing props is the only real advantage this has over withRenderedIntoDocument
.
If you wanted to test a component that has both props and state, a trivial component could be:
val Example = ReactComponentB[String]("Example")
.initialState(0)
.renderPS((_, p, s) => <.div(s" $p:$s "))
.build
you could test it like this:
ComponentTester(Example)("First props") { tester =>
// This imports:
// - component which is the mounted component.
// - setProps which changes the props and immediately re-renders.
// - setState which changes the state and immediately re-renders.
import tester._
def assertHtml(p: String, s: Int): Unit =
assert(component.outerHtmlWithoutReactDataAttr() == s"<div> $p:$s </div>")
assertHtml("First props", 0)
setState(2)
assertHtml("First props", 2)
setProps("Second props")
assertHtml("Second props", 2)
}
A ReactTestVar
is a class that can be used to mock the following types in tests:
ExternalVar[A]
ReusableVar[A]
Example:
import japgolly.scalajs.react._, vdom.prefix_<^._
import japgolly.scalajs.react.extra._
import japgolly.scalajs.react.test._
object ExampleTest extends TestSuite {
val NameChanger = ReactComponentB[ExternalVar[String]]("Name changer")
.render_P { evar =>
def updateName = (event: ReactEventI) => evar.set(event.target.value)
<.input(
^.`type` := "text",
^.value := evar.value,
^.onChange ==> updateName)
}
.build
override def tests = TestSuite {
val nameVar = ReactTestVar("guy")
val comp = ReactTestUtils renderIntoDocument NameChanger(nameVar.externalVar())
ChangeEventData("bob").simulate(comp)
assert(nameVar.value() == "bob")
}
}
Allows you to test a component that requires access to some external component state.
Say you have a component like:
val Example = ReactComponentB[(CompState.WriteAccess[Int], Int)]("I")
.render_P { case (w, i) =>
<.div(
<.div("state = ", <.span(i)),
<.button("inc", ^.onClick --> w.modState(_ + 1)) // weird here - just an example
)
}
.build
You can use WithExternalCompStateAccess
to write a test like this:
import japgolly.scalajs.react.test.WithExternalCompStateAccess
import utest._
object ExampleTest extends TestSuite {
val Parent = WithExternalCompStateAccess[Int](($, i) => Example(($, i)))
override def tests = TestSuite {
val c = ReactTestUtils renderIntoDocument Parent(3)
def state = ReactTestUtils.findRenderedDOMComponentWithTag(c, "span").getDOMNode().innerHTML.toInt
def button = ReactTestUtils.findRenderedDOMComponentWithTag(c, "button")
assert(state == 3)
ReactTestUtils.Simulate click button
assert(state == 4)
}
}
DebugJs is a dumping ground for functionality useful when testing raw JS.
It doesn't have much but inspectObject
can be tremendously useful.
Example:
.componentDidMount($ => Callback {
val dom = $.getDOMNode()
println(DebugJs inspectObject dom)
})
Output (truncated):
[object HTMLCanvasElement]
[ 1/137] ALLOW_KEYBOARD_INPUT : number = 1
[ 2/137] ATTRIBUTE_NODE : number = 2
[ 3/137] CDATA_SECTION_NODE : number = 4
[ 4/137] COMMENT_NODE : number = 8
[ 5/137] DOCUMENT_FRAGMENT_NODE : number = 11
[ 6/137] DOCUMENT_NODE : number = 9
[ 7/137] DOCUMENT_POSITION_CONTAINED_BY : number = 16
[ 8/137] DOCUMENT_POSITION_CONTAINS : number = 8
[ 9/137] DOCUMENT_POSITION_DISCONNECTED : number = 1
[ 10/137] DOCUMENT_POSITION_FOLLOWING : number = 4
[ 11/137] DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC : number = 32
[ 12/137] DOCUMENT_POSITION_PRECEDING : number = 2
[ 13/137] DOCUMENT_TYPE_NODE : number = 10
[ 14/137] ELEMENT_NODE : number = 1
[ 15/137] ENTITY_NODE : number = 6
[ 16/137] ENTITY_REFERENCE_NODE : number = 5
[ 17/137] NOTATION_NODE : number = 12
[ 18/137] PROCESSING_INSTRUCTION_NODE : number = 7
[ 19/137] TEXT_NODE : number = 3
[ 20/137] accessKey : string =
[ 21/137] addEventListener : function = function addEventListener() {
[ 22/137] appendChild : function = function appendChild() {
[ 23/137] attributes : object = [object NamedNodeMap]
[ 24/137] baseURI : object = null
[ 25/137] blur : function = function blur() {
[ 26/137] childElementCount : number = 0
[ 27/137] childNodes : object = [object NodeList]
[ 28/137] children : object = [object HTMLCollection]
[ 29/137] classList : object =
[ 30/137] className : string =
[ 31/137] click : function = function click() {
[ 32/137] clientHeight : number = 0
[ 33/137] clientLeft : number = 0
[ 34/137] clientTop : number = 0
[ 35/137] clientWidth : number = 0
[ 36/137] cloneNode : function = function cloneNode() {
[ 37/137] compareDocumentPosition : function = function compareDocumentPosition() {
[ 38/137] contains : function = function contains() {
[ 39/137] contentEditable : string = inherit
[ 40/137] dataset : object = [object DOMStringMap]
[ 41/137] dir : string =
[ 42/137] dispatchEvent : function = function dispatchEvent() {
[ 43/137] draggable : boolean = false
[ 44/137] firstChild : object = null
[ 45/137] firstElementChild : object = null
[ 46/137] focus : function = function focus() {
[ 47/137] getAttribute : function = function getAttribute() {
[ 48/137] getAttributeNS : function = function getAttributeNS() {
[ 49/137] getAttributeNode : function = function getAttributeNode() {
[ 50/137] getAttributeNodeNS : function = function getAttributeNodeNS() {
[ 51/137] getBoundingClientRect : function = function getBoundingClientRect() {
[ 52/137] getClientRects : function = function getClientRects() {
[ 53/137] getContext : function = function getContext() {
[ 54/137] getElementsByClassName : function = function getElementsByClassName() {
[ 55/137] getElementsByTagName : function = function getElementsByTagName() {
[ 56/137] getElementsByTagNameNS : function = function getElementsByTagNameNS() {
[ 57/137] hasAttribute : function = function hasAttribute() {
[ 58/137] hasAttributeNS : function = function hasAttributeNS() {
[ 59/137] hasAttributes : function = function hasAttributes() {
[ 60/137] hasChildNodes : function = function hasChildNodes() {
[ 61/137] height : number = 150
[ 62/137] hidden : boolean = false
[ 63/137] id : string =
...
It's possible to write test scripts like
- click this
- verify that
- press the Back button
- type name
- press Enter
In case you missed the notice at the top of the file, that functionality is provided in a sister library called Scala Test-State.
See this example for how to write tests for real-world scalajs-react applications.