How to properly (unit) test Om/React components?
Asked Answered
I

1

8

I have developed Om/React components, but I feel really uncomfortable not being able to drive my development with unit tests. I have tried to setup my clojurescript project to run unit tests on those components, and so far reached the point where I am able to write unit tests and instantiate my components. What I am missing is the ability to ensure my components properly react to some events, e.g. onChange so that I can simulate user inputs.

Here is my test code:

(defn simulate-click-event
  "From https://github.com/levand/domina/blob/master/test/cljs/domina/test.cljs"
  [el]
  (let [document (.-document js/window)]
    (cond
     (.-click el) (.click el)
     (.-createEvent document) (let [e (.createEvent document "MouseEvents")]
                                (.initMouseEvent e "click" true true
                                                 js/window 0 0 0 0 0
                                                 false false false false 0 nil)
                                (.dispatchEvent el e))
     :default (throw "Unable to simulate click event"))))

(defn simulate-change-event
  "From https://github.com/levand/domina/blob/master/test/cljs/domina/test.cljs"
  [el]
  (let [document (.-document js/window)]
    (cond
     (.-onChange el) (do (print "firing on change on "  el) (.onChange el))
     (.-createEvent document) (let [e (.createEvent document "HTMLEvents")]
                                (print "firing  " e " on change on "  (.-id el))
                                (.initEvent e "change" true true)
                                (.dispatchEvent el e))
     :default (throw "Unable to simulate change event"))))

(def sink
  "contains a channel that receives messages along with notification type"
  (chan))

;; see http://yobriefca.se/blog/2014/06/04/publish-and-subscribe-with-core-dot-asyncs-pub-and-sub/
(def source
  (pub sink #(:topic %)))

(defn change-field!
  [id value]
  (let [el (sel1 (keyword (str "#" id)))]
     (dommy/set-value! el  value)
     (simulate-change-event el)
     ))

(deftest ^:async password-confirmation
  (testing "do not submit if passwords are not equal"
    (let [subscription (chan)]
      (sub source :user-registration subscription)
      (om/root
       (partial u/registration-view source sink)
       nil
       {:target (sel1 :#view)})

      (go
       (let [m (<! subscription)]
         (is (= :error (:state m)))
         (done)
         ))

      (change-field! "userRequestedEmail"    "[email protected]")
      (change-field! "userRequestedPassword" "secret")
      (change-field! "confirmPassword"       "nosecret")

      (simulate-click-event (sel1 :#submitRegistration))
      )))

This test runs but fails because the change-field! function does not actually change the state of the component. Here is (part of) the code of the component (forgive duplication...):

(defn registration-view
  "Registration form for users.

  Submitting form triggers a request to server"
  [source sink _ owner]
  (reify

    om/IInitState
    (init-state [_]
                {:userRequestedEmail ""
                 :userRequestedPassword ""
                 :confirmPassword ""}
                )

    om/IRenderState
    (render-state
     [this state]
     (dom/fieldset
      nil
      (dom/legend nil "User Registration")
      (dom/div #js { :className "pure-control-group" }

               (dom/label #js { :for "userRequestedEmail" } "EMail")
               (dom/input #js { :id "userRequestedEmail" :type "text" :placeholder "Enter an e-mail"
                                :value (:userRequestedEmail state)
                                :onChange #(om/set-state! owner :userRequestedEmail (.. % -target -value))}))

      (dom/div #js { :className "pure-control-group" }
               (dom/label #js { :for "userRequestedPassword" } "Password")
               (dom/input #js { :id "userRequestedPassword" :type "password" :placeholder "Enter password"
                                :value (:userRequestedPassword state)
                                :onChange #(om/set-state! owner :userRequestedPassword (.. % -target -value))}))

      (dom/div #js { :className "pure-control-group" }
               (dom/label #js { :for "confirmPassword" } "")
               (dom/input #js { :id "confirmPassword" :type "password" :placeholder "Confirm password"
                                :value (:confirmPassword state)
                                :onChange #(om/set-state! owner :confirmPassword (.. % -target -value))}))


      (dom/button #js {:type "submit"
                       :id "submitRegistration"
                       :className "pure-button pure-button-primary"
                       :onClick #(submit-registration state sink)}
                  "Register")))))

What I can see by putting traces in the tests is that the state of the component is not updated when I trigger the change event, although it is correctly triggered. I suspect this has to do with the way Om/React works, wrapping DOM components, but not sure how to deal with this.

Illhumored answered 6/10, 2014 at 5:31 Comment(4)
Just to make sure : is your tested component rendered by om at all (even only 'in memory' ?) Can you confirm the DOM elements are actually created, and the onChange handler is attached ?Doukhobor
Yes. The on click event is triggered, I can see the message going through the core.async channel: That's what submit-registration does, sending the result of a xhrio call to the source channel which then is received by the (go ...) loop inside the test.Illhumored
@Illhumored Maybe different approach will help. I test react components using mochify and I added an example to mochify's wiki page: github.com/mantoni/mochify.js/wiki/…Evalyn
@TJ. Thanks for the pointer. I was thinking along the same way, using React's own testing tools. But this requires probably some work to integrate properly in clojurescript and Om. Will see if I can get back to it...Illhumored
K
1

You can mock events in your components using ReactTestUtils from the react libraries. I'm using mocha and doing something like this to test change events:

var comp = ReactTestUtils.renderIntoDocument(<Component />);
var changingElement = ReactTestUtils.findRenderedDOMComponentWithClass(comp, 'el-class'); 
it ('calls myChangeMethod on change', function() {
  ReactTestUtils.Simulate.change(changingElement);
  assert(comp.myChangeEventMethod.called, true); 
}
Kizzie answered 7/5, 2015 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.