Om, Clojurescript, and Testing

Posted by on Thu 16 July 2015

This past week I started learning React, Om, and Clojurescript all at once. When beginning to use cemerick's .clojurescript.test, I kept running into this error:

1
2
3
4
5
6
7
Error: cemerick is undefined

ERROR: cemerick.cljs.test was not required.

You can resolve this issue by ensuring [cemerick.cljs.test] appears
in the :require clause of your test suite namespaces.
Also make sure that your build has actually included any test files.

But I clearly had included it in my test! I googled and grumbled, but could not figure out what was wrong. Finally I discovered that slimerjs has the -jsconsole flag, which, as the docs say, will

1
Open a window to view all javascript errors during the execution

Great, using that I finally found the actual problem:

1
2
3
4
Script Error: Error: Assert failed: No target specified to om.core/root
(not (nil? target))
       Stack:
         -> file:///tmp/runner6386761518784950059.js.html: 55456

This makes much more sense. The issue is that my core.cljs namespace was running om/root when the page loads. The code looked like:

1
2
3
(om/root main-view
         app-state
         {:target (. js/document (getElementById "app"))}))

But since the tests are not loading the index.html page (as they shouldn't), there is no element with ID app. Ultimately the problem is with running code at the namespace level. What would be preferred would be if there were some way to specify a main function to initialize the app. This would be run for the actual application, but not the tests.

First take at a Solution

It took awhile of searching, but I finally found some inspiration from this project and specifically this line of code:

1
<script type="text/javascript">goog.require("react_tutorial_om.app");</script>

I realized I could just wrap my om/root call in a main function and then call this from the index.html page. Here is what the code in core.cljs looks like now:

1
2
3
4
(defn app []
  (om/root main-view
           app-state
           {:target (. js/document (getElementById "app"))}))

and the corresponding code in index.html:

1
2
3
4
<script type="text/javascript">
goog.require("my.namespace.core");
my.namespace.core.app();
</script>

Now running the tests no longer had any problem. However, I realized that lein figwheel was not reloading the page properly when I made changes to the code. This is because the javascript would be reloaded, which previously was running om/root every time. To solve this I added to the on-js-reload function so that the app was reinitialized:

1
2
(defn on-js-reload []
  (app))

Improving the Solution

As I continued learning about Om, I came across the om-cookbook repository. The following is based on the structure of the project in the recipes/routing-with-secretary directory (and possibly others in the repo).

Let's assume your project currently has this directory structure:

resources/...
src/my/namespace/core.cljs
project.clj

We are going to add a directory called env which will house code that is specific to different environments, namely development vs. production. Create directories such that your project now looks like this:

env/dev/src/my/namespace/dev.cljs
   /prod/src/my/namespace/prod.cljs
resources/...
src/my/namespace/core.cljs
project.clj

You can see that in env/dev and env/prod we mimick the src directory. Within dev.cljs we will add code that is only to be run when developing. Here is what that namespace will basically look like for the dev environment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(ns my.namespace.dev
  (:require [my.namespace.core :as core]
            [figwheel.client :as figwheel :include-macros true]))

(enable-console-print!)

(defn on-js-reload []
  (core/app))

(core/app)

For production, this can be much simpler:

1
2
3
4
(ns my.namespace.prod
  (:require [my.namespace.core :as core]))

(core/app)

Now all we need to do is modify project.clj to use these environments. This is accomplished using different build configurations. Here is a sample of how that would look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
:cljsbuild {:builds [{:id "dev"
                      :source-paths ["src" "env/dev/src"]
                      ; blah blah blah
                      }
                     {:id "prod"
                      :source-paths ["src" "env/prod/src"]
                      ; blah blah blah
                      }
                     {:id "test"
                      :source-paths ["src" "test"]
                      ; blah blah blah
                      }]}

Finally, make sure to remove the code that was added to index.html in our first take at a solution.

And there you have it. The dev environment will end up compiling dev.cljs, and since this namespace includes a call to core/app at the namespace-level, it will run when the javascript is loaded. We do not include the file for the test build, meaning the tests do not try to run om/root.

Alternative Approach

An entirely different approach to all of this (and perhaps a lot simpler) would be to simply check if your target element exists. Your code could then look like

1
2
3
4
(if-let [target (. js/document (getElementById "app"))]
  (om/root main-view
           app-state
           {:target target}))

You might prefer this approach. The reason I tend to not like this is that you still have functionality that executes when you require the namespace. It also introduces a silent failure. If you change the main element to have a different id, your app would just show up blank with no errors printed.

I was pretty surprised to not be able to find anything about this. Maybe I'm missing something obvious. I am new to Clojurescript and Om, so it could just be a newbie mistake. If so let me know!

tags:


Comments !