627 lines
26 KiB
Clojure
627 lines
26 KiB
Clojure
(ns tui.examples-test
|
|
"Unit tests derived directly from example code patterns.
|
|
Tests update functions, view functions, and helper functions from examples."
|
|
(:require [clojure.test :refer [deftest testing is are]]
|
|
[clojure.string :as str]
|
|
[tui.core :as tui]
|
|
[tui.events :as ev]
|
|
[tui.render :as render]))
|
|
|
|
;; =============================================================================
|
|
;; COUNTER EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest counter-initial-model-test
|
|
(testing "counter initial model structure"
|
|
(let [initial-model {:count 0}]
|
|
(is (= 0 (:count initial-model)))
|
|
(is (map? initial-model)))))
|
|
|
|
(deftest counter-update-all-keys-test
|
|
(testing "counter responds to all documented keys"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(cond
|
|
(or (ev/key= event \q)
|
|
(ev/key= event \c #{:ctrl}))
|
|
{:model model :events [(ev/quit)]}
|
|
|
|
(or (ev/key= event :up)
|
|
(ev/key= event \k))
|
|
{:model (update model :count inc)}
|
|
|
|
(or (ev/key= event :down)
|
|
(ev/key= event \j))
|
|
{:model (update model :count dec)}
|
|
|
|
(ev/key= event \r)
|
|
{:model (assoc model :count 0)}
|
|
|
|
:else
|
|
{:model model}))]
|
|
|
|
;; All increment keys
|
|
(are [event] (= 1 (:count (:model (update-fn {:model {:count 0} :event event}))))
|
|
{:type :key :key :up}
|
|
{:type :key :key \k})
|
|
|
|
;; All decrement keys
|
|
(are [event] (= -1 (:count (:model (update-fn {:model {:count 0} :event event}))))
|
|
{:type :key :key :down}
|
|
{:type :key :key \j})
|
|
|
|
;; All quit keys
|
|
(are [event] (= [(ev/quit)] (:events (update-fn {:model {:count 0} :event event})))
|
|
{:type :key :key \q}
|
|
{:type :key :key \c :modifiers #{:ctrl}})
|
|
|
|
;; Reset key
|
|
(is (= 0 (:count (:model (update-fn {:model {:count 42} :event {:type :key :key \r}}))))))))
|
|
|
|
(deftest counter-view-color-logic-test
|
|
(testing "counter view shows correct colors based on count"
|
|
(let [get-color (fn [count]
|
|
(cond
|
|
(pos? count) :green
|
|
(neg? count) :red
|
|
:else :default))]
|
|
|
|
(is (= :green (get-color 5)))
|
|
(is (= :green (get-color 1)))
|
|
(is (= :red (get-color -1)))
|
|
(is (= :red (get-color -100)))
|
|
(is (= :default (get-color 0))))))
|
|
|
|
(deftest counter-view-structure-test
|
|
(testing "counter view produces valid hiccup"
|
|
(let [view (fn [{:keys [count]}]
|
|
[:col {:gap 1}
|
|
[:box {:border :rounded :padding [0 1]}
|
|
[:col
|
|
[:text {:bold true} "Counter"]
|
|
[:text ""]
|
|
[:text {:fg (cond
|
|
(pos? count) :green
|
|
(neg? count) :red
|
|
:else :default)}
|
|
(str "Count: " count)]]]
|
|
[:text {:fg :gray} "j/k or up/down: change value"]])]
|
|
|
|
;; View returns valid hiccup
|
|
(let [result (view {:count 5})]
|
|
(is (vector? result))
|
|
(is (= :col (first result))))
|
|
|
|
;; View renders without error
|
|
(is (string? (render/render (view {:count 5}))))
|
|
(is (string? (render/render (view {:count -3}))))
|
|
(is (string? (render/render (view {:count 0})))))))
|
|
|
|
;; =============================================================================
|
|
;; TIMER EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest timer-initial-model-test
|
|
(testing "timer initial model structure"
|
|
(let [initial-model {:seconds 10 :running true :done false}]
|
|
(is (= 10 (:seconds initial-model)))
|
|
(is (true? (:running initial-model)))
|
|
(is (false? (:done initial-model))))))
|
|
|
|
(deftest timer-format-time-test
|
|
(testing "format-time produces MM:SS format"
|
|
(let [format-time (fn [seconds]
|
|
(let [mins (quot seconds 60)
|
|
secs (mod seconds 60)]
|
|
(format "%02d:%02d" mins secs)))]
|
|
|
|
(is (= "00:00" (format-time 0)))
|
|
(is (= "00:01" (format-time 1)))
|
|
(is (= "00:10" (format-time 10)))
|
|
(is (= "00:59" (format-time 59)))
|
|
(is (= "01:00" (format-time 60)))
|
|
(is (= "01:30" (format-time 90)))
|
|
(is (= "05:00" (format-time 300)))
|
|
(is (= "10:00" (format-time 600)))
|
|
(is (= "59:59" (format-time 3599))))))
|
|
|
|
(deftest timer-tick-countdown-test
|
|
(testing "timer tick decrements and reaches zero"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (= (:type event) :timer-tick)
|
|
(if (:running model)
|
|
(let [new-seconds (dec (:seconds model))]
|
|
(if (<= new-seconds 0)
|
|
{:model (assoc model :seconds 0 :done true :running false)}
|
|
{:model (assoc model :seconds new-seconds)
|
|
:events [(ev/delayed-event 1000 {:type :timer-tick})]}))
|
|
{:model model})
|
|
{:model model}))]
|
|
|
|
;; Normal tick
|
|
(let [result (update-fn {:model {:seconds 10 :running true :done false}
|
|
:event {:type :timer-tick}})]
|
|
(is (= 9 (:seconds (:model result))))
|
|
(is (= 1 (count (:events result)))))
|
|
|
|
;; Tick to zero
|
|
(let [result (update-fn {:model {:seconds 1 :running true :done false}
|
|
:event {:type :timer-tick}})]
|
|
(is (= 0 (:seconds (:model result))))
|
|
(is (true? (:done (:model result))))
|
|
(is (false? (:running (:model result))))
|
|
(is (nil? (:events result))))
|
|
|
|
;; Tick when paused does nothing
|
|
(let [result (update-fn {:model {:seconds 5 :running false :done false}
|
|
:event {:type :timer-tick}})]
|
|
(is (= 5 (:seconds (:model result))))
|
|
(is (nil? (:events result)))))))
|
|
|
|
(deftest timer-pause-resume-test
|
|
(testing "timer pause/resume with space key"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event \space)
|
|
(let [new-running (not (:running model))]
|
|
{:model (assoc model :running new-running)
|
|
:events (when new-running [(ev/delayed-event 1000 {:type :timer-tick})])})
|
|
{:model model}))]
|
|
|
|
;; Pause (running -> not running)
|
|
(let [result (update-fn {:model {:seconds 5 :running true}
|
|
:event {:type :key :key \space}})]
|
|
(is (false? (:running (:model result))))
|
|
(is (nil? (:events result))))
|
|
|
|
;; Resume (not running -> running)
|
|
(let [result (update-fn {:model {:seconds 5 :running false}
|
|
:event {:type :key :key \space}})]
|
|
(is (true? (:running (:model result))))
|
|
(is (= 1 (count (:events result))))))))
|
|
|
|
(deftest timer-reset-test
|
|
(testing "timer reset restores initial state"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event \r)
|
|
{:model (assoc model :seconds 10 :done false :running true)
|
|
:events [(ev/delayed-event 1000 {:type :timer-tick})]}
|
|
{:model model}))]
|
|
|
|
(let [result (update-fn {:model {:seconds 0 :done true :running false}
|
|
:event {:type :key :key \r}})]
|
|
(is (= 10 (:seconds (:model result))))
|
|
(is (false? (:done (:model result))))
|
|
(is (true? (:running (:model result))))
|
|
(is (= 1 (count (:events result))))))))
|
|
|
|
(deftest timer-view-color-logic-test
|
|
(testing "timer view shows correct colors"
|
|
(let [get-color (fn [done seconds]
|
|
(cond
|
|
done :green
|
|
(< seconds 5) :red
|
|
:else :cyan))]
|
|
|
|
(is (= :green (get-color true 0)))
|
|
(is (= :red (get-color false 4)))
|
|
(is (= :red (get-color false 1)))
|
|
(is (= :cyan (get-color false 5)))
|
|
(is (= :cyan (get-color false 10))))))
|
|
|
|
;; =============================================================================
|
|
;; SPINNER EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(def spinner-styles
|
|
{:dots ["⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏"]
|
|
:line ["|" "/" "-" "\\"]
|
|
:circle ["◐" "◓" "◑" "◒"]
|
|
:square ["◰" "◳" "◲" "◱"]
|
|
:triangle ["◢" "◣" "◤" "◥"]
|
|
:bounce ["⠁" "⠂" "⠄" "⠂"]
|
|
:dots2 ["⣾" "⣽" "⣻" "⢿" "⡿" "⣟" "⣯" "⣷"]
|
|
:arc ["◜" "◠" "◝" "◞" "◡" "◟"]
|
|
:moon ["🌑" "🌒" "🌓" "🌔" "🌕" "🌖" "🌗" "🌘"]})
|
|
|
|
(deftest spinner-frame-cycling-test
|
|
(testing "spinner frame cycles through all frames"
|
|
(let [spinner-view (fn [frame style]
|
|
(let [frames (get spinner-styles style)
|
|
idx (mod frame (count frames))]
|
|
(nth frames idx)))]
|
|
|
|
;; Dots style has 10 frames
|
|
(is (= "⠋" (spinner-view 0 :dots)))
|
|
(is (= "⠙" (spinner-view 1 :dots)))
|
|
(is (= "⠋" (spinner-view 10 :dots))) ; Wraps around
|
|
(is (= "⠙" (spinner-view 11 :dots)))
|
|
|
|
;; Line style has 4 frames
|
|
(is (= "|" (spinner-view 0 :line)))
|
|
(is (= "/" (spinner-view 1 :line)))
|
|
(is (= "|" (spinner-view 4 :line))) ; Wraps around
|
|
|
|
;; Circle style
|
|
(is (= "◐" (spinner-view 0 :circle)))
|
|
(is (= "◐" (spinner-view 4 :circle)))))) ; Wraps around after 4 frames
|
|
|
|
(deftest spinner-tick-advances-frame-test
|
|
(testing "spinner tick advances frame when loading"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (= (:type event) :spinner-frame)
|
|
(if (:loading model)
|
|
{:model (update model :frame inc)
|
|
:events [(ev/delayed-event 80 {:type :spinner-frame})]}
|
|
{:model model})
|
|
{:model model}))]
|
|
|
|
;; Tick advances frame when loading
|
|
(let [result (update-fn {:model {:frame 0 :loading true}
|
|
:event {:type :spinner-frame}})]
|
|
(is (= 1 (:frame (:model result))))
|
|
(is (= 1 (count (:events result)))))
|
|
|
|
;; Tick does nothing when not loading
|
|
(let [result (update-fn {:model {:frame 5 :loading false}
|
|
:event {:type :spinner-frame}})]
|
|
(is (= 5 (:frame (:model result))))
|
|
(is (nil? (:events result)))))))
|
|
|
|
(deftest spinner-style-switching-test
|
|
(testing "spinner tab key cycles through styles"
|
|
(let [styles (vec (keys spinner-styles))
|
|
update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event :tab)
|
|
(let [new-idx (mod (inc (:style-idx model)) (count styles))]
|
|
{:model (assoc model
|
|
:style-idx new-idx
|
|
:style (nth styles new-idx))})
|
|
{:model model}))]
|
|
|
|
;; Tab advances style
|
|
(let [result (update-fn {:model {:style-idx 0 :style (first styles)}
|
|
:event {:type :key :key :tab}})]
|
|
(is (= 1 (:style-idx (:model result)))))
|
|
|
|
;; Tab wraps around
|
|
(let [last-idx (dec (count styles))
|
|
result (update-fn {:model {:style-idx last-idx :style (last styles)}
|
|
:event {:type :key :key :tab}})]
|
|
(is (= 0 (:style-idx (:model result))))))))
|
|
|
|
(deftest spinner-completion-test
|
|
(testing "spinner space key completes loading"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event \space)
|
|
{:model (assoc model :loading false :message "Done!")}
|
|
{:model model}))]
|
|
|
|
(let [result (update-fn {:model {:loading true :message "Loading..."}
|
|
:event {:type :key :key \space}})]
|
|
(is (false? (:loading (:model result))))
|
|
(is (= "Done!" (:message (:model result))))))))
|
|
|
|
(deftest spinner-restart-test
|
|
(testing "spinner r key restarts animation"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event \r)
|
|
{:model (assoc model :loading true :frame 0 :message "Loading...")
|
|
:events [(ev/delayed-event 80 {:type :spinner-frame})]}
|
|
{:model model}))]
|
|
|
|
(let [result (update-fn {:model {:loading false :frame 100 :message "Done!"}
|
|
:event {:type :key :key \r}})]
|
|
(is (true? (:loading (:model result))))
|
|
(is (= 0 (:frame (:model result))))
|
|
(is (= "Loading..." (:message (:model result))))
|
|
(is (= 1 (count (:events result))))))))
|
|
|
|
;; =============================================================================
|
|
;; LIST SELECTION EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest list-selection-initial-model-test
|
|
(testing "list selection initial model structure"
|
|
(let [initial-model {:cursor 0
|
|
:items ["Pizza" "Sushi" "Tacos" "Burger" "Pasta"]
|
|
:selected #{}
|
|
:submitted false}]
|
|
(is (= 0 (:cursor initial-model)))
|
|
(is (= 5 (count (:items initial-model))))
|
|
(is (empty? (:selected initial-model)))
|
|
(is (false? (:submitted initial-model))))))
|
|
|
|
(deftest list-selection-cursor-navigation-test
|
|
(testing "cursor navigation respects bounds"
|
|
(let [items ["A" "B" "C" "D" "E"]
|
|
update-fn (fn [{:keys [model event]}]
|
|
(cond
|
|
(or (ev/key= event :up)
|
|
(ev/key= event \k))
|
|
{:model (update model :cursor #(max 0 (dec %)))}
|
|
|
|
(or (ev/key= event :down)
|
|
(ev/key= event \j))
|
|
{:model (update model :cursor #(min (dec (count items)) (inc %)))}
|
|
|
|
:else {:model model}))]
|
|
|
|
;; Move down through list
|
|
(let [m0 {:cursor 0}
|
|
m1 (:model (update-fn {:model m0 :event {:type :key :key :down}}))
|
|
m2 (:model (update-fn {:model m1 :event {:type :key :key :down}}))
|
|
m3 (:model (update-fn {:model m2 :event {:type :key :key :down}}))
|
|
m4 (:model (update-fn {:model m3 :event {:type :key :key :down}}))
|
|
m5 (:model (update-fn {:model m4 :event {:type :key :key :down}}))]
|
|
(is (= 1 (:cursor m1)))
|
|
(is (= 2 (:cursor m2)))
|
|
(is (= 3 (:cursor m3)))
|
|
(is (= 4 (:cursor m4)))
|
|
(is (= 4 (:cursor m5)))) ; Clamped at max
|
|
|
|
;; Move up from top
|
|
(let [result (update-fn {:model {:cursor 0} :event {:type :key :key :up}})]
|
|
(is (= 0 (:cursor (:model result)))))))) ; Clamped at 0
|
|
|
|
(deftest list-selection-toggle-test
|
|
(testing "space toggles selection"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event \space)
|
|
{:model (update model :selected
|
|
#(let [cursor (:cursor model)]
|
|
(if (contains? % cursor)
|
|
(disj % cursor)
|
|
(conj % cursor))))}
|
|
{:model model}))]
|
|
|
|
;; Select item
|
|
(let [result (update-fn {:model {:cursor 0 :selected #{}}
|
|
:event {:type :key :key \space}})]
|
|
(is (= #{0} (:selected (:model result)))))
|
|
|
|
;; Select multiple items
|
|
(let [m0 {:cursor 0 :selected #{}}
|
|
m1 (:model (update-fn {:model m0 :event {:type :key :key \space}}))
|
|
m2 (:model (update-fn {:model (assoc m1 :cursor 2) :event {:type :key :key \space}}))
|
|
m3 (:model (update-fn {:model (assoc m2 :cursor 4) :event {:type :key :key \space}}))]
|
|
(is (= #{0 2 4} (:selected m3))))
|
|
|
|
;; Deselect item
|
|
(let [result (update-fn {:model {:cursor 1 :selected #{1 2}}
|
|
:event {:type :key :key \space}})]
|
|
(is (= #{2} (:selected (:model result))))))))
|
|
|
|
(deftest list-selection-submit-test
|
|
(testing "enter submits selection"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(if (ev/key= event :enter)
|
|
{:model (assoc model :submitted true)
|
|
:events [(ev/quit)]}
|
|
{:model model}))]
|
|
|
|
(let [result (update-fn {:model {:selected #{0 2} :submitted false}
|
|
:event {:type :key :key :enter}})]
|
|
(is (true? (:submitted (:model result))))
|
|
(is (= [(ev/quit)] (:events result)))))))
|
|
|
|
(deftest list-selection-view-item-count-test
|
|
(testing "view shows correct item count"
|
|
(let [item-count-text (fn [n]
|
|
(str n " item" (when (not= 1 n) "s") " selected"))]
|
|
|
|
(is (= "0 items selected" (item-count-text 0)))
|
|
(is (= "1 item selected" (item-count-text 1)))
|
|
(is (= "2 items selected" (item-count-text 2)))
|
|
(is (= "5 items selected" (item-count-text 5))))))
|
|
|
|
;; =============================================================================
|
|
;; VIEWS EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest views-initial-model-test
|
|
(testing "views initial model structure"
|
|
(let [initial-model {:view :menu
|
|
:cursor 0
|
|
:items [{:name "Profile" :desc "Profile settings"}
|
|
{:name "Settings" :desc "App preferences"}]
|
|
:selected nil}]
|
|
(is (= :menu (:view initial-model)))
|
|
(is (= 0 (:cursor initial-model)))
|
|
(is (nil? (:selected initial-model))))))
|
|
|
|
(deftest views-menu-navigation-test
|
|
(testing "menu view cursor navigation"
|
|
(let [items [{:name "A"} {:name "B"} {:name "C"} {:name "D"}]
|
|
update-fn (fn [{:keys [model event]}]
|
|
(cond
|
|
(or (ev/key= event :up)
|
|
(ev/key= event \k))
|
|
{:model (update model :cursor #(max 0 (dec %)))}
|
|
|
|
(or (ev/key= event :down)
|
|
(ev/key= event \j))
|
|
{:model (update model :cursor #(min (dec (count items)) (inc %)))}
|
|
|
|
:else {:model model}))]
|
|
|
|
;; Navigate down
|
|
(let [result (update-fn {:model {:cursor 0} :event {:type :key :key \j}})]
|
|
(is (= 1 (:cursor (:model result)))))
|
|
|
|
;; Navigate up
|
|
(let [result (update-fn {:model {:cursor 2} :event {:type :key :key \k}})]
|
|
(is (= 1 (:cursor (:model result))))))))
|
|
|
|
(deftest views-state-transitions-test
|
|
(testing "all view state transitions"
|
|
(let [items [{:name "Profile"} {:name "Settings"}]
|
|
update-fn (fn [{:keys [model event]}]
|
|
(case (:view model)
|
|
:menu
|
|
(cond
|
|
(ev/key= event :enter)
|
|
{:model (assoc model :view :detail :selected (nth items (:cursor model)))}
|
|
(ev/key= event \q)
|
|
{:model model :events [(ev/quit)]}
|
|
:else {:model model})
|
|
|
|
:detail
|
|
(cond
|
|
(or (ev/key= event :escape)
|
|
(ev/key= event \b))
|
|
{:model (assoc model :view :menu :selected nil)}
|
|
(ev/key= event \q)
|
|
{:model (assoc model :view :confirm)}
|
|
:else {:model model})
|
|
|
|
:confirm
|
|
(cond
|
|
(ev/key= event \y)
|
|
{:model model :events [(ev/quit)]}
|
|
(or (ev/key= event \n)
|
|
(ev/key= event :escape))
|
|
{:model (assoc model :view :detail)}
|
|
:else {:model model})))]
|
|
|
|
;; Menu -> Detail via enter
|
|
(let [result (update-fn {:model {:view :menu :cursor 0 :items items}
|
|
:event {:type :key :key :enter}})]
|
|
(is (= :detail (:view (:model result))))
|
|
(is (= "Profile" (:name (:selected (:model result))))))
|
|
|
|
;; Detail -> Menu via escape
|
|
(let [result (update-fn {:model {:view :detail :selected {:name "X"}}
|
|
:event {:type :key :key :escape}})]
|
|
(is (= :menu (:view (:model result))))
|
|
(is (nil? (:selected (:model result)))))
|
|
|
|
;; Detail -> Menu via b
|
|
(let [result (update-fn {:model {:view :detail :selected {:name "X"}}
|
|
:event {:type :key :key \b}})]
|
|
(is (= :menu (:view (:model result)))))
|
|
|
|
;; Detail -> Confirm via q
|
|
(let [result (update-fn {:model {:view :detail}
|
|
:event {:type :key :key \q}})]
|
|
(is (= :confirm (:view (:model result)))))
|
|
|
|
;; Confirm -> Quit via y
|
|
(let [result (update-fn {:model {:view :confirm}
|
|
:event {:type :key :key \y}})]
|
|
(is (= [(ev/quit)] (:events result))))
|
|
|
|
;; Confirm -> Detail via n
|
|
(let [result (update-fn {:model {:view :confirm}
|
|
:event {:type :key :key \n}})]
|
|
(is (= :detail (:view (:model result)))))
|
|
|
|
;; Confirm -> Detail via escape
|
|
(let [result (update-fn {:model {:view :confirm}
|
|
:event {:type :key :key :escape}})]
|
|
(is (= :detail (:view (:model result))))))))
|
|
|
|
;; =============================================================================
|
|
;; HTTP EXAMPLE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest http-initial-model-test
|
|
(testing "http initial model structure"
|
|
(let [initial-model {:state :idle
|
|
:status nil
|
|
:error nil
|
|
:url "https://httpstat.us/200"}]
|
|
(is (= :idle (:state initial-model)))
|
|
(is (nil? (:status initial-model)))
|
|
(is (nil? (:error initial-model)))
|
|
(is (string? (:url initial-model))))))
|
|
|
|
(deftest http-state-machine-test
|
|
(testing "http state transitions"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(cond
|
|
;; Start request
|
|
(and (= (:state model) :idle)
|
|
(ev/key= event :enter))
|
|
{:model (assoc model :state :loading)
|
|
:events [(ev/shell ["curl" "-s" (:url model)] {:type :http-result})]}
|
|
|
|
;; Reset
|
|
(ev/key= event \r)
|
|
{:model (assoc model :state :idle :status nil :error nil)}
|
|
|
|
;; HTTP result
|
|
(= (:type event) :http-result)
|
|
(if (get-in event [:result :success])
|
|
{:model (assoc model :state :success :status 200)}
|
|
{:model (assoc model :state :error :error (get-in event [:result :err]))})
|
|
|
|
:else
|
|
{:model model}))]
|
|
|
|
;; Idle -> Loading via enter
|
|
(let [result (update-fn {:model {:state :idle :url "http://test.com"}
|
|
:event {:type :key :key :enter}})]
|
|
(is (= :loading (:state (:model result))))
|
|
(is (= 1 (count (:events result)))))
|
|
|
|
;; Enter ignored when not idle
|
|
(let [result (update-fn {:model {:state :loading}
|
|
:event {:type :key :key :enter}})]
|
|
(is (= :loading (:state (:model result))))
|
|
(is (nil? (:events result))))
|
|
|
|
;; Loading -> Success
|
|
(let [result (update-fn {:model {:state :loading}
|
|
:event {:type :http-result :result {:success true :out "200"}}})]
|
|
(is (= :success (:state (:model result))))
|
|
(is (= 200 (:status (:model result)))))
|
|
|
|
;; Loading -> Error
|
|
(let [result (update-fn {:model {:state :loading}
|
|
:event {:type :http-result :result {:success false :err "Connection refused"}}})]
|
|
(is (= :error (:state (:model result))))
|
|
(is (= "Connection refused" (:error (:model result)))))
|
|
|
|
;; Reset from any state
|
|
(doseq [state [:idle :loading :success :error]]
|
|
(let [result (update-fn {:model {:state state :status 200 :error "err"}
|
|
:event {:type :key :key \r}})]
|
|
(is (= :idle (:state (:model result))))
|
|
(is (nil? (:status (:model result))))
|
|
(is (nil? (:error (:model result)))))))))
|
|
|
|
(deftest http-view-states-test
|
|
(testing "http view renders different states"
|
|
(let [render-state (fn [state status error]
|
|
(case state
|
|
:idle [:text {:fg :gray} "Press enter to fetch..."]
|
|
:loading [:row {:gap 1}
|
|
[:text {:fg :yellow} "⠋"]
|
|
[:text "Fetching..."]]
|
|
:success [:row {:gap 1}
|
|
[:text {:fg :green} "✓"]
|
|
[:text (str "Status: " status)]]
|
|
:error [:col
|
|
[:row {:gap 1}
|
|
[:text {:fg :red} "✗"]
|
|
[:text {:fg :red} "Error:"]]
|
|
[:text {:fg :red} error]]))]
|
|
|
|
;; Idle state
|
|
(let [view (render-state :idle nil nil)]
|
|
(is (= :text (first view)))
|
|
(is (str/includes? (render/render view) "Press enter")))
|
|
|
|
;; Loading state
|
|
(let [view (render-state :loading nil nil)]
|
|
(is (= :row (first view)))
|
|
(is (str/includes? (render/render view) "Fetching")))
|
|
|
|
;; Success state
|
|
(let [view (render-state :success 200 nil)]
|
|
(is (str/includes? (render/render view) "Status: 200")))
|
|
|
|
;; Error state
|
|
(let [view (render-state :error nil "Connection refused")]
|
|
(is (str/includes? (render/render view) "Error"))
|
|
(is (str/includes? (render/render view) "Connection refused"))))))
|