refactor
This commit is contained in:
+146
-30
@@ -4,16 +4,17 @@
|
||||
(: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]))
|
||||
|
||||
;; === Command Tests ===
|
||||
;; === Legacy Command Tests (Backward Compatibility) ===
|
||||
|
||||
(deftest quit-command-test
|
||||
(testing "quit command is correct vector"
|
||||
(testing "quit command is correct vector (legacy)"
|
||||
(is (= [:quit] tui/quit))))
|
||||
|
||||
(deftest after-command-test
|
||||
(testing "after creates a function command"
|
||||
(testing "after creates a function command (legacy)"
|
||||
(let [cmd (tui/after 0 :my-tick)]
|
||||
(is (fn? cmd))
|
||||
(is (= :my-tick (cmd)))))
|
||||
@@ -23,16 +24,15 @@
|
||||
(is (= :simple-msg ((tui/after 0 :simple-msg)))))
|
||||
|
||||
(testing "after with non-zero delay creates function"
|
||||
;; Don't invoke - these would sleep
|
||||
(is (fn? (tui/after 100 :tick)))
|
||||
(is (fn? (tui/after 1000 :tick)))))
|
||||
|
||||
(deftest batch-command-test
|
||||
(testing "batch combines commands"
|
||||
(testing "batch combines commands (legacy)"
|
||||
(let [cmd (tui/batch (tui/send-msg :msg1) tui/quit)]
|
||||
(is (vector? cmd))
|
||||
(is (= :batch (first cmd)))
|
||||
(is (= 3 (count cmd))) ; [:batch fn [:quit]]
|
||||
(is (= 3 (count cmd)))
|
||||
(is (= [:quit] (last cmd)))))
|
||||
|
||||
(testing "batch filters nil commands"
|
||||
@@ -41,7 +41,7 @@
|
||||
(is (= 2 (count cmd))))))
|
||||
|
||||
(deftest sequentially-command-test
|
||||
(testing "sequentially creates seq command"
|
||||
(testing "sequentially creates seq command (legacy)"
|
||||
(let [cmd (tui/sequentially (tui/send-msg :msg1) tui/quit)]
|
||||
(is (vector? cmd))
|
||||
(is (= :seq (first cmd)))
|
||||
@@ -54,24 +54,35 @@
|
||||
(is (= 2 (count cmd))))))
|
||||
|
||||
(deftest send-msg-command-test
|
||||
(testing "send-msg creates function that returns message"
|
||||
(testing "send-msg creates function that returns message (legacy)"
|
||||
(let [cmd (tui/send-msg {:type :custom :data 42})]
|
||||
(is (fn? cmd))
|
||||
(is (= {:type :custom :data 42} (cmd))))))
|
||||
|
||||
;; === Key Matching Tests ===
|
||||
;; === Legacy Key Matching Tests ===
|
||||
|
||||
(deftest key=-test
|
||||
(testing "key= delegates to input/key-match?"
|
||||
(deftest key=-legacy-test
|
||||
(testing "key= works with legacy format"
|
||||
(is (tui/key= [:key {:char \q}] "q"))
|
||||
(is (tui/key= [:key :enter] :enter))
|
||||
(is (tui/key= [:key {:ctrl true :char \c}] [:ctrl \c]))
|
||||
(is (not (tui/key= [:key {:char \a}] "b")))))
|
||||
|
||||
(deftest key=-new-format-test
|
||||
(testing "key= works with new format"
|
||||
(is (tui/key= {:type :key :key \q} "q"))
|
||||
(is (tui/key= {:type :key :key :enter} :enter))
|
||||
(is (tui/key= {:type :key :key \c :modifiers #{:ctrl}} [:ctrl \c]))
|
||||
(is (not (tui/key= {:type :key :key \a} "b")))))
|
||||
|
||||
(deftest key-str-test
|
||||
(testing "key-str converts key to string"
|
||||
(testing "key-str converts key to string (legacy)"
|
||||
(is (= "q" (tui/key-str [:key {:char \q}])))
|
||||
(is (= "enter" (tui/key-str [:key :enter])))))
|
||||
(is (= "enter" (tui/key-str [:key :enter]))))
|
||||
|
||||
(testing "key-str converts key to string (new format)"
|
||||
(is (= "q" (tui/key-str {:type :key :key \q})))
|
||||
(is (= "enter" (tui/key-str {:type :key :key :enter})))))
|
||||
|
||||
;; === Full Pipeline Tests ===
|
||||
|
||||
@@ -87,8 +98,36 @@
|
||||
(is (clojure.string/includes? rendered "Counter"))
|
||||
(is (clojure.string/includes? rendered "Count: 5")))))
|
||||
|
||||
(deftest update-function-contract-test
|
||||
(testing "update function returns [model cmd] tuple"
|
||||
;; === New API Update Function Tests ===
|
||||
|
||||
(deftest new-update-function-contract-test
|
||||
(testing "new 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))))))
|
||||
|
||||
;; === Legacy Update Function Tests ===
|
||||
|
||||
(deftest legacy-update-function-contract-test
|
||||
(testing "legacy update function returns [model cmd] tuple"
|
||||
(let [update-fn (fn [model msg]
|
||||
(cond
|
||||
(tui/key= msg "q") [model tui/quit]
|
||||
@@ -111,20 +150,87 @@
|
||||
(is (= model new-model))
|
||||
(is (nil? cmd))))))
|
||||
|
||||
;; === Command Execution Tests ===
|
||||
;; These test the internal command execution logic
|
||||
;; === Event Execution Tests ===
|
||||
|
||||
(deftest execute-quit-command-test
|
||||
(testing "quit command puts :quit on channel"
|
||||
(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-delay-event-test
|
||||
(testing "delay event sends message after delay"
|
||||
(let [msg-chan (chan 1)
|
||||
event (ev/delay 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))))
|
||||
|
||||
;; === Legacy Command Execution Tests ===
|
||||
|
||||
(deftest execute-quit-command-legacy-test
|
||||
(testing "quit command puts {:type :quit} on channel"
|
||||
(let [msg-chan (chan 1)]
|
||||
(#'tui/execute-cmd! [:quit] msg-chan)
|
||||
(let [result (alt!!
|
||||
msg-chan ([v] v)
|
||||
(timeout 100) :timeout)]
|
||||
(is (= [:quit] result)))
|
||||
(is (= {:type :quit} result)))
|
||||
(close! msg-chan))))
|
||||
|
||||
(deftest execute-after-command-test
|
||||
(deftest execute-after-command-legacy-test
|
||||
(testing "after command sends message after delay"
|
||||
(let [msg-chan (chan 1)
|
||||
cmd (tui/after 50 :delayed-msg)]
|
||||
@@ -141,7 +247,7 @@
|
||||
(is (= :delayed-msg delayed)))
|
||||
(close! msg-chan))))
|
||||
|
||||
(deftest execute-function-command-test
|
||||
(deftest execute-function-command-legacy-test
|
||||
(testing "function command executes and sends result"
|
||||
(let [msg-chan (chan 1)
|
||||
cmd (fn [] {:custom :message})]
|
||||
@@ -152,7 +258,7 @@
|
||||
(is (= {:custom :message} result)))
|
||||
(close! msg-chan))))
|
||||
|
||||
(deftest execute-batch-command-test
|
||||
(deftest execute-batch-command-legacy-test
|
||||
(testing "batch executes multiple commands"
|
||||
(let [msg-chan (chan 10)]
|
||||
(#'tui/execute-cmd! [:batch
|
||||
@@ -171,7 +277,7 @@
|
||||
(is (= #{:msg1 :msg2} (set results))))
|
||||
(close! msg-chan))))
|
||||
|
||||
(deftest execute-nil-command-test
|
||||
(deftest execute-nil-command-legacy-test
|
||||
(testing "nil command does nothing"
|
||||
(let [msg-chan (chan 1)]
|
||||
(#'tui/execute-cmd! nil msg-chan)
|
||||
@@ -184,12 +290,22 @@
|
||||
;; === Defapp Macro Tests ===
|
||||
|
||||
(deftest defapp-macro-test
|
||||
(testing "defapp creates app map"
|
||||
(tui/defapp test-app
|
||||
(testing "defapp creates app map (legacy)"
|
||||
(tui/defapp test-app-legacy
|
||||
:init {:count 0}
|
||||
:update (fn [m msg] [m nil])
|
||||
:view (fn [m] [:text "test"]))
|
||||
(is (map? test-app))
|
||||
(is (= {:count 0} (:init test-app)))
|
||||
(is (fn? (:update test-app)))
|
||||
(is (fn? (:view test-app)))))
|
||||
(is (map? test-app-legacy))
|
||||
(is (= {:count 0} (:init test-app-legacy)))
|
||||
(is (fn? (:update test-app-legacy)))
|
||||
(is (fn? (:view test-app-legacy))))
|
||||
|
||||
(testing "defapp creates app map (new)"
|
||||
(tui/defapp test-app-new
|
||||
:init {:count 0}
|
||||
:update (fn [{:keys [model event]}] {:model model})
|
||||
:view (fn [m size] [:text "test"]))
|
||||
(is (map? test-app-new))
|
||||
(is (= {:count 0} (:init test-app-new)))
|
||||
(is (fn? (:update test-app-new)))
|
||||
(is (fn? (:view test-app-new)))))
|
||||
|
||||
@@ -152,26 +152,27 @@
|
||||
;; The current implementation only looks at first char
|
||||
(is (input/key-match? [:key {:char \q}] "quit")))
|
||||
|
||||
(testing "nil message returns nil"
|
||||
(is (nil? (input/key-match? nil "q")))
|
||||
(is (nil? (input/key-match? nil :enter))))
|
||||
(testing "nil message returns false"
|
||||
(is (not (input/key-match? nil "q")))
|
||||
(is (not (input/key-match? nil :enter))))
|
||||
|
||||
(testing "non-key message returns nil"
|
||||
(is (nil? (input/key-match? [:tick 123] "q")))
|
||||
(is (nil? (input/key-match? [:http-success 200] :enter)))
|
||||
(is (nil? (input/key-match? "not a vector" "q"))))
|
||||
(testing "non-key message returns false"
|
||||
(is (not (input/key-match? [:tick 123] "q")))
|
||||
(is (not (input/key-match? [:http-success 200] :enter)))
|
||||
(is (not (input/key-match? "not a vector" "q"))))
|
||||
|
||||
(testing "unknown key message structure"
|
||||
(is (not (input/key-match? [:key {:unknown true}] "q")))
|
||||
(is (not (input/key-match? [:key {}] "q")))))
|
||||
|
||||
(deftest key-str-edge-cases-test
|
||||
(testing "nil message returns nil"
|
||||
(is (nil? (input/key->str nil))))
|
||||
(testing "nil message returns empty string"
|
||||
(is (= "" (input/key->str nil))))
|
||||
|
||||
(testing "non-key message returns nil"
|
||||
(is (nil? (input/key->str [:tick 123])))
|
||||
(is (nil? (input/key->str [:custom :message]))))
|
||||
(testing "non-key message returns string representation"
|
||||
;; Legacy format returns the second element as string
|
||||
(is (string? (input/key->str [:tick 123])))
|
||||
(is (string? (input/key->str [:custom :message]))))
|
||||
|
||||
(testing "key message with empty map"
|
||||
(is (= "" (input/key->str [:key {}]))))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
(ns tui.events-test
|
||||
"Unit tests for event constructors and key matching."
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[tui.events :as ev]))
|
||||
|
||||
;; === Event Constructor Tests ===
|
||||
|
||||
(deftest quit-test
|
||||
(testing "quit returns quit event"
|
||||
(is (= {:type :quit} (ev/quit)))))
|
||||
|
||||
(deftest delay-test
|
||||
(testing "delay creates delay event"
|
||||
(is (= {:type :delay :ms 1000 :event {:type :tick}}
|
||||
(ev/delay 1000 {:type :tick}))))
|
||||
|
||||
(testing "delay with different ms values"
|
||||
(is (= 0 (:ms (ev/delay 0 {:type :x}))))
|
||||
(is (= 5000 (:ms (ev/delay 5000 {:type :x}))))))
|
||||
|
||||
(deftest shell-test
|
||||
(testing "shell creates shell event with vector cmd"
|
||||
(is (= {:type :shell :cmd ["git" "status"] :event {:type :result}}
|
||||
(ev/shell ["git" "status"] {:type :result}))))
|
||||
|
||||
(testing "shell preserves event data"
|
||||
(let [event {:type :git-result :file "foo.txt"}]
|
||||
(is (= event (:event (ev/shell ["git" "diff"] event)))))))
|
||||
|
||||
(deftest batch-test
|
||||
(testing "batch creates batch event"
|
||||
(let [e1 {:type :a}
|
||||
e2 {:type :b}
|
||||
result (ev/batch e1 e2)]
|
||||
(is (= :batch (:type result)))
|
||||
(is (= [e1 e2] (:events result)))))
|
||||
|
||||
(testing "batch filters nil events"
|
||||
(let [result (ev/batch nil {:type :a} nil {:type :b} nil)]
|
||||
(is (= [{:type :a} {:type :b}] (:events result)))))
|
||||
|
||||
(testing "batch returns nil for all-nil events"
|
||||
(is (nil? (ev/batch nil nil nil))))
|
||||
|
||||
(testing "batch returns nil for no events"
|
||||
(is (nil? (ev/batch)))))
|
||||
|
||||
(deftest sequential-test
|
||||
(testing "sequential creates sequential event"
|
||||
(let [e1 {:type :a}
|
||||
e2 {:type :b}
|
||||
result (ev/sequential e1 e2)]
|
||||
(is (= :sequential (:type result)))
|
||||
(is (= [e1 e2] (:events result)))))
|
||||
|
||||
(testing "sequential filters nil events"
|
||||
(let [result (ev/sequential nil {:type :a} nil {:type :b} nil)]
|
||||
(is (= [{:type :a} {:type :b}] (:events result)))))
|
||||
|
||||
(testing "sequential returns nil for all-nil events"
|
||||
(is (nil? (ev/sequential nil nil nil))))
|
||||
|
||||
(testing "sequential returns nil for no events"
|
||||
(is (nil? (ev/sequential)))))
|
||||
|
||||
(deftest debounce-test
|
||||
(testing "debounce creates debounce event"
|
||||
(is (= {:type :debounce :id :search :ms 300 :event {:type :search-query}}
|
||||
(ev/debounce :search 300 {:type :search-query}))))
|
||||
|
||||
(testing "debounce with different ids"
|
||||
(is (= :resize (:id (ev/debounce :resize 100 {:type :x}))))
|
||||
(is (= :input (:id (ev/debounce :input 50 {:type :x}))))))
|
||||
|
||||
;; === Key Matching Tests ===
|
||||
|
||||
(deftest key=-basic-test
|
||||
(testing "matches plain character"
|
||||
(is (ev/key= {:type :key :key \q} \q))
|
||||
(is (ev/key= {:type :key :key \a} \a))
|
||||
(is (ev/key= {:type :key :key \1} \1)))
|
||||
|
||||
(testing "does not match different character"
|
||||
(is (not (ev/key= {:type :key :key \q} \a)))
|
||||
(is (not (ev/key= {:type :key :key \x} \y)))))
|
||||
|
||||
(deftest key=-special-keys-test
|
||||
(testing "matches special keys"
|
||||
(is (ev/key= {:type :key :key :enter} :enter))
|
||||
(is (ev/key= {:type :key :key :escape} :escape))
|
||||
(is (ev/key= {:type :key :key :up} :up))
|
||||
(is (ev/key= {:type :key :key :f1} :f1)))
|
||||
|
||||
(testing "does not match wrong special keys"
|
||||
(is (not (ev/key= {:type :key :key :up} :down)))
|
||||
(is (not (ev/key= {:type :key :key :enter} :escape)))))
|
||||
|
||||
(deftest key=-with-modifiers-test
|
||||
(testing "matches key with exact modifiers"
|
||||
(is (ev/key= {:type :key :key \c :modifiers #{:ctrl}} \c #{:ctrl}))
|
||||
(is (ev/key= {:type :key :key \z :modifiers #{:ctrl :shift}} \z #{:ctrl :shift}))
|
||||
(is (ev/key= {:type :key :key \a :modifiers #{:shift}} \a #{:shift})))
|
||||
|
||||
(testing "does not match with wrong modifiers"
|
||||
(is (not (ev/key= {:type :key :key \c :modifiers #{:ctrl}} \c #{:alt})))
|
||||
(is (not (ev/key= {:type :key :key \c :modifiers #{:ctrl :shift}} \c #{:ctrl}))))
|
||||
|
||||
(testing "does not match when modifiers present but not specified"
|
||||
(is (not (ev/key= {:type :key :key \c :modifiers #{:ctrl}} \c)))
|
||||
(is (not (ev/key= {:type :key :key \a :modifiers #{:shift}} \a))))
|
||||
|
||||
(testing "does not match when modifiers specified but not present"
|
||||
(is (not (ev/key= {:type :key :key \c} \c #{:ctrl})))))
|
||||
|
||||
(deftest key=-non-key-events-test
|
||||
(testing "returns false for non-key events"
|
||||
(is (not (ev/key= {:type :quit} \q)))
|
||||
(is (not (ev/key= {:type :tick} :enter)))
|
||||
(is (not (ev/key= nil \a)))))
|
||||
|
||||
;; === Composition Tests ===
|
||||
|
||||
(deftest composed-events-test
|
||||
(testing "can compose multiple event constructors"
|
||||
(let [result (ev/batch
|
||||
(ev/shell ["git" "status"] {:type :status})
|
||||
(ev/delay 1000 {:type :refresh}))]
|
||||
(is (= :batch (:type result)))
|
||||
(is (= 2 (count (:events result))))
|
||||
(is (= :shell (:type (first (:events result)))))
|
||||
(is (= :delay (:type (second (:events result)))))))
|
||||
|
||||
(testing "can nest batch in sequential"
|
||||
(let [result (ev/sequential
|
||||
{:type :start}
|
||||
(ev/batch
|
||||
(ev/shell ["git" "add" "."] {:type :added})
|
||||
(ev/shell ["git" "status"] {:type :status})))]
|
||||
(is (= :sequential (:type result)))
|
||||
(is (= :batch (:type (second (:events result))))))))
|
||||
+97
-25
@@ -3,10 +3,68 @@
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[tui.input :as input]))
|
||||
|
||||
;; === Key Matching Tests ===
|
||||
;; === New Event Format Tests ===
|
||||
|
||||
(deftest key-match-character-test
|
||||
(testing "matches single character keys"
|
||||
(deftest key-match-new-format-character-test
|
||||
(testing "matches single character keys (new format)"
|
||||
(is (input/key-match? {:type :key :key \q} "q"))
|
||||
(is (input/key-match? {:type :key :key \a} "a"))
|
||||
(is (input/key-match? {:type :key :key \1} "1")))
|
||||
|
||||
(testing "does not match different characters"
|
||||
(is (not (input/key-match? {:type :key :key \q} "a")))
|
||||
(is (not (input/key-match? {:type :key :key \x} "y"))))
|
||||
|
||||
(testing "does not match ctrl+char as plain char"
|
||||
(is (not (input/key-match? {:type :key :key \c :modifiers #{:ctrl}} "c"))))
|
||||
|
||||
(testing "does not match alt+char as plain char"
|
||||
(is (not (input/key-match? {:type :key :key \x :modifiers #{:alt}} "x")))))
|
||||
|
||||
(deftest key-match-new-format-special-keys-test
|
||||
(testing "matches special keys by keyword (new format)"
|
||||
(is (input/key-match? {:type :key :key :enter} :enter))
|
||||
(is (input/key-match? {:type :key :key :escape} :escape))
|
||||
(is (input/key-match? {:type :key :key :backspace} :backspace))
|
||||
(is (input/key-match? {:type :key :key :tab} :tab)))
|
||||
|
||||
(testing "matches arrow keys"
|
||||
(is (input/key-match? {:type :key :key :up} :up))
|
||||
(is (input/key-match? {:type :key :key :down} :down))
|
||||
(is (input/key-match? {:type :key :key :left} :left))
|
||||
(is (input/key-match? {:type :key :key :right} :right)))
|
||||
|
||||
(testing "matches function keys"
|
||||
(is (input/key-match? {:type :key :key :f1} :f1))
|
||||
(is (input/key-match? {:type :key :key :f12} :f12)))
|
||||
|
||||
(testing "does not match wrong special keys"
|
||||
(is (not (input/key-match? {:type :key :key :up} :down)))
|
||||
(is (not (input/key-match? {:type :key :key :enter} :escape)))))
|
||||
|
||||
(deftest key-match-new-format-ctrl-combo-test
|
||||
(testing "matches ctrl+char combinations (new format)"
|
||||
(is (input/key-match? {:type :key :key \c :modifiers #{:ctrl}} [:ctrl \c]))
|
||||
(is (input/key-match? {:type :key :key \x :modifiers #{:ctrl}} [:ctrl \x]))
|
||||
(is (input/key-match? {:type :key :key \z :modifiers #{:ctrl}} [:ctrl \z])))
|
||||
|
||||
(testing "does not match wrong ctrl combinations"
|
||||
(is (not (input/key-match? {:type :key :key \c :modifiers #{:ctrl}} [:ctrl \x])))
|
||||
(is (not (input/key-match? {:type :key :key \c} [:ctrl \c])))))
|
||||
|
||||
(deftest key-match-new-format-alt-combo-test
|
||||
(testing "matches alt+char combinations (new format)"
|
||||
(is (input/key-match? {:type :key :key \x :modifiers #{:alt}} [:alt \x]))
|
||||
(is (input/key-match? {:type :key :key \a :modifiers #{:alt}} [:alt \a])))
|
||||
|
||||
(testing "does not match wrong alt combinations"
|
||||
(is (not (input/key-match? {:type :key :key \x :modifiers #{:alt}} [:alt \y])))
|
||||
(is (not (input/key-match? {:type :key :key \x} [:alt \x])))))
|
||||
|
||||
;; === Legacy Event Format Tests (Backward Compatibility) ===
|
||||
|
||||
(deftest key-match-legacy-character-test
|
||||
(testing "matches single character keys (legacy format)"
|
||||
(is (input/key-match? [:key {:char \q}] "q"))
|
||||
(is (input/key-match? [:key {:char \a}] "a"))
|
||||
(is (input/key-match? [:key {:char \1}] "1")))
|
||||
@@ -21,8 +79,8 @@
|
||||
(testing "does not match alt+char as plain char"
|
||||
(is (not (input/key-match? [:key {:alt true :char \x}] "x")))))
|
||||
|
||||
(deftest key-match-special-keys-test
|
||||
(testing "matches special keys by keyword"
|
||||
(deftest key-match-legacy-special-keys-test
|
||||
(testing "matches special keys by keyword (legacy format)"
|
||||
(is (input/key-match? [:key :enter] :enter))
|
||||
(is (input/key-match? [:key :escape] :escape))
|
||||
(is (input/key-match? [:key :backspace] :backspace))
|
||||
@@ -42,8 +100,8 @@
|
||||
(is (not (input/key-match? [:key :up] :down)))
|
||||
(is (not (input/key-match? [:key :enter] :escape)))))
|
||||
|
||||
(deftest key-match-ctrl-combo-test
|
||||
(testing "matches ctrl+char combinations"
|
||||
(deftest key-match-legacy-ctrl-combo-test
|
||||
(testing "matches ctrl+char combinations (legacy format)"
|
||||
(is (input/key-match? [:key {:ctrl true :char \c}] [:ctrl \c]))
|
||||
(is (input/key-match? [:key {:ctrl true :char \x}] [:ctrl \x]))
|
||||
(is (input/key-match? [:key {:ctrl true :char \z}] [:ctrl \z])))
|
||||
@@ -52,8 +110,8 @@
|
||||
(is (not (input/key-match? [:key {:ctrl true :char \c}] [:ctrl \x])))
|
||||
(is (not (input/key-match? [:key {:char \c}] [:ctrl \c])))))
|
||||
|
||||
(deftest key-match-alt-combo-test
|
||||
(testing "matches alt+char combinations"
|
||||
(deftest key-match-legacy-alt-combo-test
|
||||
(testing "matches alt+char combinations (legacy format)"
|
||||
(is (input/key-match? [:key {:alt true :char \x}] [:alt \x]))
|
||||
(is (input/key-match? [:key {:alt true :char \a}] [:alt \a])))
|
||||
|
||||
@@ -62,33 +120,47 @@
|
||||
(is (not (input/key-match? [:key {:char \x}] [:alt \x])))))
|
||||
|
||||
(deftest key-match-non-key-messages-test
|
||||
(testing "returns nil for non-key messages"
|
||||
(is (nil? (input/key-match? [:tick 123] "q")))
|
||||
(is (nil? (input/key-match? [:quit] :enter)))
|
||||
(is (nil? (input/key-match? nil "a")))))
|
||||
(testing "returns false for non-key messages"
|
||||
(is (not (input/key-match? {:type :tick :value 123} "q")))
|
||||
(is (not (input/key-match? {:type :quit} :enter)))
|
||||
(is (not (input/key-match? nil "a")))))
|
||||
|
||||
;; === Key to String Tests ===
|
||||
|
||||
(deftest key->str-special-keys-test
|
||||
(testing "converts special keys to strings"
|
||||
(deftest key->str-new-format-test
|
||||
(testing "converts special keys to strings (new format)"
|
||||
(is (= "enter" (input/key->str {:type :key :key :enter})))
|
||||
(is (= "escape" (input/key->str {:type :key :key :escape})))
|
||||
(is (= "up" (input/key->str {:type :key :key :up})))
|
||||
(is (= "f1" (input/key->str {:type :key :key :f1}))))
|
||||
|
||||
(testing "converts character keys to strings (new format)"
|
||||
(is (= "q" (input/key->str {:type :key :key \q})))
|
||||
(is (= "a" (input/key->str {:type :key :key \a}))))
|
||||
|
||||
(testing "converts modifier combinations to strings (new format)"
|
||||
(is (= "ctrl+c" (input/key->str {:type :key :key \c :modifiers #{:ctrl}})))
|
||||
(is (= "alt+x" (input/key->str {:type :key :key \x :modifiers #{:alt}})))
|
||||
(is (= "shift+a" (input/key->str {:type :key :key \a :modifiers #{:shift}})))))
|
||||
|
||||
(deftest key->str-legacy-format-test
|
||||
(testing "converts special keys to strings (legacy format)"
|
||||
(is (= "enter" (input/key->str [:key :enter])))
|
||||
(is (= "escape" (input/key->str [:key :escape])))
|
||||
(is (= "up" (input/key->str [:key :up])))
|
||||
(is (= "f1" (input/key->str [:key :f1])))))
|
||||
(is (= "f1" (input/key->str [:key :f1]))))
|
||||
|
||||
(deftest key->str-character-keys-test
|
||||
(testing "converts character keys to strings"
|
||||
(testing "converts character keys to strings (legacy format)"
|
||||
(is (= "q" (input/key->str [:key {:char \q}])))
|
||||
(is (= "a" (input/key->str [:key {:char \a}])))))
|
||||
(is (= "a" (input/key->str [:key {:char \a}]))))
|
||||
|
||||
(deftest key->str-modifier-keys-test
|
||||
(testing "converts ctrl combinations to strings"
|
||||
(testing "converts ctrl combinations to strings (legacy format)"
|
||||
(is (= "ctrl+c" (input/key->str [:key {:ctrl true :char \c}]))))
|
||||
|
||||
(testing "converts alt combinations to strings"
|
||||
(testing "converts alt combinations to strings (legacy format)"
|
||||
(is (= "alt+x" (input/key->str [:key {:alt true :char \x}])))))
|
||||
|
||||
(deftest key->str-non-key-messages-test
|
||||
(testing "returns nil for non-key messages"
|
||||
(is (nil? (input/key->str [:tick 123])))
|
||||
(is (nil? (input/key->str nil)))))
|
||||
(testing "returns string for non-key messages"
|
||||
(is (string? (input/key->str [:tick 123])))
|
||||
(is (= "" (input/key->str nil)))))
|
||||
|
||||
+187
-1
@@ -80,7 +80,10 @@
|
||||
[:row "c" " " "d"]]))))
|
||||
|
||||
(testing "renders col inside row"
|
||||
(is (= "a\nb c\nd" (render/render [:row
|
||||
;; Row places children side-by-side, aligning lines
|
||||
;; col1 = "a\nb", col2 = " ", col3 = "c\nd"
|
||||
;; Result: line1 = "a c", line2 = "b d" (space between a/c and b/d is from the " " child)
|
||||
(is (= "a c\nb d" (render/render [:row
|
||||
[:col "a" "b"]
|
||||
" "
|
||||
[:col "c" "d"]])))))
|
||||
@@ -138,6 +141,189 @@
|
||||
|
||||
;; === Convenience Function Tests ===
|
||||
|
||||
;; === Grid Tests ===
|
||||
|
||||
(deftest parse-template-test
|
||||
(testing "parses simple template"
|
||||
(let [result (#'render/parse-template ["a a" "b c"])]
|
||||
(is (= {:row 0 :col 0 :row-span 1 :col-span 2} (get result "a")))
|
||||
(is (= {:row 1 :col 0 :row-span 1 :col-span 1} (get result "b")))
|
||||
(is (= {:row 1 :col 1 :row-span 1 :col-span 1} (get result "c")))))
|
||||
|
||||
(testing "parses template with row spans"
|
||||
(let [result (#'render/parse-template ["a b" "a c"])]
|
||||
(is (= {:row 0 :col 0 :row-span 2 :col-span 1} (get result "a")))))
|
||||
|
||||
(testing "ignores . for empty cells"
|
||||
(let [result (#'render/parse-template [". a" "b a"])]
|
||||
(is (nil? (get result ".")))
|
||||
(is (= {:row 0 :col 1 :row-span 2 :col-span 1} (get result "a"))))))
|
||||
|
||||
(deftest render-grid-test
|
||||
(testing "renders simple 2x2 grid with explicit positioning"
|
||||
(let [result (render/render [:grid {:rows [1 1] :cols [3 3]}
|
||||
[:area {:row 0 :col 0} "A"]
|
||||
[:area {:row 0 :col 1} "B"]
|
||||
[:area {:row 1 :col 0} "C"]
|
||||
[:area {:row 1 :col 1} "D"]]
|
||||
{:available-width 6 :available-height 2})]
|
||||
(is (str/includes? result "A"))
|
||||
(is (str/includes? result "B"))
|
||||
(is (str/includes? result "C"))
|
||||
(is (str/includes? result "D"))))
|
||||
|
||||
(testing "renders grid with named template"
|
||||
(let [result (render/render [:grid {:template ["header header"
|
||||
"nav main"]
|
||||
:rows [1 1]
|
||||
:cols [3 3]}
|
||||
[:area {:name "header"} "H"]
|
||||
[:area {:name "nav"} "N"]
|
||||
[:area {:name "main"} "M"]]
|
||||
{:available-width 6 :available-height 2})]
|
||||
(is (str/includes? result "H"))
|
||||
(is (str/includes? result "N"))
|
||||
(is (str/includes? result "M"))))
|
||||
|
||||
(testing "grid convenience functions create proper elements"
|
||||
(is (= [:grid {} "a" "b"] (render/grid "a" "b")))
|
||||
(is (= [:grid {:rows [1 1]} "a"] (render/grid {:rows [1 1]} "a")))
|
||||
(is (= [:area {} "content"] (render/area "content")))
|
||||
(is (= [:area {:row 0 :col 1} "x"] (render/area {:row 0 :col 1} "x")))))
|
||||
|
||||
;; === Scroll Tests ===
|
||||
|
||||
(deftest visible-window-calc-test
|
||||
(testing "all items fit when total <= max-visible"
|
||||
(let [result (#'render/visible-window-calc 3 0 5)]
|
||||
(is (= 0 (:start result)))
|
||||
(is (= 3 (:end result)))
|
||||
(is (false? (:has-above result)))
|
||||
(is (false? (:has-below result)))))
|
||||
|
||||
(testing "cursor at start shows beginning of list"
|
||||
(let [result (#'render/visible-window-calc 10 0 3)]
|
||||
(is (= 0 (:start result)))
|
||||
(is (= 3 (:end result)))
|
||||
(is (false? (:has-above result)))
|
||||
(is (true? (:has-below result)))))
|
||||
|
||||
(testing "cursor at end shows end of list"
|
||||
(let [result (#'render/visible-window-calc 10 9 3)]
|
||||
(is (= 7 (:start result)))
|
||||
(is (= 10 (:end result)))
|
||||
(is (true? (:has-above result)))
|
||||
(is (false? (:has-below result)))))
|
||||
|
||||
(testing "cursor in middle centers window"
|
||||
(let [result (#'render/visible-window-calc 10 5 3)]
|
||||
(is (>= (:start result) 3))
|
||||
(is (<= (:end result) 7))
|
||||
(is (true? (:has-above result)))
|
||||
(is (true? (:has-below result))))))
|
||||
|
||||
(deftest render-scroll-test
|
||||
(testing "renders all items when they fit"
|
||||
(let [result (render/render [:scroll {:cursor 0 :indicators false}
|
||||
"item1" "item2" "item3"]
|
||||
{:available-height 10})]
|
||||
(is (str/includes? result "item1"))
|
||||
(is (str/includes? result "item2"))
|
||||
(is (str/includes? result "item3"))))
|
||||
|
||||
(testing "renders only visible items when content exceeds height"
|
||||
(let [result (render/render [:scroll {:cursor 0 :indicators false}
|
||||
"item1" "item2" "item3" "item4" "item5"]
|
||||
{:available-height 2})]
|
||||
(is (str/includes? result "item1"))
|
||||
(is (str/includes? result "item2"))
|
||||
(is (not (str/includes? result "item5")))))
|
||||
|
||||
(testing "shows down indicator when more content below"
|
||||
(let [result (render/render [:scroll {:cursor 0}
|
||||
"item1" "item2" "item3" "item4" "item5"]
|
||||
{:available-height 4})]
|
||||
(is (str/includes? result "↓"))))
|
||||
|
||||
(testing "shows up indicator when more content above"
|
||||
(let [result (render/render [:scroll {:cursor 4}
|
||||
"item1" "item2" "item3" "item4" "item5"]
|
||||
{:available-height 4})]
|
||||
(is (str/includes? result "↑"))))
|
||||
|
||||
(testing "scroll convenience function creates scroll element"
|
||||
(is (= [:scroll {} "a" "b"] (render/scroll "a" "b")))
|
||||
(is (= [:scroll {:cursor 2} "a" "b" "c"] (render/scroll {:cursor 2} "a" "b" "c")))))
|
||||
|
||||
;; === Enhanced Sizing Tests ===
|
||||
|
||||
(deftest parse-size-spec-test
|
||||
(testing "parses fixed numbers"
|
||||
(is (= {:type :fixed :value 30} (#'render/parse-size-spec 30)))
|
||||
(is (= {:type :fixed :value 0} (#'render/parse-size-spec 0))))
|
||||
|
||||
(testing "parses :flex shorthand"
|
||||
(is (= {:type :flex :value 1} (#'render/parse-size-spec :flex))))
|
||||
|
||||
(testing "parses {:flex n} weighted flex"
|
||||
(is (= {:type :flex :value 2 :min nil :max nil}
|
||||
(#'render/parse-size-spec {:flex 2})))
|
||||
(is (= {:type :flex :value 3 :min 10 :max 50}
|
||||
(#'render/parse-size-spec {:flex 3 :min 10 :max 50}))))
|
||||
|
||||
(testing "parses percentage strings"
|
||||
(is (= {:type :percent :value 50} (#'render/parse-size-spec "50%")))
|
||||
(is (= {:type :percent :value 100} (#'render/parse-size-spec "100%"))))
|
||||
|
||||
(testing "parses fractional unit strings"
|
||||
(is (= {:type :fr :value 1} (#'render/parse-size-spec "1fr")))
|
||||
(is (= {:type :fr :value 2} (#'render/parse-size-spec "2fr"))))
|
||||
|
||||
(testing "parses {:percent n} with constraints"
|
||||
(is (= {:type :percent :value 30 :min 10 :max 100}
|
||||
(#'render/parse-size-spec {:percent 30 :min 10 :max 100}))))
|
||||
|
||||
(testing "parses nil as auto"
|
||||
(is (= {:type :auto :value nil} (#'render/parse-size-spec nil)))))
|
||||
|
||||
(deftest calculate-sizes-test
|
||||
(testing "calculates fixed sizes"
|
||||
(is (= [30 40] (#'render/calculate-sizes [30 40] [:a :b] 100 0))))
|
||||
|
||||
(testing "calculates flex sizes evenly"
|
||||
(is (= [50 50] (#'render/calculate-sizes [:flex :flex] [:a :b] 100 0))))
|
||||
|
||||
(testing "calculates weighted flex sizes"
|
||||
(let [result (#'render/calculate-sizes [{:flex 1} {:flex 2}] [:a :b] 90 0)]
|
||||
(is (= 30 (first result)))
|
||||
(is (= 60 (second result)))))
|
||||
|
||||
(testing "calculates mixed fixed and flex"
|
||||
(is (= [20 40 40] (#'render/calculate-sizes [20 :flex :flex] [:a :b :c] 100 0))))
|
||||
|
||||
(testing "accounts for gap in calculations"
|
||||
;; 100 - 10 gap = 90 usable, split evenly
|
||||
(is (= [45 45] (#'render/calculate-sizes [:flex :flex] [:a :b] 100 10))))
|
||||
|
||||
(testing "calculates percentage sizes"
|
||||
(let [result (#'render/calculate-sizes ["50%" "50%"] [:a :b] 100 0)]
|
||||
(is (= 50 (first result)))
|
||||
(is (= 50 (second result)))))
|
||||
|
||||
(testing "calculates fractional unit sizes"
|
||||
(let [result (#'render/calculate-sizes ["1fr" "2fr"] [:a :b] 90 0)]
|
||||
(is (= 30 (first result)))
|
||||
(is (= 60 (second result)))))
|
||||
|
||||
(testing "handles mixed percentage, fixed, and flex"
|
||||
(let [result (#'render/calculate-sizes [20 "50%" :flex] [:a :b :c] 100 0)]
|
||||
;; Fixed: 20, remaining: 80
|
||||
;; Percentage: 50% of 80 = 40
|
||||
;; Flex gets remaining: 80 - 40 = 40
|
||||
(is (= 20 (first result)))
|
||||
(is (= 40 (second result)))
|
||||
(is (= 40 (nth result 2))))))
|
||||
|
||||
(deftest convenience-functions-test
|
||||
(testing "text function creates text element"
|
||||
(is (= [:text {} "hello"] (render/text "hello")))
|
||||
|
||||
Reference in New Issue
Block a user