Testing utilities for hyper page handlers.
Provides test-page and test-action for rendering pages and
simulating user interactions in an isolated test context.
Example: (require '[hyper.test :as ht]) (require '[hyper.core :as h])
(defn my-page [req] (let [count* (h/tab-cursor :count 0)] [:div [:h1 "Count: " @count*] [:button {:data-on:click (h/action {:as "increment"} (swap! (h/tab-cursor :count) inc))} "Increment"]]))
(let [result (ht/test-page my-page)] ;; Assert on rendered output (assert (str/includes? (:body-html result) "Count: 0")) ;; Simulate button click and check cursors (let [after (ht/test-action result "increment")] (assert (= 1 (get-in after [:cursors :tab :count])))) ;; Re-render and verify (let [result2 (ht/test-page my-page {:app-state (:app-state result)})] (assert (str/includes? (:body-html result2) "Count: 1"))))
Testing utilities for hyper page handlers.
Provides `test-page` and `test-action` for rendering pages and
simulating user interactions in an isolated test context.
Example:
(require '[hyper.test :as ht])
(require '[hyper.core :as h])
(defn my-page [req]
(let [count* (h/tab-cursor :count 0)]
[:div
[:h1 "Count: " @count*]
[:button {:data-on:click (h/action {:as "increment"}
(swap! (h/tab-cursor :count) inc))}
"Increment"]]))
(let [result (ht/test-page my-page)]
;; Assert on rendered output
(assert (str/includes? (:body-html result) "Count: 0"))
;; Simulate button click and check cursors
(let [after (ht/test-action result "increment")]
(assert (= 1 (get-in after [:cursors :tab :count]))))
;; Re-render and verify
(let [result2 (ht/test-page my-page {:app-state (:app-state result)})]
(assert (str/includes? (:body-html result2) "Count: 1"))))(test-action result action-name)(test-action result action-name client-params)Execute a named action from a test-page result and return a state snapshot.
Looks up the action by its :as name (or raw action-id) in the result's :actions map, executes it with proper request context bindings, and returns a map describing the cursor values after execution.
result: The map returned by test-page. action-name: The :as name (or action-id) of the action to execute. client-params: Optional map of client params (e.g. {:value "hello"}). Simulates $value, $checked, $key, $form-data, etc.
Returns a map:
Throws if the action name is not found in the result.
Example: (let [result (ht/test-page my-page) after (ht/test-action result "increment")] (is (= 1 (get-in after [:cursors :tab :count]))))
;; With client params (simulating $value) (let [result (ht/test-page search-page) after (ht/test-action result "search" {:value "clojure"})] (is (= "clojure" (get-in after [:cursors :tab :query]))))
;; Assert on effects (let [result (ht/test-page my-page) after (ht/test-action result "publish")] (is (seq (get-in after [:effects :scripts]))))
;; Chain into another render (let [r1 (ht/test-page my-page) _ (ht/test-action r1 "increment") r2 (ht/test-page my-page {:app-state (:app-state r1)})] (is (str/includes? (:body-html r2) "Count: 1")))
Execute a named action from a test-page result and return a state snapshot.
Looks up the action by its :as name (or raw action-id) in the result's
:actions map, executes it with proper request context bindings, and
returns a map describing the cursor values after execution.
result: The map returned by test-page.
action-name: The :as name (or action-id) of the action to execute.
client-params: Optional map of client params (e.g. {:value "hello"}).
Simulates $value, $checked, $key, $form-data, etc.
Returns a map:
- :cursors — Cursor values after the action executed:
:global, :session, :tab, :route
- :effects — Effects accumulated during execution. A map with:
:cookies — map of cookie-name to cookie opts
:scripts — vector of JS strings to execute
Effects are collected but NOT applied — this lets tests
assert on what effects *would* happen without actually
setting cookies or sending SSE events.
- :app-state — The app-state atom, for threading into test-page.
Throws if the action name is not found in the result.
Example:
(let [result (ht/test-page my-page)
after (ht/test-action result "increment")]
(is (= 1 (get-in after [:cursors :tab :count]))))
;; With client params (simulating $value)
(let [result (ht/test-page search-page)
after (ht/test-action result "search" {:value "clojure"})]
(is (= "clojure" (get-in after [:cursors :tab :query]))))
;; Assert on effects
(let [result (ht/test-page my-page)
after (ht/test-action result "publish")]
(is (seq (get-in after [:effects :scripts]))))
;; Chain into another render
(let [r1 (ht/test-page my-page)
_ (ht/test-action r1 "increment")
r2 (ht/test-page my-page {:app-state (:app-state r1)})]
(is (str/includes? (:body-html r2) "Count: 1")))(test-page handler)(test-page handler opts)Render a page handler in an isolated test context and return a result map.
handler: A page render function (fn [req] -> hiccup).
opts (optional map):
h/navigate (during render) and
effects/navigate! (during actions, via test-action)
resolve named routes reliably — no need to spin up a full
create-handler. The router and flattened routes are stored
in app-state and threaded into the request as :hyper/router,
and they persist across re-renders that share :app-state.
Example: {:routes [["/" {:name :home :get home-fn}]
["/about" {:name :about :get about-fn}]]}.Returns a map:
If the handler returns a Ring response map (a map with :status), it is returned as-is without wrapping.
Render a page handler in an isolated test context and return a result map.
handler: A page render function (fn [req] -> hiccup).
opts (optional map):
- :app-state — Atom for application state. Pass the :app-state from a
previous test-page call to preserve state across renders.
Default: fresh atom with init-state.
- :cursors — Initial cursor values to seed before rendering. A map with
optional keys :global, :session, :tab, each a map of data
that is merged into the corresponding cursor scope.
Example: {:tab {:count 5} :session {:user "alice"}}
- :session-id — Session ID string. Default: "test-session".
- :tab-id — Tab ID string. Default: "test-tab".
- :route — Route info map {:name :path :path-params :query-params}.
Default: {:name :test-page :path "/" :path-params {} :query-params {}}.
- :routes — A reitit routes vector (or a Var holding one) used to build
a bound router so that `h/navigate` (during render) and
`effects/navigate!` (during actions, via `test-action`)
resolve named routes reliably — no need to spin up a full
`create-handler`. The router and flattened routes are stored
in app-state and threaded into the request as :hyper/router,
and they persist across re-renders that share :app-state.
Example: {:routes [["/" {:name :home :get home-fn}]
["/about" {:name :about :get about-fn}]]}.
- :router — A pre-built reitit router, used as-is for name matching.
Escape hatch for advanced cases; prefer :routes. When both
are given, :router matches and :routes provides metadata.
- :req — Extra keys to merge into the request map passed to handler.
- :render-middleware — Vector of middleware fns to wrap the handler.
Each is (fn [handler] (fn [req] ...)), identical to Ring
middleware. Applied in order (first = outermost).
Returns a map:
- :body — Raw hiccup returned by the handler (before HTML serialization).
- :body-html — Serialized HTML string of the body.
- :title — Resolved page title string, or nil.
- :url — Current route URL string.
- :signals — Map of signal declarations from the render, keyed by
the path used to create the signal (e.g. :user-name,
[:user :name]). Each value has :default-val and :local?.
- :actions — Map of actions registered during render. Actions with an
:as name are keyed by that name; others by their action-id.
Each value has :fn which can be called as ((:fn action) client-params).
- :cursors — Snapshot of cursor values after render:
:global — global cursor state map
:session — this session's cursor data map
:tab — this tab's cursor data map
:route — this tab's route info
- :watches — Vector of external sources registered via h/watch!.
- :app-state — The app-state atom, for threading into subsequent calls.
If the handler returns a Ring response map (a map with :status), it is
returned as-is without wrapping.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |