Skip to content

re-frame.test-helpers

Helpers for testing views without a browser. A view returns hiccup, and these pure functions walk that data: they find nodes by :data-testid or any other attribute, read their text, and call the event handlers attached to them. That catches what state assertions miss: a view that reads the wrong path or formats a value wrongly, or a button wired to dispatch into the wrong frame.

(:require [re-frame.core :as rf]
          [re-frame.test-helpers :as th])
(defn counter-view [{:keys [n]}]
  [:span (th/testid "counter-label") "Count: " n])

(let [tree  (counter-view {:n 5})
      label (th/find-by-testid tree "counter-label")]
  (th/text-content label))
;; => "Count: 5"

The view above needs no frame because its value comes from props. In a test, assert on the returned string with your test runner's equality assertion.

Everything here, the connected view test included, runs on the JVM with no DOM, no React and no act(). It needs a view you can call as a function, as a Reagent view is. A UIx defui that calls use-sub or use-frame only runs inside React's render, so mount it instead, as in Test a view §4. Its companion re-frame.test-support holds the fixtures that reset the runtime between tests; a test that checks both state and views requires both. To assert on rendered HTML markup rather than on structure or handlers, use render-to-string from re-frame.ssr. Test a view walks through a complete view test.

Reading hiccup nodes

attrs

  • Kind: function
  • Signature:
    (attrs node) → map | nil
    
  • Description: Returns the attrs map of a hiccup node, or nil when it has none.
  • Example:
    (th/attrs [:div {:k 1} "child"])   ; => {:k 1}
    (th/attrs [:div "child"])          ; => nil
    

children

  • Kind: function
  • Signature:
    (children node) → vector | nil
    
  • Description: Returns everything after the tag and the optional attrs map. The result is always a vector, empty when the node has no children. Non-vector input returns nil.
  • Example:
    (th/children [:div {:k 1} "a" "b"])  ; => ["a" "b"]
    (th/children [:div "a" "b"])         ; => ["a" "b"]
    

text-content

  • Kind: function
  • Signature:
    (text-content node) → string
    
  • Description: Returns the text under node: every string leaf, with nested components expanded, joined into one string. Numbers become strings and nils are skipped. With no text, the result is "".
    • This walks data without a browser: it does not insert spaces between elements or account for CSS visibility. Use keyword tags (:div); a string tag ("div") is itself a string in the walk and contributes to the result.
  • Example:
    (th/text-content [:div "Count: " [:b 5]])  ; => "Count: 5"
    

extract-handler

  • Kind: function
  • Signature:
    (extract-handler node event-key) → value or nil
    
  • Description: Returns the value under event-key in node's attrs map, or nil. Equivalent to (get (attrs node) event-key).
  • Example:
    (let [btn (th/find-by-testid tree "counter-inc")]
      (th/extract-handler btn :on-click))   ; => the handler fn, or nil
    

Finding nodes by attribute

These walk the whole tree, expanding components as they go, and work with any attribute keyword: :data-testid, :id, :data-test, or your own.

find-by-attr

  • Kind: function
  • Signature:
    (find-by-attr tree attr val) → node | nil
    
  • Description: Returns the first node whose attrs map has attr equal to val, or nil when nothing matches. A nil val matches any node without attr, leaves included, so looking up an unset test id returns the root or another unrelated node instead of nil.
  • Example:
    (th/find-by-attr tree :data-test "submit")
    (th/find-by-attr tree :id        "login")
    

find-all-by-attr

  • Kind: function
  • Signature:
    (find-all-by-attr tree attr val) → vector
    
  • Description: Returns every node whose attrs map has attr equal to val, in depth-first order, or an empty vector when nothing matches.
  • Example:
    (th/find-all-by-attr tree :data-test "row")  ; => every matching node
    

find-by-attr-prefix

  • Kind: function
  • Signature:
    (find-by-attr-prefix tree attr prefix) → vector
    
  • Description: Returns every node whose attr value is a string starting with prefix, or an empty vector when nothing matches. Non-string values never match.
  • Example:
    ;; Matches "row-1", "row-2", …
    (th/find-by-attr-prefix tree :data-test "row-")
    

Finding nodes by testid

The same three searches, keyed on :data-testid.

find-by-testid

  • Kind: function
  • Signature:
    (find-by-testid tree test-id) → node | nil
    
  • Description: Returns the first node whose :data-testid is test-id, or nil. Equivalent to (find-by-attr tree :data-testid test-id).
  • Example:
    (th/find-by-testid tree "counter-inc")  ; => the first matching node, or nil
    

find-all-by-testid

  • Kind: function
  • Signature:
    (find-all-by-testid tree test-id) → vector
    
  • Description: Returns every node whose :data-testid is test-id, in depth-first order. Equivalent to (find-all-by-attr tree :data-testid test-id).
  • Example:
    (th/find-all-by-testid tree "cart-row")  ; => vector of every match
    

find-by-testid-prefix

  • Kind: function
  • Signature:
    (find-by-testid-prefix tree prefix) → vector
    
  • Description: Returns every node whose :data-testid starts with prefix. Equivalent to (find-by-attr-prefix tree :data-testid prefix).
  • Example:
    ;; Matches "item-1", "item-2", …
    (th/find-by-testid-prefix tree "item-")
    

Driving handlers

invoke-handler

  • Kind: function
  • Signature:
    (invoke-handler node event-key & args) → any
    
  • Description: Calls the handler under event-key on node with args and returns its value. Use it to click a button or change an input in a test.
    • The handler must be a function. A declarative event vector is not invoked here; use Fresco's testing helpers for Fresco event attributes. Supply any event argument the callback reads, and expect exceptions from the callback to propagate.
    • An ordinary dispatch inside the handler only queues the event, so app-db has not changed yet when invoke-handler returns. Wait for the result with re-frame.test-support/poll-until, in the same fixture-owned frame the click dispatched into. A handler that calls dispatch-sync drains in place.
    • On CLJS, do not wrap the click and the wait in rf/with-new-frame: poll-until returns a Promise at once, so the body returns and destroys the frame before the queued event drains. On the JVM, poll-until blocks inside the body, so the frame outlives the wait.
  • Errors: invoke-handler throws, because a missing handler is usually the bug under test:
    • :rf.error/invoke-handler-bad-node: node is not a hiccup vector. A find-by-testid that matched nothing returns nil, which lands here.
    • :rf.error/invoke-handler-missing: there is no handler fn under event-key, including when the node has no attrs map.
  • Example:
    (let [btn (th/find-by-testid tree "counter-inc")]
      (th/invoke-handler btn :on-click))   ; calls the attached :on-click
    

Authoring testids

testid

  • Kind: function
  • Signature:
    (testid id) → map
    (testid id extra) → map
    
  • Description: Returns an attrs map carrying :data-testid id, for use in a view; find the node again with find-by-testid. The 2-arity merges extra into the map, and :data-testid always wins on collision.
    • In a rf/reg-view body, write the callback with the dispatch that reg-view provides, as below; the closure captures it. A bare rf/dispatch in the callback runs after the render scope has unwound, finds no frame in scope (there is no fallback to :rf/default), and raises :rf.error/no-frame-context.
  • Example:
    (rf/reg-view counter-inc-button []
      [:button (th/testid "counter-inc" {:on-click #(dispatch [:counter/inc])})
       "+"])
    ;; the button node => [:button {:data-testid "counter-inc" :on-click ...} "+"]
    

Tree expansion

expand-tree

  • Kind: function
  • Signature:
    (expand-tree tree) → tree
    
  • Description: Expands every component in a hiccup tree by calling it with its args, as Reagent's renderer would: function components, Form-2 components (a function returning the render function) and Form-3 class components. Afterwards, every vector starts with a keyword tag or a non-component value.
    • The find-* functions and text-content already expand as they walk. Call expand-tree yourself only to re-expand a sub-tree mid-walk.
    • A Form-3 class expands by calling its stashed :reagent-render function directly. No React component is created and no lifecycle methods run.
    • Form-3 detection looks for the tag that reagent-slim's create-class sets, so a class from stock Reagent's create-class is not recognised. On the JVM there are no classes to detect.
  • Example:
    (th/expand-tree [parent-view {:n 5}])  ; => hiccup whose vectors all start
                                           ;    with keyword tags
    

A connected view test

A view that subscribes or dispatches needs a frame in scope. Use make-reset-runtime-fixture with an :adapter, which makes :rf/default the ambient frame, then call the view and walk the tree it returns. After a plain dispatch, including one fired by invoke-handler, wait with poll-until. On CLJS the fixture takes :async? true, and the test composes poll-until's Promise under (async done …). Test a view walks through a complete test.

See also

  • re-frame.core: dispatch-sync, with-new-frame, make-frame, app-db-value and compute-sub, the production functions these tests drive.