Sunday, 20 March 2016

Testing Scala.JS on Firefox & Chrome from SBT

It's been fantastic being able to write Scala and compile it to JavaScript thanks to Scala.JS. Going further, Scala.JS lets you write accompanying unit tests, run them from SBT, and choose a target environment from {Rhino, Node.JS, Phantom.JS}. If you needed DOM in your testing, your only option was Phantom.JS which seems great at first but—because it only simulates DOM—you soon discover that there are many cases in which its behaviour diverges from normal browsers (eg. DOM types for <td> tags), or isn't supported at all (text selection, anything to do with focus, more). Oh, and it's also riddled with bugs, many significant and long-standing. So while Phantom.JS's effort is appreciated, it's no substitute for a real browser. This was where the story ended until very recently.

Recently, the Scala.JS team released scala-js-env-selenium which allows you to use Selenium in the same way you would the other JS environments. That means that Scala.JS can now interface with real browsers, namely Firefox and Chrome. Awesome!

The next step, and the purpose of this blog post, is to effectively integrate it into your SBT/Scala/Scala.JS project. Let me tell you about my ideal environment and then I'll show you how to achieve it.

Goals

In my ideal environment:
  • I write my Scala.JS unit tests the same way I already do, and I can continue to run them against Node.JS or Phantom.JS because it's fast. No changes there.
  • To run tests against Firefox or Chrome, I simply prefix my SBT command with firefox: or chrome:.
    For example, firefox:test would run tests in Firefox, chrome:testOnly a.b.c would run my a.b.c test in Chrome.
  • Testing in FF/Chrome doesn't require recompilation (All in the Land know Scalac is slow). It should use all of the same bits and bobs (especially the output JS) that normal tests use.
  • I specify that certain tests will only run in FF and/or Chrome.
    For example, tests that use focus should skip Phantom.JS where I know focus doesn't work.
  • I run testAll to run the same tests in all environments (fast-env & Firefox & Chrome). This will happen concurrently.
  • I can use FF/Chrome headlessly (i.e. without the windows popping up when the browsers are launched and running).

1. Selenium support.

Add this to your project/plugins.sbt:
libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "0.1.1"
Then install ChromeDriver which is needed so that Selenium can interface with Chrome.

2. General SBT Config

Create a file called project/InBrowserTesting.scala with the following content: This creates SBT configurations for each browser, then delegates most of the settings to the test:* settings.

3. Project-specific SBT config

Next you need to look at your existing SBT build settings.

For each cross-JVM/JS project, add:
  .configure(InBrowserTesting.cross)
For each JS-only project, add:
  .configure(InBrowserTesting.js)
For each JVM-only project, add:
  .configure(InBrowserTesting.jvm)

You might be wondering why JVM projects need any configuration at all. It's so that when testAll is run from a JVM project or the root project, you want it to run the JVM tests. Without this setting, testAll in a JVM project would do nothing at all, and testAll from the root would only run the JS tests.

4. Environment-Dependent Tests

I often forget that I'm writing JS when I write Scala.JS. As we're in JS land, to determine our environment all we have to do is check the user-agent. How you skip tests depends on the test framework you're using but all you have to do is put something like this in your test code:
  if (JsEnvUtils.isRealBrowser) {
    // test here
  } else {
    // skip
  }

5. Headlessness

Super simple (unless you're a Windows user). Install xvfb, "X Virtual FrameBuffer", which starts X without a graphics display.

Here are two different ways you can use it:
  1. Either start it in a separate window via
    Xvfb :1
    
    or in the background
    nohup Xvfb :1 &
    
    then launch SBT like this:
    DISPLAY=:1 sbt
    
  2. This tip comes from danielkza (thanks!). You can simply prepend your SBT command with xvfb-run -a to have an X server spun up on demand without the need to start it yourself. Beware though, that xvfb-run is a bit naive and susceptible to race conditions so while it'll be fine on your local machine, it may cause you problems on your CI server or similar.
You'll no longer see any Firefox and Chrome windows; all your output just appears in the SBT console as usual. Too easy.

Note: The :1 indicates the X display-number, which is a means to uniquely identify an X server on a host. The 1 is completely arbitrary—you can choose any number so long as there isn't another X server running that's already associated with it.

Done.

All goals described are thus achieved.
I hope you found this helpful.
Happy coding!

4 comments:

  1. Xvfb includes the xvfb-run utility that can start the server only for the runtime of a particular executable (with the correct environment variables) and clean it up automatically. Example: `xvfb-run -a sbt firefox:test`.

    ReplyDelete
    Replies
    1. Oh that's cool. Thanks! I'll update the post.

      Delete