211 lines
7.9 KiB
Clojure
211 lines
7.9 KiB
Clojure
(ns tui.core-test
|
|
"Integration tests for the TUI engine.
|
|
Tests the update loop, event handling, and full render pipeline."
|
|
(:require [clojure.test :refer [deftest testing is]]
|
|
[clojure.core.async :as async :refer [chan >!! <!! timeout alt!! close!]]
|
|
[tui.core :as tui]
|
|
[tui.events :as ev]
|
|
[tui.render :as render]))
|
|
|
|
;; =============================================================================
|
|
;; EVENT CONSTRUCTOR TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest quit-event-test
|
|
(testing "tui/quit creates quit event"
|
|
(is (= {:type :quit} (tui/quit)))))
|
|
|
|
(deftest delayed-event-test
|
|
(testing "tui/delayed-event creates delayed-event event"
|
|
(let [event (tui/delayed-event 1000 {:type :timer-tick})]
|
|
(is (= :delayed-event (:type event)))
|
|
(is (= 1000 (:ms event)))
|
|
(is (= {:type :timer-tick} (:event event))))))
|
|
|
|
(deftest batch-event-test
|
|
(testing "tui/batch creates batch event"
|
|
(let [event (tui/batch {:type :msg1} {:type :msg2})]
|
|
(is (= :batch (:type event)))
|
|
(is (= 2 (count (:events event))))))
|
|
|
|
(testing "batch filters nil"
|
|
(let [event (tui/batch nil {:type :msg1} nil)]
|
|
(is (= 1 (count (:events event)))))
|
|
(is (nil? (tui/batch nil nil nil)))))
|
|
|
|
(deftest sequential-event-test
|
|
(testing "tui/sequential creates sequential event"
|
|
(let [event (tui/sequential {:type :msg1} {:type :msg2})]
|
|
(is (= :sequential (:type event)))
|
|
(is (= 2 (count (:events event))))))
|
|
|
|
(testing "sequential filters nil"
|
|
(let [event (tui/sequential nil {:type :msg1} nil)]
|
|
(is (= 1 (count (:events event)))))))
|
|
|
|
(deftest shell-event-test
|
|
(testing "tui/shell creates shell event"
|
|
(let [event (tui/shell ["git" "status"] {:type :git-result})]
|
|
(is (= :shell (:type event)))
|
|
(is (= ["git" "status"] (:cmd event)))
|
|
(is (= {:type :git-result} (:event event))))))
|
|
|
|
(deftest debounce-event-test
|
|
(testing "tui/debounce creates debounce event"
|
|
(let [event (tui/debounce :search 300 {:type :do-search})]
|
|
(is (= :debounce (:type event)))
|
|
(is (= :search (:id event)))
|
|
(is (= 300 (:ms event))))))
|
|
|
|
;; =============================================================================
|
|
;; KEY MATCHING TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest key=-test
|
|
(testing "key= matches characters"
|
|
(is (tui/key= {:type :key :key \q} \q))
|
|
(is (not (tui/key= {:type :key :key \a} \q))))
|
|
|
|
(testing "key= matches special keys"
|
|
(is (tui/key= {:type :key :key :enter} :enter))
|
|
(is (tui/key= {:type :key :key :up} :up))
|
|
(is (tui/key= {:type :key :key :escape} :escape)))
|
|
|
|
(testing "key= matches modifiers"
|
|
(is (tui/key= {:type :key :key \c :modifiers #{:ctrl}} \c #{:ctrl}))
|
|
(is (not (tui/key= {:type :key :key \c :modifiers #{:ctrl}} \c)))
|
|
(is (not (tui/key= {:type :key :key \c} \c #{:ctrl}))))
|
|
|
|
(testing "key= doesn't match non-key events"
|
|
(is (not (tui/key= {:type :timer-tick} \q)))
|
|
(is (not (tui/key= {:type :quit} :enter)))))
|
|
|
|
;; =============================================================================
|
|
;; RENDER PIPELINE TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest render-pipeline-test
|
|
(testing "model -> view -> render produces valid output"
|
|
(let [model {:count 5}
|
|
view (fn [{:keys [count]}]
|
|
[:col
|
|
[:text {:bold true} "Counter"]
|
|
[:text (str "Count: " count)]])
|
|
rendered (render/render (view model))]
|
|
(is (string? rendered))
|
|
(is (clojure.string/includes? rendered "Counter"))
|
|
(is (clojure.string/includes? rendered "Count: 5")))))
|
|
|
|
;; =============================================================================
|
|
;; UPDATE FUNCTION CONTRACT TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest update-function-contract-test
|
|
(testing "update function returns {:model ... :events ...}"
|
|
(let [update-fn (fn [{:keys [model event]}]
|
|
(cond
|
|
(ev/key= event \q) {:model model :events [(ev/quit)]}
|
|
(ev/key= event :up) {:model (update model :n inc)}
|
|
:else {:model model}))
|
|
model {:n 0}]
|
|
|
|
;; Test quit returns event
|
|
(let [{:keys [model events]} (update-fn {:model model :event {:type :key :key \q}})]
|
|
(is (= {:n 0} model))
|
|
(is (= [{:type :quit}] events)))
|
|
|
|
;; Test up returns updated model
|
|
(let [{:keys [model events]} (update-fn {:model model :event {:type :key :key :up}})]
|
|
(is (= {:n 1} model))
|
|
(is (nil? events)))
|
|
|
|
;; Test unknown key returns model unchanged
|
|
(let [{:keys [model events]} (update-fn {:model model :event {:type :key :key \x}})]
|
|
(is (= {:n 0} model))
|
|
(is (nil? events))))))
|
|
|
|
;; =============================================================================
|
|
;; EVENT EXECUTION TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest execute-quit-event-test
|
|
(testing "quit event puts {:type :quit} on channel"
|
|
(let [msg-chan (chan 1)]
|
|
(#'tui/execute-event! {:type :quit} msg-chan)
|
|
(let [result (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 100) :timeout)]
|
|
(is (= {:type :quit} result)))
|
|
(close! msg-chan))))
|
|
|
|
(deftest execute-delayed-event-test
|
|
(testing "delayed-event sends message after delay"
|
|
(let [msg-chan (chan 1)
|
|
event (ev/delayed-event 50 {:type :delayed-msg})]
|
|
(#'tui/execute-event! event msg-chan)
|
|
;; Should not receive immediately
|
|
(let [immediate (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 10) :timeout)]
|
|
(is (= :timeout immediate)))
|
|
;; Should receive after delay
|
|
(let [delayed (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 200) :timeout)]
|
|
(is (= {:type :delayed-msg} delayed)))
|
|
(close! msg-chan))))
|
|
|
|
(deftest execute-batch-event-test
|
|
(testing "batch executes multiple events"
|
|
(let [msg-chan (chan 10)]
|
|
(#'tui/execute-event! {:type :batch
|
|
:events [{:type :msg1}
|
|
{:type :msg2}]}
|
|
msg-chan)
|
|
;; Give time for dispatch
|
|
(Thread/sleep 50)
|
|
(let [results (loop [msgs []]
|
|
(let [msg (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 10) nil)]
|
|
(if msg
|
|
(recur (conj msgs msg))
|
|
msgs)))]
|
|
(is (= #{{:type :msg1} {:type :msg2}} (set results))))
|
|
(close! msg-chan))))
|
|
|
|
(deftest execute-unknown-event-test
|
|
(testing "unknown event type is dispatched to update"
|
|
(let [msg-chan (chan 1)]
|
|
(#'tui/execute-event! {:type :custom-app-event :data 42} msg-chan)
|
|
(let [result (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 100) :timeout)]
|
|
(is (= {:type :custom-app-event :data 42} result)))
|
|
(close! msg-chan))))
|
|
|
|
(deftest execute-nil-event-test
|
|
(testing "nil event does nothing"
|
|
(let [msg-chan (chan 1)]
|
|
(#'tui/execute-event! nil msg-chan)
|
|
(let [result (alt!!
|
|
msg-chan ([v] v)
|
|
(timeout 50) :timeout)]
|
|
(is (= :timeout result)))
|
|
(close! msg-chan))))
|
|
|
|
;; =============================================================================
|
|
;; DEFAPP MACRO TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest defapp-macro-test
|
|
(testing "defapp creates app map"
|
|
(tui/defapp test-app
|
|
:init {:count 0}
|
|
:update (fn [{:keys [model event]}] {:model model})
|
|
:view (fn [m size] [:text "test"]))
|
|
(is (map? test-app))
|
|
(is (= {:count 0} (:init test-app)))
|
|
(is (fn? (:update test-app)))
|
|
(is (fn? (:view test-app)))))
|