Files

379 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.events :as ev]
[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 "nil event returns false"
(is (not (ev/key= nil \q)))
(is (not (ev/key= nil :enter))))
(testing "non-key event returns false"
(is (not (ev/key= {:type :timer-tick} \q)))
(is (not (ev/key= {:type :http-result :status 200} :enter))))
(testing "key event with missing key field"
(is (not (ev/key= {:type :key} \q)))
(is (not (ev/key= {:type :key :modifiers #{:ctrl}} \c #{:ctrl})))))
;; =============================================================================
;; EVENT EDGE CASES
;; =============================================================================
(deftest batch-edge-cases-test
(testing "batch with all nils"
(is (nil? (ev/batch nil nil nil))))
(testing "batch with single event"
(let [event (ev/batch {:type :msg1})]
(is (= :batch (:type event)))
(is (= 1 (count (:events event))))))
(testing "batch with no arguments"
(is (nil? (ev/batch))))
(testing "batch with many events"
(let [event (ev/batch {:type :t1} {:type :t2} {:type :t3} {:type :t4} {:type :t5})]
(is (= 5 (count (:events event))))
(is (= :batch (:type event))))))
(deftest sequential-edge-cases-test
(testing "sequential with all nils"
(is (nil? (ev/sequential nil nil nil))))
(testing "sequential with single event"
(let [event (ev/sequential {:type :msg1})]
(is (= :sequential (:type event)))
(is (= 1 (count (:events event))))))
(testing "sequential with no arguments"
(is (nil? (ev/sequential)))))
(deftest delayed-event-edge-cases-test
(testing "delayed-event with zero delay"
(let [event (ev/delayed-event 0 {:type :immediate})]
(is (= :delayed-event (:type event)))
(is (= 0 (:ms event)))))
(testing "delayed-event with various delays"
(is (= 1 (:ms (ev/delayed-event 1 {:type :t1}))))
(is (= 1000 (:ms (ev/delayed-event 1000 {:type :t2}))))
(is (= 999999999 (:ms (ev/delayed-event 999999999 {:type :t3})))))
(testing "delayed-event with complex message"
(let [event (ev/delayed-event 0 {:type :tick :id 1 :data [1 2 3]})]
(is (= {:type :tick :id 1 :data [1 2 3]} (:event event))))))
;; =============================================================================
;; 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-events-test
(testing "update function handles unknown events gracefully"
(let [update-fn (fn [{:keys [model event]}]
(cond
(ev/key= event \q) {:model model :events [(ev/quit)]}
:else {:model model}))]
;; Unknown key
(let [{:keys [model]} (update-fn {:model {:n 0} :event {:type :key :key \x}})]
(is (= {:n 0} model)))
;; Unknown event type
(let [{:keys [model]} (update-fn {:model {:n 0} :event {:type :unknown :message "test"}})]
(is (= {:n 0} model)))
;; Event with no type
(let [{:keys [model]} (update-fn {:model {:n 0} :event {}})]
(is (= {:n 0} model))))))
(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 [{:keys [model event]}]
(if (ev/key= event :up)
{:model (-> model
(update :count inc)
(update :history conj (:count model)))}
{:model model}))]
(let [r1 (update-fn {:model complex-model :event {:type :key :key :up}})
r2 (update-fn {:model (:model r1) :event {:type :key :key :up}})]
(is (= 1 (:count (:model r1))))
(is (= [0] (:history (:model r1))))
(is (= 2 (:count (:model r2))))
(is (= [0 1] (:history (:model r2))))
;; Other fields unchanged
(is (= ["a" "b" "c"] (:items (:model r2))))
(is (= 42 (get-in (:model r2) [: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")))))