(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 nil" (is (nil? (input/key-match? nil "q"))) (is (nil? (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 "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 "non-key message returns nil" (is (nil? (input/key->str [:tick 123]))) (is (nil? (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/tick 1) (tui/tick 2) (tui/tick 3) (tui/tick 4) (tui/tick 5))] (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 tick-edge-cases-test (testing "tick with zero" (is (= [:tick 0] (tui/tick 0)))) (testing "tick with very large value" (is (= [:tick 999999999] (tui/tick 999999999))))) (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")))))