update examples. fix bugs
This commit is contained in:
+248
-234
@@ -4,6 +4,7 @@
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[clojure.string :as str]
|
||||
[tui.core :as tui]
|
||||
[tui.events :as ev]
|
||||
[tui.render :as render]))
|
||||
|
||||
;; =============================================================================
|
||||
@@ -18,43 +19,43 @@
|
||||
|
||||
(deftest counter-update-all-keys-test
|
||||
(testing "counter responds to all documented keys"
|
||||
(let [update-fn (fn [model msg]
|
||||
(let [update-fn (fn [{:keys [model event]}]
|
||||
(cond
|
||||
(or (tui/key= msg "q")
|
||||
(tui/key= msg [:ctrl \c]))
|
||||
[model tui/quit]
|
||||
(or (ev/key= event \q)
|
||||
(ev/key= event \c #{:ctrl}))
|
||||
{:model model :events [(ev/quit)]}
|
||||
|
||||
(or (tui/key= msg :up)
|
||||
(tui/key= msg "k"))
|
||||
[(update model :count inc) nil]
|
||||
(or (ev/key= event :up)
|
||||
(ev/key= event \k))
|
||||
{:model (update model :count inc)}
|
||||
|
||||
(or (tui/key= msg :down)
|
||||
(tui/key= msg "j"))
|
||||
[(update model :count dec) nil]
|
||||
(or (ev/key= event :down)
|
||||
(ev/key= event \j))
|
||||
{:model (update model :count dec)}
|
||||
|
||||
(tui/key= msg "r")
|
||||
[(assoc model :count 0) nil]
|
||||
(ev/key= event \r)
|
||||
{:model (assoc model :count 0)}
|
||||
|
||||
:else
|
||||
[model nil]))]
|
||||
{:model model}))]
|
||||
|
||||
;; All increment keys
|
||||
(are [msg] (= 1 (:count (first (update-fn {:count 0} msg))))
|
||||
[:key :up]
|
||||
[:key {:char \k}])
|
||||
(are [event] (= 1 (:count (:model (update-fn {:model {:count 0} :event event}))))
|
||||
{:type :key :key :up}
|
||||
{:type :key :key \k})
|
||||
|
||||
;; All decrement keys
|
||||
(are [msg] (= -1 (:count (first (update-fn {:count 0} msg))))
|
||||
[:key :down]
|
||||
[:key {:char \j}])
|
||||
(are [event] (= -1 (:count (:model (update-fn {:model {:count 0} :event event}))))
|
||||
{:type :key :key :down}
|
||||
{:type :key :key \j})
|
||||
|
||||
;; All quit keys
|
||||
(are [msg] (= tui/quit (second (update-fn {:count 0} msg)))
|
||||
[:key {:char \q}]
|
||||
[:key {:ctrl true :char \c}])
|
||||
(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 (first (update-fn {:count 42} [:key {:char \r}]))))))))
|
||||
(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"
|
||||
@@ -125,66 +126,72 @@
|
||||
|
||||
(deftest timer-tick-countdown-test
|
||||
(testing "timer tick decrements and reaches zero"
|
||||
(let [update-fn (fn [{:keys [seconds running] :as model} msg]
|
||||
(cond
|
||||
(= msg :timer-tick)
|
||||
(if running
|
||||
(let [new-seconds (dec seconds)]
|
||||
(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)
|
||||
[(assoc model :seconds 0 :done true :running false) nil]
|
||||
[(assoc model :seconds new-seconds) (tui/after 1000 :timer-tick)]))
|
||||
[model nil])
|
||||
:else [model nil]))]
|
||||
{: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 [[m1 c1] (update-fn {:seconds 10 :running true :done false} :timer-tick)]
|
||||
(is (= 9 (:seconds m1)))
|
||||
(is (fn? c1)))
|
||||
(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 [[m1 c1] (update-fn {:seconds 1 :running true :done false} :timer-tick)]
|
||||
(is (= 0 (:seconds m1)))
|
||||
(is (true? (:done m1)))
|
||||
(is (false? (:running m1)))
|
||||
(is (nil? c1)))
|
||||
(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 [[m1 c1] (update-fn {:seconds 5 :running false :done false} :timer-tick)]
|
||||
(is (= 5 (:seconds m1)))
|
||||
(is (nil? c1))))))
|
||||
(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 [running] :as model} msg]
|
||||
(if (tui/key= msg " ")
|
||||
(let [new-running (not running)]
|
||||
[(assoc model :running new-running)
|
||||
(when new-running (tui/after 1000 :timer-tick))])
|
||||
[model nil]))]
|
||||
(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 [[m1 c1] (update-fn {:seconds 5 :running true} [:key {:char \space}])]
|
||||
(is (false? (:running m1)))
|
||||
(is (nil? c1)))
|
||||
(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 [[m1 c1] (update-fn {:seconds 5 :running false} [:key {:char \space}])]
|
||||
(is (true? (:running m1)))
|
||||
(is (fn? c1))))))
|
||||
(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 [model msg]
|
||||
(if (tui/key= msg "r")
|
||||
[(assoc model :seconds 10 :done false :running true)
|
||||
(tui/after 1000 :timer-tick)]
|
||||
[model nil]))]
|
||||
(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 [[m1 c1] (update-fn {:seconds 0 :done true :running false} [:key {:char \r}])]
|
||||
(is (= 10 (:seconds m1)))
|
||||
(is (false? (:done m1)))
|
||||
(is (true? (:running m1)))
|
||||
(is (fn? c1))))))
|
||||
(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"
|
||||
@@ -239,68 +246,74 @@
|
||||
|
||||
(deftest spinner-tick-advances-frame-test
|
||||
(testing "spinner tick advances frame when loading"
|
||||
(let [update-fn (fn [model msg]
|
||||
(if (= msg :spinner-frame)
|
||||
(let [update-fn (fn [{:keys [model event]}]
|
||||
(if (= (:type event) :spinner-frame)
|
||||
(if (:loading model)
|
||||
[(update model :frame inc) (tui/after 80 :spinner-frame)]
|
||||
[model nil])
|
||||
[model nil]))]
|
||||
{:model (update model :frame inc)
|
||||
:events [(ev/delayed-event 80 {:type :spinner-frame})]}
|
||||
{:model model})
|
||||
{:model model}))]
|
||||
|
||||
;; Tick advances frame when loading
|
||||
(let [[m1 c1] (update-fn {:frame 0 :loading true} :spinner-frame)]
|
||||
(is (= 1 (:frame m1)))
|
||||
(is (fn? c1)))
|
||||
(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 [[m1 c1] (update-fn {:frame 5 :loading false} :spinner-frame)]
|
||||
(is (= 5 (:frame m1)))
|
||||
(is (nil? c1))))))
|
||||
(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 (keys spinner-styles)
|
||||
update-fn (fn [{:keys [style-idx] :as model} msg]
|
||||
(if (tui/key= msg :tab)
|
||||
(let [new-idx (mod (inc style-idx) (count styles))]
|
||||
[(assoc model
|
||||
:style-idx new-idx
|
||||
:style (nth styles new-idx))
|
||||
nil])
|
||||
[model nil]))]
|
||||
(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 [[m1 _] (update-fn {:style-idx 0 :style (first styles)} [:key :tab])]
|
||||
(is (= 1 (:style-idx m1))))
|
||||
(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))
|
||||
[m1 _] (update-fn {:style-idx last-idx :style (last styles)} [:key :tab])]
|
||||
(is (= 0 (:style-idx m1)))))))
|
||||
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 [model msg]
|
||||
(if (tui/key= msg " ")
|
||||
[(assoc model :loading false :message "Done!") nil]
|
||||
[model nil]))]
|
||||
(let [update-fn (fn [{:keys [model event]}]
|
||||
(if (ev/key= event \space)
|
||||
{:model (assoc model :loading false :message "Done!")}
|
||||
{:model model}))]
|
||||
|
||||
(let [[m1 _] (update-fn {:loading true :message "Loading..."} [:key {:char \space}])]
|
||||
(is (false? (:loading m1)))
|
||||
(is (= "Done!" (:message m1)))))))
|
||||
(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 [model msg]
|
||||
(if (tui/key= msg "r")
|
||||
[(assoc model :loading true :frame 0 :message "Loading...")
|
||||
(tui/after 80 :spinner-frame)]
|
||||
[model nil]))]
|
||||
(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 [[m1 c1] (update-fn {:loading false :frame 100 :message "Done!"} [:key {:char \r}])]
|
||||
(is (true? (:loading m1)))
|
||||
(is (= 0 (:frame m1)))
|
||||
(is (= "Loading..." (:message m1)))
|
||||
(is (fn? c1))))))
|
||||
(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
|
||||
@@ -320,25 +333,25 @@
|
||||
(deftest list-selection-cursor-navigation-test
|
||||
(testing "cursor navigation respects bounds"
|
||||
(let [items ["A" "B" "C" "D" "E"]
|
||||
update-fn (fn [{:keys [cursor] :as model} msg]
|
||||
update-fn (fn [{:keys [model event]}]
|
||||
(cond
|
||||
(or (tui/key= msg :up)
|
||||
(tui/key= msg "k"))
|
||||
[(update model :cursor #(max 0 (dec %))) nil]
|
||||
(or (ev/key= event :up)
|
||||
(ev/key= event \k))
|
||||
{:model (update model :cursor #(max 0 (dec %)))}
|
||||
|
||||
(or (tui/key= msg :down)
|
||||
(tui/key= msg "j"))
|
||||
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
|
||||
(or (ev/key= event :down)
|
||||
(ev/key= event \j))
|
||||
{:model (update model :cursor #(min (dec (count items)) (inc %)))}
|
||||
|
||||
:else [model nil]))]
|
||||
:else {:model model}))]
|
||||
|
||||
;; Move down through list
|
||||
(let [m0 {:cursor 0}
|
||||
[m1 _] (update-fn m0 [:key :down])
|
||||
[m2 _] (update-fn m1 [:key :down])
|
||||
[m3 _] (update-fn m2 [:key :down])
|
||||
[m4 _] (update-fn m3 [:key :down])
|
||||
[m5 _] (update-fn m4 [:key :down])] ; Should stop at 4
|
||||
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)))
|
||||
@@ -346,47 +359,49 @@
|
||||
(is (= 4 (:cursor m5)))) ; Clamped at max
|
||||
|
||||
;; Move up from top
|
||||
(let [[m1 _] (update-fn {:cursor 0} [:key :up])]
|
||||
(is (= 0 (:cursor m1))))))) ; Clamped at 0
|
||||
(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 [cursor] :as model} msg]
|
||||
(if (tui/key= msg " ")
|
||||
[(update model :selected
|
||||
#(if (contains? % cursor)
|
||||
(disj % cursor)
|
||||
(conj % cursor)))
|
||||
nil]
|
||||
[model nil]))]
|
||||
(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 [[m1 _] (update-fn {:cursor 0 :selected #{}} [:key {:char \space}])]
|
||||
(is (= #{0} (:selected m1))))
|
||||
(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 _] (update-fn m0 [:key {:char \space}])
|
||||
m1' (assoc m1 :cursor 2)
|
||||
[m2 _] (update-fn m1' [:key {:char \space}])
|
||||
m2' (assoc m2 :cursor 4)
|
||||
[m3 _] (update-fn m2' [:key {:char \space}])]
|
||||
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 [[m1 _] (update-fn {:cursor 1 :selected #{1 2}} [:key {:char \space}])]
|
||||
(is (= #{2} (:selected m1)))))))
|
||||
(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 [model msg]
|
||||
(if (tui/key= msg :enter)
|
||||
[(assoc model :submitted true) tui/quit]
|
||||
[model nil]))]
|
||||
(let [update-fn (fn [{:keys [model event]}]
|
||||
(if (ev/key= event :enter)
|
||||
{:model (assoc model :submitted true)
|
||||
:events [(ev/quit)]}
|
||||
{:model model}))]
|
||||
|
||||
(let [[m1 c1] (update-fn {:selected #{0 2} :submitted false} [:key :enter])]
|
||||
(is (true? (:submitted m1)))
|
||||
(is (= tui/quit c1))))))
|
||||
(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"
|
||||
@@ -416,86 +431,93 @@
|
||||
(deftest views-menu-navigation-test
|
||||
(testing "menu view cursor navigation"
|
||||
(let [items [{:name "A"} {:name "B"} {:name "C"} {:name "D"}]
|
||||
update-fn (fn [{:keys [cursor] :as model} msg]
|
||||
update-fn (fn [{:keys [model event]}]
|
||||
(cond
|
||||
(or (tui/key= msg :up)
|
||||
(tui/key= msg "k"))
|
||||
[(update model :cursor #(max 0 (dec %))) nil]
|
||||
(or (ev/key= event :up)
|
||||
(ev/key= event \k))
|
||||
{:model (update model :cursor #(max 0 (dec %)))}
|
||||
|
||||
(or (tui/key= msg :down)
|
||||
(tui/key= msg "j"))
|
||||
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
|
||||
(or (ev/key= event :down)
|
||||
(ev/key= event \j))
|
||||
{:model (update model :cursor #(min (dec (count items)) (inc %)))}
|
||||
|
||||
:else [model nil]))]
|
||||
:else {:model model}))]
|
||||
|
||||
;; Navigate down
|
||||
(let [[m1 _] (update-fn {:cursor 0} [:key {:char \j}])]
|
||||
(is (= 1 (:cursor m1))))
|
||||
(let [result (update-fn {:model {:cursor 0} :event {:type :key :key \j}})]
|
||||
(is (= 1 (:cursor (:model result)))))
|
||||
|
||||
;; Navigate up
|
||||
(let [[m1 _] (update-fn {:cursor 2} [:key {:char \k}])]
|
||||
(is (= 1 (:cursor m1)))))))
|
||||
(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 [view cursor] :as model} msg]
|
||||
(case view
|
||||
update-fn (fn [{:keys [model event]}]
|
||||
(case (:view model)
|
||||
:menu
|
||||
(cond
|
||||
(tui/key= msg :enter)
|
||||
[(assoc model :view :detail :selected (nth items cursor)) nil]
|
||||
(tui/key= msg "q")
|
||||
[model tui/quit]
|
||||
:else [model nil])
|
||||
(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 (tui/key= msg :escape)
|
||||
(tui/key= msg "b"))
|
||||
[(assoc model :view :menu :selected nil) nil]
|
||||
(tui/key= msg "q")
|
||||
[(assoc model :view :confirm) nil]
|
||||
:else [model nil])
|
||||
(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
|
||||
(tui/key= msg "y")
|
||||
[model tui/quit]
|
||||
(or (tui/key= msg "n")
|
||||
(tui/key= msg :escape))
|
||||
[(assoc model :view :detail) nil]
|
||||
:else [model nil])))]
|
||||
(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 [[m1 _] (update-fn {:view :menu :cursor 0 :items items} [:key :enter])]
|
||||
(is (= :detail (:view m1)))
|
||||
(is (= "Profile" (:name (:selected m1)))))
|
||||
(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 [[m1 _] (update-fn {:view :detail :selected {:name "X"}} [:key :escape])]
|
||||
(is (= :menu (:view m1)))
|
||||
(is (nil? (:selected m1))))
|
||||
(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 [[m1 _] (update-fn {:view :detail :selected {:name "X"}} [:key {:char \b}])]
|
||||
(is (= :menu (:view m1))))
|
||||
(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 [[m1 _] (update-fn {:view :detail} [:key {:char \q}])]
|
||||
(is (= :confirm (:view m1))))
|
||||
(let [result (update-fn {:model {:view :detail}
|
||||
:event {:type :key :key \q}})]
|
||||
(is (= :confirm (:view (:model result)))))
|
||||
|
||||
;; Confirm -> Quit via y
|
||||
(let [[_ c1] (update-fn {:view :confirm} [:key {:char \y}])]
|
||||
(is (= tui/quit c1)))
|
||||
(let [result (update-fn {:model {:view :confirm}
|
||||
:event {:type :key :key \y}})]
|
||||
(is (= [(ev/quit)] (:events result))))
|
||||
|
||||
;; Confirm -> Detail via n
|
||||
(let [[m1 _] (update-fn {:view :confirm} [:key {:char \n}])]
|
||||
(is (= :detail (:view m1))))
|
||||
(let [result (update-fn {:model {:view :confirm}
|
||||
:event {:type :key :key \n}})]
|
||||
(is (= :detail (:view (:model result)))))
|
||||
|
||||
;; Confirm -> Detail via escape
|
||||
(let [[m1 _] (update-fn {:view :confirm} [:key :escape])]
|
||||
(is (= :detail (:view m1)))))))
|
||||
(let [result (update-fn {:model {:view :confirm}
|
||||
:event {:type :key :key :escape}})]
|
||||
(is (= :detail (:view (:model result))))))))
|
||||
|
||||
;; =============================================================================
|
||||
;; HTTP EXAMPLE TESTS
|
||||
@@ -514,66 +536,58 @@
|
||||
|
||||
(deftest http-state-machine-test
|
||||
(testing "http state transitions"
|
||||
(let [update-fn (fn [{:keys [state url] :as model} msg]
|
||||
(let [update-fn (fn [{:keys [model event]}]
|
||||
(cond
|
||||
;; Start request
|
||||
(and (= state :idle)
|
||||
(tui/key= msg :enter))
|
||||
[(assoc model :state :loading)
|
||||
(fn [] [:http-success 200])]
|
||||
(and (= (:state model) :idle)
|
||||
(ev/key= event :enter))
|
||||
{:model (assoc model :state :loading)
|
||||
:events [(ev/shell ["curl" "-s" (:url model)] {:type :http-result})]}
|
||||
|
||||
;; Reset
|
||||
(tui/key= msg "r")
|
||||
[(assoc model :state :idle :status nil :error nil) nil]
|
||||
(ev/key= event \r)
|
||||
{:model (assoc model :state :idle :status nil :error nil)}
|
||||
|
||||
;; HTTP success
|
||||
(= (first msg) :http-success)
|
||||
[(assoc model :state :success :status (second msg)) nil]
|
||||
|
||||
;; HTTP error
|
||||
(= (first msg) :http-error)
|
||||
[(assoc model :state :error :error (second msg)) 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 nil]))]
|
||||
{:model model}))]
|
||||
|
||||
;; Idle -> Loading via enter
|
||||
(let [[m1 c1] (update-fn {:state :idle :url "http://test.com"} [:key :enter])]
|
||||
(is (= :loading (:state m1)))
|
||||
(is (fn? c1)))
|
||||
(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 [[m1 c1] (update-fn {:state :loading} [:key :enter])]
|
||||
(is (= :loading (:state m1)))
|
||||
(is (nil? c1)))
|
||||
(let [result (update-fn {:model {:state :loading}
|
||||
:event {:type :key :key :enter}})]
|
||||
(is (= :loading (:state (:model result))))
|
||||
(is (nil? (:events result))))
|
||||
|
||||
;; Loading -> Success
|
||||
(let [[m1 _] (update-fn {:state :loading} [:http-success 200])]
|
||||
(is (= :success (:state m1)))
|
||||
(is (= 200 (:status m1))))
|
||||
(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 [[m1 _] (update-fn {:state :loading} [:http-error "Connection refused"])]
|
||||
(is (= :error (:state m1)))
|
||||
(is (= "Connection refused" (:error m1))))
|
||||
(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 [[m1 _] (update-fn {:state state :status 200 :error "err"} [:key {:char \r}])]
|
||||
(is (= :idle (:state m1)))
|
||||
(is (nil? (:status m1)))
|
||||
(is (nil? (:error m1))))))))
|
||||
|
||||
(deftest http-fetch-command-test
|
||||
(testing "fetch command creates async function"
|
||||
(let [fetch-url (fn [url]
|
||||
(fn []
|
||||
;; Simulate success
|
||||
[:http-success 200]))]
|
||||
|
||||
(let [cmd (fetch-url "https://test.com")]
|
||||
(is (fn? cmd))
|
||||
(is (= [:http-success 200] (cmd)))))))
|
||||
(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"
|
||||
|
||||
Reference in New Issue
Block a user