(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")))))