415 lines
14 KiB
Clojure
415 lines
14 KiB
Clojure
(ns tui.edge-cases-test
|
|
"Edge case tests for all TUI modules.
|
|
Tests boundary conditions, error handling, and unusual inputs."
|
|
(:require [clojure.test :refer [deftest testing is are]]
|
|
[clojure.string :as str]
|
|
[tui.core :as tui]
|
|
[tui.render :as render]
|
|
[tui.input :as input]
|
|
[tui.ansi :as ansi]))
|
|
|
|
;; =============================================================================
|
|
;; RENDER EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest render-empty-elements-test
|
|
(testing "empty col renders as empty string"
|
|
(is (= "" (render/render [:col]))))
|
|
|
|
(testing "empty row renders as empty string"
|
|
(is (= "" (render/render [:row]))))
|
|
|
|
(testing "empty text renders as empty string"
|
|
(is (= "" (render/render [:text]))))
|
|
|
|
(testing "nil renders as empty string"
|
|
(is (= "" (render/render nil)))))
|
|
|
|
(deftest render-nested-empty-test
|
|
(testing "nested empty elements produce minimal output"
|
|
;; Col with empty rows produces newlines between them
|
|
(is (= "\n" (render/render [:col [:row] [:row]])))
|
|
;; Row with empty cols produces empty string (no gap)
|
|
(is (= "" (render/render [:row [:col] [:col]])))))
|
|
|
|
(deftest render-single-element-test
|
|
(testing "single element col"
|
|
(is (= "hello" (render/render [:col "hello"]))))
|
|
|
|
(testing "single element row"
|
|
(is (= "hello" (render/render [:row "hello"])))))
|
|
|
|
(deftest render-special-characters-test
|
|
(testing "renders unicode characters"
|
|
(is (= "✓" (render/render [:text "✓"])))
|
|
(is (= "⠋" (render/render [:text "⠋"])))
|
|
(is (= "🌑" (render/render [:text "🌑"])))
|
|
(is (= "╭──╮" (render/render [:row "╭" "──" "╮"]))))
|
|
|
|
(testing "renders newlines in text"
|
|
(is (= "a\nb" (render/render [:text "a\nb"])))))
|
|
|
|
(deftest render-multiline-content-in-row-test
|
|
(testing "multiline elements in row"
|
|
(let [result (render/render [:row [:col "a" "b"] " " [:col "c" "d"]])]
|
|
(is (str/includes? result "a"))
|
|
(is (str/includes? result "b"))
|
|
(is (str/includes? result "c"))
|
|
(is (str/includes? result "d")))))
|
|
|
|
(deftest render-deeply-nested-test
|
|
(testing "deeply nested structure"
|
|
(let [result (render/render
|
|
[:col
|
|
[:row
|
|
[:col
|
|
[:row
|
|
[:text "deep"]]]]])]
|
|
(is (= "deep" result)))))
|
|
|
|
(deftest render-box-edge-cases-test
|
|
(testing "box with empty content has corners and sides"
|
|
(let [result (render/render [:box ""])]
|
|
(is (str/includes? result "╭")) ; Has corner
|
|
(is (str/includes? result "│"))))
|
|
|
|
(testing "box with very long content"
|
|
(let [long-text (apply str (repeat 100 "x"))
|
|
result (render/render [:box long-text])]
|
|
(is (str/includes? result long-text))))
|
|
|
|
(testing "box with multiline content"
|
|
(let [result (render/render [:box [:col "line1" "line2" "line3"]])]
|
|
(is (str/includes? result "line1"))
|
|
(is (str/includes? result "line2"))
|
|
(is (str/includes? result "line3"))))
|
|
|
|
(testing "box with all padding formats"
|
|
;; Single value
|
|
(is (string? (render/render [:box {:padding 1} "x"])))
|
|
;; Two values [v h]
|
|
(is (string? (render/render [:box {:padding [1 2]} "x"])))
|
|
;; Four values [t r b l]
|
|
(is (string? (render/render [:box {:padding [1 2 3 4]} "x"])))
|
|
;; Invalid (defaults to 0)
|
|
(is (string? (render/render [:box {:padding [1 2 3]} "x"])))))
|
|
|
|
(deftest render-space-edge-cases-test
|
|
(testing "space with zero width"
|
|
(is (= "" (render/render [:space {:width 0}]))))
|
|
|
|
(testing "space with large dimensions"
|
|
(let [result (render/render [:space {:width 5 :height 3}])]
|
|
(is (= " \n \n " result)))))
|
|
|
|
(deftest render-gap-edge-cases-test
|
|
(testing "col with gap 0"
|
|
(is (= "a\nb" (render/render [:col {:gap 0} "a" "b"]))))
|
|
|
|
(testing "row with gap 0"
|
|
(is (= "ab" (render/render [:row {:gap 0} "a" "b"]))))
|
|
|
|
(testing "col with large gap"
|
|
(let [result (render/render [:col {:gap 3} "a" "b"])]
|
|
(is (= "a\n\n\n\nb" result))))
|
|
|
|
(testing "row with large gap"
|
|
(let [result (render/render [:row {:gap 5} "a" "b"])]
|
|
(is (= "a b" result)))))
|
|
|
|
(deftest render-styled-text-combinations-test
|
|
(testing "all style attributes combined"
|
|
(let [result (render/render [:text {:bold true
|
|
:dim true
|
|
:italic true
|
|
:underline true
|
|
:inverse true
|
|
:strike true
|
|
:fg :red
|
|
:bg :blue}
|
|
"styled"])]
|
|
(is (str/includes? result "styled"))
|
|
(is (str/includes? result "\u001b["))))
|
|
|
|
(testing "unknown fg color defaults gracefully"
|
|
(let [result (render/render [:text {:fg :nonexistent} "text"])]
|
|
(is (str/includes? result "text"))))
|
|
|
|
(testing "numeric values render as strings"
|
|
(is (= "42" (render/render 42)))
|
|
(is (= "3.14" (render/render 3.14)))
|
|
(is (= "-10" (render/render -10)))))
|
|
|
|
;; =============================================================================
|
|
;; KEY MATCHING EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest key-match-edge-cases-test
|
|
(testing "empty string pattern"
|
|
(is (not (input/key-match? [:key {:char \a}] ""))))
|
|
|
|
(testing "multi-char string pattern only matches first char"
|
|
;; The current implementation only looks at first char
|
|
(is (input/key-match? [:key {:char \q}] "quit")))
|
|
|
|
(testing "nil message returns false"
|
|
(is (not (input/key-match? nil "q")))
|
|
(is (not (input/key-match? nil :enter))))
|
|
|
|
(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 empty string"
|
|
(is (= "" (input/key->str nil))))
|
|
|
|
(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 {}]))))
|
|
|
|
(testing "ctrl and alt combined"
|
|
;; This is an edge case - both modifiers
|
|
(is (= "ctrl+alt+x" (input/key->str [:key {:ctrl true :alt true :char \x}])))))
|
|
|
|
;; =============================================================================
|
|
;; COMMAND EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest batch-edge-cases-test
|
|
(testing "batch with all nils"
|
|
(is (= [:batch] (tui/batch nil nil nil))))
|
|
|
|
(testing "batch with single command"
|
|
(is (= [:batch tui/quit] (tui/batch tui/quit))))
|
|
|
|
(testing "batch with no arguments"
|
|
(is (= [:batch] (tui/batch))))
|
|
|
|
(testing "batch with many commands"
|
|
(let [cmd (tui/batch (tui/after 1 :t1) (tui/after 2 :t2) (tui/after 3 :t3) (tui/after 4 :t4) (tui/after 5 :t5))]
|
|
(is (= 6 (count cmd))) ; :batch + 5 commands
|
|
(is (= :batch (first cmd))))))
|
|
|
|
(deftest sequentially-edge-cases-test
|
|
(testing "sequentially with all nils"
|
|
(is (= [:seq] (tui/sequentially nil nil nil))))
|
|
|
|
(testing "sequentially with single command"
|
|
(is (= [:seq tui/quit] (tui/sequentially tui/quit))))
|
|
|
|
(testing "sequentially with no arguments"
|
|
(is (= [:seq] (tui/sequentially)))))
|
|
|
|
(deftest after-edge-cases-test
|
|
(testing "after with zero delay"
|
|
(let [cmd (tui/after 0 :immediate)]
|
|
(is (fn? cmd))
|
|
;; Zero delay executes immediately
|
|
(is (= :immediate (cmd)))))
|
|
|
|
(testing "after with various delays creates function"
|
|
;; Don't invoke - just verify the function is created correctly
|
|
(is (fn? (tui/after 1 :t1)))
|
|
(is (fn? (tui/after 1000 :t2)))
|
|
(is (fn? (tui/after 999999999 :t3))))
|
|
|
|
(testing "after with complex message"
|
|
(let [cmd (tui/after 0 [:tick {:id 1 :data [1 2 3]}])]
|
|
(is (= [:tick {:id 1 :data [1 2 3]}] (cmd))))))
|
|
|
|
(deftest send-msg-edge-cases-test
|
|
(testing "send-msg with nil"
|
|
(let [cmd (tui/send-msg nil)]
|
|
(is (fn? cmd))
|
|
(is (nil? (cmd)))))
|
|
|
|
(testing "send-msg with complex message"
|
|
(let [msg {:type :complex :data [1 2 3] :nested {:a :b}}
|
|
cmd (tui/send-msg msg)]
|
|
(is (= msg (cmd))))))
|
|
|
|
;; =============================================================================
|
|
;; ANSI EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest visible-length-edge-cases-test
|
|
(testing "empty string"
|
|
(is (= 0 (ansi/visible-length ""))))
|
|
|
|
(testing "only ANSI codes"
|
|
(is (= 0 (ansi/visible-length "\u001b[31m\u001b[0m"))))
|
|
|
|
(testing "multiple ANSI sequences"
|
|
(let [text (str (ansi/fg :red "a") (ansi/fg :blue "b") (ansi/fg :green "c"))]
|
|
(is (= 3 (ansi/visible-length text))))))
|
|
|
|
(deftest pad-right-edge-cases-test
|
|
(testing "pad to width 0"
|
|
(is (= "hello" (ansi/pad-right "hello" 0))))
|
|
|
|
(testing "pad empty string"
|
|
(is (= " " (ansi/pad-right "" 5))))
|
|
|
|
(testing "pad already wider"
|
|
(is (= "hello world" (ansi/pad-right "hello world" 5)))))
|
|
|
|
(deftest pad-left-edge-cases-test
|
|
(testing "pad to width 0"
|
|
(is (= "hello" (ansi/pad-left "hello" 0))))
|
|
|
|
(testing "pad empty string"
|
|
(is (= " " (ansi/pad-left "" 5)))))
|
|
|
|
(deftest pad-center-edge-cases-test
|
|
(testing "center in width 0"
|
|
(is (= "hi" (ansi/pad-center "hi" 0))))
|
|
|
|
(testing "center empty string"
|
|
(is (= " " (ansi/pad-center "" 3))))
|
|
|
|
(testing "center in exact width"
|
|
(is (= "hello" (ansi/pad-center "hello" 5)))))
|
|
|
|
(deftest truncate-edge-cases-test
|
|
(testing "truncate to 0"
|
|
(is (= "…" (ansi/truncate "hello" 1))))
|
|
|
|
(testing "truncate empty string"
|
|
(is (= "" (ansi/truncate "" 5))))
|
|
|
|
(testing "truncate exact length"
|
|
(is (= "hello" (ansi/truncate "hello" 5)))))
|
|
|
|
(deftest style-edge-cases-test
|
|
(testing "style with no attributes"
|
|
(is (= "text" (ansi/style "text"))))
|
|
|
|
(testing "style empty string"
|
|
(let [result (ansi/style "" :bold true)]
|
|
(is (str/includes? result ansi/reset)))))
|
|
|
|
(deftest color-functions-edge-cases-test
|
|
(testing "fg with default color"
|
|
(let [result (ansi/fg :default "text")]
|
|
(is (str/includes? result "39m"))))
|
|
|
|
(testing "bg with default color"
|
|
(let [result (ansi/bg :default "text")]
|
|
(is (str/includes? result "49m"))))
|
|
|
|
(testing "256-color boundary values"
|
|
(is (string? (ansi/fg-256 0 "text")))
|
|
(is (string? (ansi/fg-256 255 "text"))))
|
|
|
|
(testing "RGB boundary values"
|
|
(is (string? (ansi/fg-rgb 0 0 0 "text")))
|
|
(is (string? (ansi/fg-rgb 255 255 255 "text")))))
|
|
|
|
;; =============================================================================
|
|
;; UPDATE FUNCTION EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest update-with-unknown-messages-test
|
|
(testing "update function handles unknown messages gracefully"
|
|
(let [update-fn (fn [model msg]
|
|
(cond
|
|
(tui/key= msg "q") [model tui/quit]
|
|
:else [model nil]))]
|
|
|
|
;; Unknown key
|
|
(let [[m cmd] (update-fn {:n 0} [:key {:char \x}])]
|
|
(is (= {:n 0} m))
|
|
(is (nil? cmd)))
|
|
|
|
;; Unknown message type
|
|
(let [[m cmd] (update-fn {:n 0} [:unknown :message])]
|
|
(is (= {:n 0} m))
|
|
(is (nil? cmd)))
|
|
|
|
;; Empty message
|
|
(let [[m cmd] (update-fn {:n 0} [])]
|
|
(is (= {:n 0} m))
|
|
(is (nil? cmd))))))
|
|
|
|
(deftest model-with-complex-state-test
|
|
(testing "model with nested data structures"
|
|
(let [complex-model {:count 0
|
|
:items ["a" "b" "c"]
|
|
:nested {:deep {:value 42}}
|
|
:selected #{}
|
|
:history []}
|
|
update-fn (fn [model msg]
|
|
(if (tui/key= msg :up)
|
|
[(-> model
|
|
(update :count inc)
|
|
(update :history conj (:count model)))
|
|
nil]
|
|
[model nil]))]
|
|
|
|
(let [[m1 _] (update-fn complex-model [:key :up])
|
|
[m2 _] (update-fn m1 [:key :up])]
|
|
(is (= 1 (:count m1)))
|
|
(is (= [0] (:history m1)))
|
|
(is (= 2 (:count m2)))
|
|
(is (= [0 1] (:history m2)))
|
|
;; Other fields unchanged
|
|
(is (= ["a" "b" "c"] (:items m2)))
|
|
(is (= 42 (get-in m2 [:nested :deep :value])))))))
|
|
|
|
;; =============================================================================
|
|
;; VIEW FUNCTION EDGE CASES
|
|
;; =============================================================================
|
|
|
|
(deftest view-with-conditional-rendering-test
|
|
(testing "view handles nil children gracefully"
|
|
(let [view (fn [show-extra]
|
|
[:col
|
|
[:text "always"]
|
|
(when show-extra
|
|
[:text "sometimes"])])]
|
|
|
|
(let [result1 (render/render (view true))
|
|
result2 (render/render (view false))]
|
|
(is (str/includes? result1 "always"))
|
|
(is (str/includes? result1 "sometimes"))
|
|
(is (str/includes? result2 "always"))
|
|
(is (not (str/includes? result2 "sometimes"))))))
|
|
|
|
(testing "view with for generating elements"
|
|
(let [view (fn [items]
|
|
[:col
|
|
(for [item items]
|
|
[:text item])])]
|
|
|
|
(is (string? (render/render (view ["a" "b" "c"]))))
|
|
(is (string? (render/render (view []))))))) ; Empty list
|
|
|
|
(deftest view-with-dynamic-styles-test
|
|
(testing "dynamic style based on state"
|
|
(let [view (fn [{:keys [error loading success]}]
|
|
[:text {:fg (cond
|
|
error :red
|
|
loading :yellow
|
|
success :green
|
|
:else :default)}
|
|
(cond
|
|
error "Error!"
|
|
loading "Loading..."
|
|
success "Done!"
|
|
:else "Idle")])]
|
|
|
|
(is (str/includes? (render/render (view {:error true})) "Error!"))
|
|
(is (str/includes? (render/render (view {:loading true})) "Loading"))
|
|
(is (str/includes? (render/render (view {:success true})) "Done!"))
|
|
(is (str/includes? (render/render (view {})) "Idle")))))
|