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
ReactTestUtils
Simulate
andSimulation
Testing props changes
ReactTestVar
Test Scripts
- Fatal React warnings
-
Install PhantomJS.
-
Add the following to SBT:
// scalajs-react test module libraryDependencies += "com.github.japgolly.scalajs-react" %%% "test" % "2.1.1" % Test // React JS itself. // NOTE: Requires react-with-addons.js instead of just react.js jsDependencies += "org.webjars.npm" % "react-dom" % "17.0.2" % Test / "umd/react-dom-test-utils.development.js" minified "umd/react-dom-test-utils.production.min.js" dependsOn "umd/react-dom.development.js" commonJSName "ReactTestUtils"
The main bucket of testing utilities lies in japgolly.scalajs.react.test.ReactTestUtils
.
Half of the methods delegate to React.JS's React.addons.TestUtils
(for which there is a raw facade in japgolly.scalajs.react.test.raw.ReactAddonsTestUtils
if you're interested).
The other half are new functions added specifically in scalajs-react.
- Rendering into DOM with auto-removal
withRendered[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => A): A
withRenderedIntoDocument[M, A](u: Unmounted[M])(f: M => A): A
withRenderedIntoBody[M, A](u: Unmounted[M])(f: M => A): A
withNewBodyElement[A](use: Element => A): A
newBodyElement(): Element
removeNewBodyElement(e: Element): Unit
renderIntoBody[M, A](u: Unmounted[M]): M
- Asynchronously rendering into DOM with auto-removal
withRenderedAsync[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => Future[A]): Future[A]
withRenderedIntoDocumentAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]
withRenderedIntoBodyAsync[M, A](u: Unmounted[M])(f: M => Future[A]): Future[A]
withNewBodyElementAsync[A](use: Element => Future[A]): Future[A]
- Mounted props modification
replaceProps(component, mounted)(newProps: P): mounted'
modifyProps(component, mounted)(f: P => P): mounted'
- Other
removeReactInternals(html: String): String
- Removes internal annotations from HTML that React inserts.
There's only one magic implicit method this time around:
Mounted components get .outerHtmlScrubbed()
which is shorthand for
ReactTestUtils.removeReactInternals(m.getDOMNode.outerHTML)
.
To make event simulation easier, certain event types have dedicated, strongly-typed case classes to wrap event data. For example, JS like
// JavaScript
ReactAddons.TestUtils.Simulate.change(t, {target: {value: "Hi"}})
becomes
// Scala
Simulate.change(t, SimEvent.Change(value = "Hi"))
// Or shorter
SimEvent.Change("Hi") simulate t
Simulate
is from React and imperative.
If you'd like more composability and/or purity there's also Simulation
which
represents action (without a target). It does nothing until .run
is called and a target is provided.
Example:
val a = Simulation.focus
val b = Simulation.change(SimEvent.Change(value = "hi"))
val c = Simulation.blur
val s = a andThen b andThen c
// Or shorter
val s = Simulation.focus >> SimEvent.Change("hi").simulation >> Simulation.blur
// Or even shorter again, using a convenience method
val s = Simulation.focusChangeBlur("hi")
// Then run it when you're ready
s run component
When you want to simulate a parent component re-rendering a child component with different props,
you can test the child directly using ReactTestUtils.{modify,replace}Props
.
Example of code to test:
class CP {
var prev = "none"
def render(p: String) = <.div(s"$prev → $p")
}
val CP = ScalaComponent.builder[String]("asd")
.backend(_ => new CP)
.renderBackend
.componentWillReceiveProps(i => Callback(i.backend.prev = i.currentProps))
.build
Example test case:
ReactTestUtils.withRenderedIntoDocument(CP("start")) { m =>
assert(m.outerHtmlScrubbed(), "<div>none → start</div>")
ReactTestUtils.modifyProps(CP, m)(_ + "ed")
assert(m.outerHtmlScrubbed(), "<div>start → started</div>")
ReactTestUtils.replaceProps(CP, m)("done!")
assert(m.outerHtmlScrubbed(), "<div>started → done!</div>")
}
A ReactTestVar[A]
is a wrapper around a var a: A
that:
- can produce a
StateSnapshot[A]
with or withoutReusability
- can produce a
StateAccess[A]
- retains history when modified
- can perform arbitrary actions when modified
- can be reset
It's useful for testing components that accept StateSnapshot[A]
/StateAccess[A]
instances
in their props.
import utest._
import japgolly.scalajs.react._, vdom.html_<^._
import japgolly.scalajs.react.extra._
import japgolly.scalajs.react.test._
object ExampleTest extends TestSuite {
val NameChanger = ScalaComponent.builder[StateSnapshot[String]]("Name changer")
.render_P { ss =>
def updateName = (event: ReactEventFromInput) => ss.setState(event.target.value)
<.input.text(
^.value := ss.value,
^.onChange ==> updateName)
}
.build
override def tests = TestSuite {
val nameVar = ReactTestVar("guy")
ReactTestUtils.withRenderedIntoDocument(NameChanger(nameVar.stateSnapshot())) { m =>
SimEvent.Change("bob").simulate(m)
assert(nameVar.value() == "bob")
}
}
}
When testing a StateAccess
make sure to feed updates to the ReactTestVar
back into the component
via .forceUpdate
.
val component: ScalaComponent[StateAccessPure[Int], Unit, Unit] = ...
val testVar = ReactTestVar(1)
ReactTestUtils.withRenderedIntoDocument(component(testVar.stateAccess)) { m =>
testVar.onUpdate(m.forceUpdate) // Update the component when it changes the state
assert(m.outerHtmlScrubbed() == "<div>1</div>")
Simulate.click(m.getDOMNode) // our eample component calls .modState(_ + 1) onClick
assert(testVar.value() == 2)
assert(m.outerHtmlScrubbed() == "<div>2</div>")
}
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.
The easiest way to make ReactTestUtils
to turn React warnings into runtime exceptions,
is via a config option.
Alternatively, you can do any of the following...
-
Wrapping a test
import japgolly.scalajs.react.test.ReactTestUtilsConfig ReactTestUtilsConfig.AroundReact.fatalReactWarnings { // test code here }
-
Installing for all
ReactTestUtils
usageimport japgolly.scalajs.react.test.ReactTestUtilsConfig ReactTestUtilsConfig.aroundReact.set( ReactTestUtilsConfig.AroundReact.fatalReactWarnings)
-
Installing outside of test code
import japgolly.scalajs.react.util.ConsoleHijack ConsoleHijack.fatalReactWarnings.install()
-
Wrapping non-test code
import japgolly.scalajs.react.util.ConsoleHijack ConsoleHijack.fatalReactWarnings { // code here }