343 lines
14 KiB
Clojure
343 lines
14 KiB
Clojure
(ns tui.render-test
|
|
"Unit tests for hiccup rendering."
|
|
(:require [clojure.test :refer [deftest testing is]]
|
|
[clojure.string :as str]
|
|
[tui.render :as render]
|
|
[tui.ansi :as ansi]))
|
|
|
|
;; === Text Rendering Tests ===
|
|
|
|
(deftest render-plain-text-test
|
|
(testing "renders plain strings"
|
|
(is (= "hello" (render/render "hello")))
|
|
(is (= "world" (render/render "world"))))
|
|
|
|
(testing "renders numbers as strings"
|
|
(is (= "42" (render/render 42)))
|
|
(is (= "3.14" (render/render 3.14))))
|
|
|
|
(testing "renders nil as empty string"
|
|
(is (= "" (render/render nil)))))
|
|
|
|
(deftest render-text-element-test
|
|
(testing "renders :text element with string child"
|
|
(is (= "hello" (render/render [:text "hello"]))))
|
|
|
|
(testing "renders :text element with multiple children"
|
|
(is (= "hello world" (render/render [:text "hello" " " "world"]))))
|
|
|
|
(testing "renders nested text"
|
|
(is (= "42" (render/render [:text 42])))))
|
|
|
|
(deftest render-styled-text-test
|
|
(testing "renders bold text"
|
|
(let [result (render/render [:text {:bold true} "bold"])]
|
|
(is (str/includes? result "bold"))
|
|
(is (str/includes? result "\u001b[")) ; Contains ANSI escape
|
|
(is (str/includes? result "1m")))) ; Bold code
|
|
|
|
(testing "renders colored text"
|
|
(let [result (render/render [:text {:fg :red} "red"])]
|
|
(is (str/includes? result "red"))
|
|
(is (str/includes? result "31m")))) ; Red foreground code
|
|
|
|
(testing "renders multiple styles"
|
|
(let [result (render/render [:text {:bold true :fg :green} "styled"])]
|
|
(is (str/includes? result "styled"))
|
|
(is (str/includes? result "1")) ; Bold
|
|
(is (str/includes? result "32"))))) ; Green
|
|
|
|
;; === Layout Tests ===
|
|
|
|
(deftest render-row-test
|
|
(testing "renders row with children horizontally"
|
|
(is (= "ab" (render/render [:row "a" "b"])))
|
|
(is (= "abc" (render/render [:row "a" "b" "c"]))))
|
|
|
|
(testing "renders row with gap"
|
|
(is (= "a b" (render/render [:row {:gap 1} "a" "b"])))
|
|
(is (= "a b" (render/render [:row {:gap 2} "a" "b"]))))
|
|
|
|
(testing "renders nested elements in row"
|
|
(is (= "hello world" (render/render [:row [:text "hello"] " " [:text "world"]])))))
|
|
|
|
(deftest render-col-test
|
|
(testing "renders col with children vertically"
|
|
(is (= "a\nb" (render/render [:col "a" "b"])))
|
|
(is (= "a\nb\nc" (render/render [:col "a" "b" "c"]))))
|
|
|
|
(testing "renders col with gap"
|
|
(is (= "a\n\nb" (render/render [:col {:gap 1} "a" "b"])))
|
|
(is (= "a\n\n\nb" (render/render [:col {:gap 2} "a" "b"]))))
|
|
|
|
(testing "renders nested elements in col"
|
|
(is (= "line1\nline2" (render/render [:col [:text "line1"] [:text "line2"]])))))
|
|
|
|
(deftest render-nested-layout-test
|
|
(testing "renders row inside col"
|
|
(is (= "a b\nc d" (render/render [:col
|
|
[:row "a" " " "b"]
|
|
[:row "c" " " "d"]]))))
|
|
|
|
(testing "renders col inside 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"]])))))
|
|
|
|
;; === Box Tests ===
|
|
|
|
(deftest render-box-test
|
|
(testing "renders box with content"
|
|
(let [result (render/render [:box "hello"])]
|
|
(is (str/includes? result "hello"))
|
|
(is (str/includes? result "─")) ; Horizontal border
|
|
(is (str/includes? result "│")))) ; Vertical border
|
|
|
|
(testing "renders box with title"
|
|
(let [result (render/render [:box {:title "Title"} "content"])]
|
|
(is (str/includes? result "Title"))
|
|
(is (str/includes? result "content"))))
|
|
|
|
(testing "renders box with different border styles"
|
|
(let [rounded (render/render [:box {:border :rounded} "x"])
|
|
single (render/render [:box {:border :single} "x"])
|
|
double (render/render [:box {:border :double} "x"])
|
|
ascii (render/render [:box {:border :ascii} "x"])]
|
|
(is (str/includes? rounded "╭"))
|
|
(is (str/includes? single "┌"))
|
|
(is (str/includes? double "╔"))
|
|
(is (str/includes? ascii "+")))))
|
|
|
|
(deftest render-box-padding-test
|
|
(testing "renders box with numeric padding"
|
|
(let [result (render/render [:box {:padding 1} "x"])
|
|
lines (str/split result #"\n")]
|
|
;; Should have empty lines for vertical padding
|
|
(is (> (count lines) 3))))
|
|
|
|
(testing "renders box with vector padding"
|
|
(let [result (render/render [:box {:padding [0 2]} "x"])]
|
|
;; Should have horizontal padding (spaces around content)
|
|
(is (str/includes? result " x ")))))
|
|
|
|
;; === Space Tests ===
|
|
|
|
(deftest render-space-test
|
|
(testing "renders space with default size"
|
|
(is (= " " (render/render [:space]))))
|
|
|
|
(testing "renders space with custom width"
|
|
(is (= " " (render/render [:space {:width 3}]))))
|
|
|
|
(testing "renders space with custom height"
|
|
(is (= " \n " (render/render [:space {:height 2}]))))
|
|
|
|
(testing "renders space with width and height"
|
|
(is (= " \n " (render/render [:space {:width 2 :height 2}])))))
|
|
|
|
;; === 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")))
|
|
(is (= [:text {:bold true} "bold"] (render/text {:bold true} "bold"))))
|
|
|
|
(testing "row function creates row element"
|
|
(is (= [:row {} "a" "b"] (render/row "a" "b")))
|
|
(is (= [:row {:gap 1} "a" "b"] (render/row {:gap 1} "a" "b"))))
|
|
|
|
(testing "col function creates col element"
|
|
(is (= [:col {} "a" "b"] (render/col "a" "b")))
|
|
(is (= [:col {:gap 1} "a" "b"] (render/col {:gap 1} "a" "b"))))
|
|
|
|
(testing "box function creates box element"
|
|
(is (= [:box {} "content"] (render/box "content")))
|
|
(is (= [:box {:border :single} "x"] (render/box {:border :single} "x")))))
|