591 lines
22 KiB
Clojure
591 lines
22 KiB
Clojure
(ns tui.api-test
|
|
"Comprehensive unit tests for user-facing API functions.
|
|
Test cases derived from actual usage patterns in examples."
|
|
(:require [clojure.test :refer [deftest testing is are]]
|
|
[clojure.string :as str]
|
|
[tui.core :as tui]
|
|
[tui.render :as render]
|
|
[tui.input :as input]))
|
|
|
|
;; =============================================================================
|
|
;; KEY MATCHING TESTS (tui/key=)
|
|
;; Patterns from: counter, timer, list-selection, spinner, views, http
|
|
;; =============================================================================
|
|
|
|
(deftest key=-character-keys-test
|
|
(testing "from counter: matching q for quit"
|
|
(is (tui/key= [:key {:char \q}] "q"))
|
|
(is (not (tui/key= [:key {:char \a}] "q"))))
|
|
|
|
(testing "from counter: matching k/j for navigation"
|
|
(is (tui/key= [:key {:char \k}] "k"))
|
|
(is (tui/key= [:key {:char \j}] "j")))
|
|
|
|
(testing "from counter: matching r for reset"
|
|
(is (tui/key= [:key {:char \r}] "r")))
|
|
|
|
(testing "from timer: matching space for pause/resume"
|
|
(is (tui/key= [:key {:char \space}] " "))
|
|
(is (tui/key= [:key {:char \space}] " ")))
|
|
|
|
(testing "from views: matching b for back, y/n for confirm"
|
|
(is (tui/key= [:key {:char \b}] "b"))
|
|
(is (tui/key= [:key {:char \y}] "y"))
|
|
(is (tui/key= [:key {:char \n}] "n"))))
|
|
|
|
(deftest key=-arrow-keys-test
|
|
(testing "from counter/list-selection: up/down arrows"
|
|
(is (tui/key= [:key :up] :up))
|
|
(is (tui/key= [:key :down] :down))
|
|
(is (not (tui/key= [:key :up] :down)))
|
|
(is (not (tui/key= [:key :left] :up))))
|
|
|
|
(testing "left/right arrows"
|
|
(is (tui/key= [:key :left] :left))
|
|
(is (tui/key= [:key :right] :right))))
|
|
|
|
(deftest key=-special-keys-test
|
|
(testing "from list-selection/http: enter key"
|
|
(is (tui/key= [:key :enter] :enter)))
|
|
|
|
(testing "from views: escape key"
|
|
(is (tui/key= [:key :escape] :escape)))
|
|
|
|
(testing "from spinner: tab key"
|
|
(is (tui/key= [:key :tab] :tab)))
|
|
|
|
(testing "backspace key"
|
|
(is (tui/key= [:key :backspace] :backspace))))
|
|
|
|
(deftest key=-ctrl-combos-test
|
|
(testing "from all examples: ctrl+c for quit"
|
|
(is (tui/key= [:key {:ctrl true :char \c}] [:ctrl \c])))
|
|
|
|
(testing "other ctrl combinations"
|
|
(is (tui/key= [:key {:ctrl true :char \x}] [:ctrl \x]))
|
|
(is (tui/key= [:key {:ctrl true :char \z}] [:ctrl \z]))
|
|
(is (tui/key= [:key {:ctrl true :char \a}] [:ctrl \a])))
|
|
|
|
(testing "ctrl combo does not match plain char"
|
|
(is (not (tui/key= [:key {:ctrl true :char \c}] "c")))
|
|
(is (not (tui/key= [:key {:char \c}] [:ctrl \c])))))
|
|
|
|
(deftest key=-alt-combos-test
|
|
(testing "alt+char combinations"
|
|
(is (tui/key= [:key {:alt true :char \x}] [:alt \x]))
|
|
(is (tui/key= [:key {:alt true :char \a}] [:alt \a])))
|
|
|
|
(testing "alt combo does not match plain char"
|
|
(is (not (tui/key= [:key {:alt true :char \x}] "x")))
|
|
(is (not (tui/key= [:key {:char \x}] [:alt \x])))))
|
|
|
|
(deftest key=-non-key-messages-test
|
|
(testing "from timer/spinner: tick messages are not keys"
|
|
(is (not (tui/key= [:tick 123456789] "q")))
|
|
(is (not (tui/key= [:tick 123456789] :enter))))
|
|
|
|
(testing "from http: custom messages are not keys"
|
|
(is (not (tui/key= [:http-success 200] "q")))
|
|
(is (not (tui/key= [:http-error "timeout"] :enter))))
|
|
|
|
(testing "quit command is not a key"
|
|
(is (not (tui/key= [:quit] "q")))))
|
|
|
|
;; =============================================================================
|
|
;; COMMAND TESTS
|
|
;; Patterns from: timer, spinner, http
|
|
;; =============================================================================
|
|
|
|
(deftest quit-command-test
|
|
(testing "from all examples: tui/quit is [:quit]"
|
|
(is (= [:quit] tui/quit))
|
|
(is (vector? tui/quit))
|
|
(is (= :quit (first tui/quit)))))
|
|
|
|
(deftest after-command-test
|
|
(testing "from timer: after creates function"
|
|
(let [cmd (tui/after 1000 :timer-tick)]
|
|
(is (fn? cmd))))
|
|
|
|
(testing "from spinner: after creates function"
|
|
(let [cmd (tui/after 80 :spinner-frame)]
|
|
(is (fn? cmd))))
|
|
|
|
(testing "after with zero delay returns message immediately"
|
|
(is (= :timer-tick ((tui/after 0 :timer-tick))))
|
|
(is (= [:my-tick {:id 1}] ((tui/after 0 [:my-tick {:id 1}]))))))
|
|
|
|
(deftest batch-command-test
|
|
(testing "batch two commands"
|
|
(let [cmd (tui/batch (tui/after 100 :tick1) tui/quit)]
|
|
(is (= :batch (first cmd)))
|
|
(is (= 3 (count cmd)))
|
|
(is (fn? (second cmd)))
|
|
(is (= [:quit] (last cmd)))))
|
|
|
|
(testing "batch three commands"
|
|
(let [cmd (tui/batch (tui/after 50 :t1) (tui/after 100 :t2) tui/quit)]
|
|
(is (= :batch (first cmd)))
|
|
(is (= 4 (count cmd)))))
|
|
|
|
(testing "batch filters nil"
|
|
(let [cmd (tui/batch nil (tui/send-msg :msg1) nil)]
|
|
(is (= :batch (first cmd)))
|
|
(is (= 2 (count cmd))))
|
|
(is (= [:batch] (tui/batch nil nil nil))))
|
|
|
|
(testing "batch with single command"
|
|
(is (= [:batch [:quit]] (tui/batch tui/quit)))))
|
|
|
|
(deftest sequentially-command-test
|
|
(testing "sequentially two commands"
|
|
(let [cmd (tui/sequentially (tui/after 100 :tick1) tui/quit)]
|
|
(is (= :seq (first cmd)))
|
|
(is (= 3 (count cmd)))
|
|
(is (fn? (second cmd)))
|
|
(is (= [:quit] (last cmd)))))
|
|
|
|
(testing "sequentially filters nil"
|
|
(let [cmd (tui/sequentially nil (tui/send-msg :msg1) nil)]
|
|
(is (= :seq (first cmd)))
|
|
(is (= 2 (count cmd)))))
|
|
|
|
(testing "sequentially with functions"
|
|
(let [f (fn [] :msg)
|
|
cmd (tui/sequentially f tui/quit)]
|
|
(is (= 3 (count cmd)))
|
|
(is (= :seq (first cmd)))
|
|
(is (fn? (second cmd))))))
|
|
|
|
(deftest send-msg-command-test
|
|
(testing "from http pattern: send-msg creates function"
|
|
(let [cmd (tui/send-msg [:http-success 200])]
|
|
(is (fn? cmd))
|
|
(is (= [:http-success 200] (cmd)))))
|
|
|
|
(testing "send-msg with map"
|
|
(let [cmd (tui/send-msg {:type :custom :data 42})]
|
|
(is (= {:type :custom :data 42} (cmd)))))
|
|
|
|
(testing "send-msg with keyword"
|
|
(let [cmd (tui/send-msg :done)]
|
|
(is (= :done (cmd))))))
|
|
|
|
(deftest custom-command-function-test
|
|
(testing "from http: custom async command pattern"
|
|
(let [fetch-result (atom nil)
|
|
cmd (fn []
|
|
(reset! fetch-result :fetched)
|
|
[:http-success 200])]
|
|
;; Execute command
|
|
(is (= [:http-success 200] (cmd)))
|
|
(is (= :fetched @fetch-result)))))
|
|
|
|
;; =============================================================================
|
|
;; UPDATE FUNCTION PATTERNS
|
|
;; Testing the [model cmd] return contract
|
|
;; =============================================================================
|
|
|
|
(deftest update-returns-tuple-test
|
|
(testing "update always returns [model cmd] tuple"
|
|
(let [model {:count 0}
|
|
;; Counter-style update
|
|
update-fn (fn [m msg]
|
|
(cond
|
|
(tui/key= msg "q") [m tui/quit]
|
|
(tui/key= msg :up) [(update m :count inc) nil]
|
|
:else [m nil]))]
|
|
|
|
;; Quit returns original model + quit command
|
|
(let [[new-model cmd] (update-fn model [:key {:char \q}])]
|
|
(is (= model new-model))
|
|
(is (= tui/quit cmd)))
|
|
|
|
;; Up returns modified model + nil command
|
|
(let [[new-model cmd] (update-fn model [:key :up])]
|
|
(is (= {:count 1} new-model))
|
|
(is (nil? cmd)))
|
|
|
|
;; Unknown key returns unchanged model + nil command
|
|
(let [[new-model cmd] (update-fn model [:key {:char \x}])]
|
|
(is (= model new-model))
|
|
(is (nil? cmd))))))
|
|
|
|
(deftest counter-update-pattern-test
|
|
(testing "counter increment/decrement pattern"
|
|
(let [update-fn (fn [{:keys [count] :as model} msg]
|
|
(cond
|
|
(or (tui/key= msg :up)
|
|
(tui/key= msg "k"))
|
|
[(update model :count inc) nil]
|
|
|
|
(or (tui/key= msg :down)
|
|
(tui/key= msg "j"))
|
|
[(update model :count dec) nil]
|
|
|
|
(tui/key= msg "r")
|
|
[(assoc model :count 0) nil]
|
|
|
|
:else
|
|
[model nil]))]
|
|
|
|
;; Test sequence: up, up, down, reset
|
|
(let [m0 {:count 0}
|
|
[m1 _] (update-fn m0 [:key :up])
|
|
[m2 _] (update-fn m1 [:key {:char \k}])
|
|
[m3 _] (update-fn m2 [:key :down])
|
|
[m4 _] (update-fn m3 [:key {:char \r}])]
|
|
(is (= 1 (:count m1)))
|
|
(is (= 2 (:count m2)))
|
|
(is (= 1 (:count m3)))
|
|
(is (= 0 (:count m4)))))))
|
|
|
|
(deftest timer-update-pattern-test
|
|
(testing "timer tick handling pattern"
|
|
(let [update-fn (fn [{:keys [seconds running] :as model} msg]
|
|
(cond
|
|
(= msg :timer-tick)
|
|
(if running
|
|
(let [new-seconds (dec seconds)]
|
|
(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])
|
|
|
|
(tui/key= msg " ")
|
|
(let [new-running (not running)]
|
|
[(assoc model :running new-running)
|
|
(when new-running (tui/after 1000 :timer-tick))])
|
|
|
|
:else
|
|
[model nil]))]
|
|
|
|
;; Test tick countdown
|
|
(let [m0 {:seconds 3 :running true :done false}
|
|
[m1 c1] (update-fn m0 :timer-tick)
|
|
[m2 c2] (update-fn m1 :timer-tick)
|
|
[m3 c3] (update-fn m2 :timer-tick)]
|
|
(is (= 2 (:seconds m1)))
|
|
(is (fn? c1))
|
|
(is (= 1 (:seconds m2)))
|
|
(is (fn? c2))
|
|
(is (= 0 (:seconds m3)))
|
|
(is (:done m3))
|
|
(is (not (:running m3)))
|
|
(is (nil? c3)))
|
|
|
|
;; Test pause/resume
|
|
(let [m0 {:seconds 5 :running true :done false}
|
|
[m1 c1] (update-fn m0 [:key {:char \space}])
|
|
[m2 c2] (update-fn m1 [:key {:char \space}])]
|
|
(is (not (:running m1)))
|
|
(is (nil? c1))
|
|
(is (:running m2))
|
|
(is (fn? c2))))))
|
|
|
|
(deftest list-selection-update-pattern-test
|
|
(testing "cursor navigation with bounds"
|
|
(let [items ["a" "b" "c" "d"]
|
|
update-fn (fn [{:keys [cursor] :as model} msg]
|
|
(cond
|
|
(or (tui/key= msg :up) (tui/key= msg "k"))
|
|
[(update model :cursor #(max 0 (dec %))) nil]
|
|
|
|
(or (tui/key= msg :down) (tui/key= msg "j"))
|
|
[(update model :cursor #(min (dec (count items)) (inc %))) nil]
|
|
|
|
:else
|
|
[model nil]))]
|
|
|
|
;; Test bounds
|
|
(let [m0 {:cursor 0}
|
|
[m1 _] (update-fn m0 [:key :up]) ; Can't go below 0
|
|
[m2 _] (update-fn m1 [:key :down])
|
|
[m3 _] (update-fn m2 [:key :down])
|
|
[m4 _] (update-fn m3 [:key :down])
|
|
[m5 _] (update-fn m4 [:key :down])] ; Can't go above 3
|
|
(is (= 0 (:cursor m1)))
|
|
(is (= 1 (:cursor m2)))
|
|
(is (= 2 (:cursor m3)))
|
|
(is (= 3 (:cursor m4)))
|
|
(is (= 3 (:cursor m5))))))
|
|
|
|
(testing "toggle selection pattern"
|
|
(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 [m0 {:cursor 0 :selected #{}}
|
|
[m1 _] (update-fn m0 [:key {:char \space}])
|
|
[m2 _] (update-fn (assoc m1 :cursor 2) [:key {:char \space}])
|
|
[m3 _] (update-fn (assoc m2 :cursor 0) [:key {:char \space}])]
|
|
(is (= #{0} (:selected m1)))
|
|
(is (= #{0 2} (:selected m2)))
|
|
(is (= #{2} (:selected m3)))))))
|
|
|
|
(deftest views-state-machine-pattern-test
|
|
(testing "view state transitions"
|
|
(let [update-fn (fn [{:keys [view] :as model} msg]
|
|
(case view
|
|
:menu
|
|
(cond
|
|
(tui/key= msg :enter)
|
|
[(assoc model :view :detail) nil]
|
|
(tui/key= msg "q")
|
|
[(assoc model :view :confirm) nil]
|
|
:else [model nil])
|
|
|
|
:detail
|
|
(cond
|
|
(or (tui/key= msg :escape) (tui/key= msg "b"))
|
|
[(assoc model :view :menu) nil]
|
|
(tui/key= msg "q")
|
|
[(assoc model :view :confirm) nil]
|
|
:else [model nil])
|
|
|
|
:confirm
|
|
(cond
|
|
(tui/key= msg "y")
|
|
[model tui/quit]
|
|
(tui/key= msg "n")
|
|
[(assoc model :view :detail) nil]
|
|
:else [model nil])))]
|
|
|
|
;; Menu -> Detail -> Confirm -> Quit
|
|
(let [m0 {:view :menu}
|
|
[m1 _] (update-fn m0 [:key :enter])
|
|
[m2 _] (update-fn m1 [:key {:char \q}])
|
|
[m3 c3] (update-fn m2 [:key {:char \y}])]
|
|
(is (= :detail (:view m1)))
|
|
(is (= :confirm (:view m2)))
|
|
(is (= tui/quit c3)))
|
|
|
|
;; Detail -> Menu (back)
|
|
(let [m0 {:view :detail}
|
|
[m1 _] (update-fn m0 [:key :escape])]
|
|
(is (= :menu (:view m1)))))))
|
|
|
|
(deftest http-async-pattern-test
|
|
(testing "HTTP state machine pattern"
|
|
(let [update-fn (fn [{:keys [state url] :as model} msg]
|
|
(cond
|
|
(and (= state :idle) (tui/key= msg :enter))
|
|
[(assoc model :state :loading)
|
|
(fn [] [:http-success 200])]
|
|
|
|
(= (first msg) :http-success)
|
|
[(assoc model :state :success :status (second msg)) nil]
|
|
|
|
(= (first msg) :http-error)
|
|
[(assoc model :state :error :error (second msg)) nil]
|
|
|
|
(tui/key= msg "r")
|
|
[(assoc model :state :idle :status nil :error nil) nil]
|
|
|
|
:else
|
|
[model nil]))]
|
|
|
|
;; Idle -> Loading
|
|
(let [m0 {:state :idle :url "http://test.com"}
|
|
[m1 c1] (update-fn m0 [:key :enter])]
|
|
(is (= :loading (:state m1)))
|
|
(is (fn? c1)))
|
|
|
|
;; Loading -> Success
|
|
(let [m0 {:state :loading}
|
|
[m1 _] (update-fn m0 [:http-success 200])]
|
|
(is (= :success (:state m1)))
|
|
(is (= 200 (:status m1))))
|
|
|
|
;; Loading -> Error
|
|
(let [m0 {:state :loading}
|
|
[m1 _] (update-fn m0 [:http-error "Connection refused"])]
|
|
(is (= :error (:state m1)))
|
|
(is (= "Connection refused" (:error m1))))
|
|
|
|
;; Reset
|
|
(let [m0 {:state :error :error "timeout"}
|
|
[m1 _] (update-fn m0 [:key {:char \r}])]
|
|
(is (= :idle (:state m1)))
|
|
(is (nil? (:error m1)))))))
|
|
|
|
;; =============================================================================
|
|
;; RENDER TESTS
|
|
;; Testing hiccup patterns from examples
|
|
;; =============================================================================
|
|
|
|
(deftest render-text-styles-test
|
|
(testing "from counter: bold title"
|
|
(let [result (render/render [:text {:bold true} "Counter"])]
|
|
(is (str/includes? result "Counter"))
|
|
(is (str/includes? result "\u001b[1m"))))
|
|
|
|
(testing "from counter: conditional fg color"
|
|
(let [pos-result (render/render [:text {:fg :green} "Count: 5"])
|
|
neg-result (render/render [:text {:fg :red} "Count: -3"])
|
|
zero-result (render/render [:text {:fg :default} "Count: 0"])]
|
|
(is (str/includes? pos-result "32m")) ; Green
|
|
(is (str/includes? neg-result "31m")) ; Red
|
|
(is (str/includes? zero-result "Count: 0"))))
|
|
|
|
(testing "from timer: multiple styles"
|
|
(let [result (render/render [:text {:fg :cyan :bold true} "00:10"])]
|
|
(is (str/includes? result "36")) ; Cyan
|
|
(is (str/includes? result "1")))) ; Bold
|
|
|
|
(testing "from views: italic style"
|
|
(let [result (render/render [:text {:fg :gray :italic true} "Help text"])]
|
|
(is (str/includes? result "3m"))))) ; Italic
|
|
|
|
(deftest render-layout-patterns-test
|
|
(testing "from counter: col with gap"
|
|
(let [result (render/render [:col {:gap 1}
|
|
[:text "Line 1"]
|
|
[:text "Line 2"]])]
|
|
(is (str/includes? result "Line 1"))
|
|
(is (str/includes? result "Line 2"))
|
|
;; Gap of 1 means extra newline between items
|
|
(is (str/includes? result "\n\n"))))
|
|
|
|
(testing "from list-selection: row with gap"
|
|
(let [result (render/render [:row {:gap 1}
|
|
[:text ">"]
|
|
[:text "[x]"]
|
|
[:text "Pizza"]])]
|
|
(is (str/includes? result ">"))
|
|
(is (str/includes? result "[x]"))
|
|
(is (str/includes? result "Pizza"))))
|
|
|
|
(testing "from views: nested col in row"
|
|
(let [result (render/render [:row {:gap 2}
|
|
[:text "A"]
|
|
[:text "B"]
|
|
[:text "C"]])]
|
|
(is (= "A B C" result)))))
|
|
|
|
(deftest render-box-patterns-test
|
|
(testing "from counter: rounded border with padding"
|
|
(let [result (render/render [:box {:border :rounded :padding [0 1]}
|
|
[:text "Content"]])]
|
|
(is (str/includes? result "╭")) ; Rounded corner
|
|
(is (str/includes? result "╯"))
|
|
(is (str/includes? result "Content"))))
|
|
|
|
(testing "from list-selection: box with title"
|
|
(let [result (render/render [:box {:border :rounded :title "Menu"}
|
|
[:text "Item 1"]])]
|
|
(is (str/includes? result "Menu"))
|
|
(is (str/includes? result "Item 1"))))
|
|
|
|
(testing "from views: double border"
|
|
(let [result (render/render [:box {:border :double}
|
|
[:text "Detail"]])]
|
|
(is (str/includes? result "╔")) ; Double corner
|
|
(is (str/includes? result "║")))) ; Double vertical
|
|
|
|
(testing "box with complex padding"
|
|
(let [result (render/render [:box {:padding [1 2]}
|
|
[:text "X"]])]
|
|
;; Should have vertical and horizontal padding
|
|
(is (str/includes? result "X")))))
|
|
|
|
(deftest render-dynamic-content-test
|
|
(testing "from list-selection: generating items with into"
|
|
;; Use into to build hiccup with dynamic children
|
|
(let [items ["Pizza" "Sushi" "Tacos"]
|
|
result (render/render
|
|
(into [:col]
|
|
(for [item items]
|
|
[:text item])))]
|
|
(is (str/includes? result "Pizza"))
|
|
(is (str/includes? result "Sushi"))
|
|
(is (str/includes? result "Tacos"))))
|
|
|
|
(testing "conditional rendering"
|
|
(let [loading? true
|
|
result (render/render
|
|
(if loading?
|
|
[:text {:fg :yellow} "Loading..."]
|
|
[:text {:fg :green} "Done"]))]
|
|
(is (str/includes? result "Loading..."))))
|
|
|
|
(testing "from http: case-based view selection"
|
|
(let [render-state (fn [state]
|
|
(render/render
|
|
(case state
|
|
:idle [:text "Press enter"]
|
|
:loading [:text {:fg :yellow} "Fetching..."]
|
|
:success [:text {:fg :green} "Done"]
|
|
:error [:text {:fg :red} "Failed"])))]
|
|
(is (str/includes? (render-state :idle) "Press enter"))
|
|
(is (str/includes? (render-state :loading) "Fetching"))
|
|
(is (str/includes? (render-state :success) "Done"))
|
|
(is (str/includes? (render-state :error) "Failed")))))
|
|
|
|
(deftest render-complex-view-test
|
|
(testing "from counter: full view structure"
|
|
(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: change value"]])
|
|
result (render/render (view {:count 5}))]
|
|
(is (str/includes? result "Counter"))
|
|
(is (str/includes? result "Count: 5"))
|
|
(is (str/includes? result "j/k: change value"))
|
|
(is (str/includes? result "╭")))) ; Has box
|
|
|
|
(testing "from list-selection: cursor indicator"
|
|
(let [render-item (fn [idx cursor item selected]
|
|
(let [is-cursor (= idx cursor)
|
|
is-selected (contains? selected idx)]
|
|
[:row {:gap 1}
|
|
[:text (if is-cursor ">" " ")]
|
|
[:text (if is-selected "[x]" "[ ]")]
|
|
[:text {:bold is-cursor} item]]))
|
|
result (render/render
|
|
[:col
|
|
(render-item 0 0 "Pizza" #{0})
|
|
(render-item 1 0 "Sushi" #{})
|
|
(render-item 2 0 "Tacos" #{})])]
|
|
;; Note: bold text includes ANSI codes, so check for components
|
|
(is (str/includes? result "> [x]"))
|
|
(is (str/includes? result "Pizza"))
|
|
(is (str/includes? result "[ ] Sushi"))
|
|
(is (str/includes? result "[ ] Tacos")))))
|
|
|
|
;; =============================================================================
|
|
;; KEY-STR TESTS
|
|
;; =============================================================================
|
|
|
|
(deftest key-str-comprehensive-test
|
|
(testing "character keys"
|
|
(is (= "q" (tui/key-str [:key {:char \q}])))
|
|
(is (= " " (tui/key-str [:key {:char \space}]))))
|
|
|
|
(testing "special keys"
|
|
(is (= "enter" (tui/key-str [:key :enter])))
|
|
(is (= "escape" (tui/key-str [:key :escape])))
|
|
(is (= "tab" (tui/key-str [:key :tab])))
|
|
(is (= "backspace" (tui/key-str [:key :backspace])))
|
|
(is (= "up" (tui/key-str [:key :up])))
|
|
(is (= "down" (tui/key-str [:key :down])))
|
|
(is (= "left" (tui/key-str [:key :left])))
|
|
(is (= "right" (tui/key-str [:key :right]))))
|
|
|
|
(testing "modifier keys"
|
|
(is (= "ctrl+c" (tui/key-str [:key {:ctrl true :char \c}])))
|
|
(is (= "alt+x" (tui/key-str [:key {:alt true :char \x}])))))
|