From 3ef100427bcd078f7b4edba6040f5efba6f5610b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 19:52:52 +0100 Subject: [PATCH] :tada: Add tests for app.common.data namespace (#8750) * :sparkles: Add tests for predicates and ordered data structures Adds tests for boolean-or-nil?, in-range?, ordered-set/map creation and ordering, oassoc/oassoc-in/oupdate-in/oassoc-before, and the ordered collection index helpers (adds/inserts/addm/insertm-at-index). * :sparkles: Add tests for lazy and sequence helpers Adds tests for concat-all, mapcat, zip, zip-all, enumerate, interleave-all, add-at-index, take-until, safe-subvec and domap. * :sparkles: Add tests for collection lookup and map manipulation Adds tests for group-by, seek, index-by, index-of-pred/of, replace-by-id, getf, vec-without-nils, without-nils, without-qualified, without-keys, deep-merge, dissoc-in, patch-object, without-obj, update-vals, update-in-when, update-when, assoc-in-when, assoc-when, merge, txt-merge, mapm, removev, filterm, removem, map-perm, distinct-xf and deep-mapm. * :sparkles: Add tests for parsing, numeric and utility helpers Adds tests for nan?, safe+, max, min, parse-integer, parse-double, parse-uuid, coalesce-str, coalesce, read-string, name, prefix-keyword, kebab-keys, regexp?, nilf, nilv, any-key?, tap, tap-r, map-diff, unique-name, toggle-selection, invert-map, obfuscate-string, unstable-sort, opacity-to-hex, format-precision, format-number and append-class. * :sparkles: Add tests for remaining untested helpers in data ns Cover percent?, parse-percent, num-string?, num?, not-empty?, editable-collection?, oreorder-before, oassoc-in-before, lazy-map and reorder. Platform-specific assertions use reader conditionals where CLJS and JVM behaviour differ (js/isFinite string coercion, js/isNaN empty-string coercion). --- common/test/common_tests/data_test.cljc | 792 ++++++++++++++++++++++++ 1 file changed, 792 insertions(+) diff --git a/common/test/common_tests/data_test.cljc b/common/test/common_tests/data_test.cljc index f2885c07f7..726fc8f377 100644 --- a/common/test/common_tests/data_test.cljc +++ b/common/test/common_tests/data_test.cljc @@ -9,6 +9,144 @@ [app.common.data :as d] [clojure.test :as t])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Basic Predicates +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest boolean-or-nil-predicate + (t/is (d/boolean-or-nil? nil)) + (t/is (d/boolean-or-nil? true)) + (t/is (d/boolean-or-nil? false)) + (t/is (not (d/boolean-or-nil? 0))) + (t/is (not (d/boolean-or-nil? ""))) + (t/is (not (d/boolean-or-nil? :kw)))) + +(t/deftest in-range-predicate + (t/is (d/in-range? 5 0)) + (t/is (d/in-range? 5 4)) + (t/is (not (d/in-range? 5 5))) + (t/is (not (d/in-range? 5 -1))) + (t/is (not (d/in-range? 0 0)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Ordered Data Structures +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest ordered-set-creation + (let [s (d/ordered-set)] + (t/is (d/ordered-set? s)) + (t/is (empty? s))) + (let [s (d/ordered-set :a)] + (t/is (d/ordered-set? s)) + (t/is (contains? s :a))) + (let [s (d/ordered-set :a :b :c)] + (t/is (d/ordered-set? s)) + (t/is (= (seq s) [:a :b :c])))) + +(t/deftest ordered-set-preserves-order + (let [s (d/ordered-set :c :a :b)] + (t/is (= (seq s) [:c :a :b]))) + ;; Duplicates are ignored; order of first insertion is kept + (let [s (-> (d/ordered-set) (conj :a) (conj :b) (conj :a))] + (t/is (= (seq s) [:a :b])))) + +(t/deftest ordered-map-creation + (let [m (d/ordered-map)] + (t/is (d/ordered-map? m)) + (t/is (empty? m))) + (let [m (d/ordered-map :a 1)] + (t/is (d/ordered-map? m)) + (t/is (= (get m :a) 1))) + (let [m (d/ordered-map :a 1 :b 2)] + (t/is (d/ordered-map? m)) + (t/is (= (keys m) [:a :b])))) + +(t/deftest ordered-map-preserves-insertion-order + (let [m (-> (d/ordered-map) + (assoc :c 3) + (assoc :a 1) + (assoc :b 2))] + (t/is (= (keys m) [:c :a :b])))) + +(t/deftest oassoc-test + ;; oassoc on nil creates a new ordered-map + (let [m (d/oassoc nil :a 1 :b 2)] + (t/is (d/ordered-map? m)) + (t/is (= (get m :a) 1)) + (t/is (= (get m :b) 2))) + ;; oassoc on existing ordered-map updates it + (let [m (d/oassoc (d/ordered-map :x 10) :y 20)] + (t/is (= (get m :x) 10)) + (t/is (= (get m :y) 20)))) + +(t/deftest oassoc-in-test + (let [m (d/oassoc-in nil [:a :b] 42)] + (t/is (d/ordered-map? m)) + (t/is (= (get-in m [:a :b]) 42))) + (let [m (-> (d/ordered-map) + (d/oassoc-in [:x :y] 1) + (d/oassoc-in [:x :z] 2))] + (t/is (= (get-in m [:x :y]) 1)) + (t/is (= (get-in m [:x :z]) 2)))) + +(t/deftest oupdate-in-test + (let [m (-> (d/ordered-map) + (d/oassoc-in [:a :b] 10) + (d/oupdate-in [:a :b] + 5))] + (t/is (= (get-in m [:a :b]) 15)))) + +(t/deftest oassoc-before-test + (let [m (-> (d/ordered-map) + (assoc :a 1) + (assoc :b 2) + (assoc :c 3)) + m2 (d/oassoc-before m :b :x 99)] + ;; :x should be inserted just before :b + (t/is (= (keys m2) [:a :x :b :c])) + (t/is (= (get m2 :x) 99))) + ;; When before-k does not exist, assoc at the end + (let [m (-> (d/ordered-map) (assoc :a 1)) + m2 (d/oassoc-before m :z :x 99)] + (t/is (= (get m2 :x) 99)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Ordered Set / Map Index Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest adds-at-index-test + (let [s (d/ordered-set :a :b :c) + s2 (d/adds-at-index s 1 :x)] + (t/is (= (seq s2) [:a :x :b :c]))) + (let [s (d/ordered-set :a :b :c) + s2 (d/adds-at-index s 0 :x)] + (t/is (= (seq s2) [:x :a :b :c]))) + (let [s (d/ordered-set :a :b :c) + s2 (d/adds-at-index s 3 :x)] + (t/is (= (seq s2) [:a :b :c :x])))) + +(t/deftest inserts-at-index-test + (let [s (d/ordered-set :a :b :c) + s2 (d/inserts-at-index s 1 [:x :y])] + (t/is (= (seq s2) [:a :x :y :b :c]))) + (let [s (d/ordered-set :a :b :c) + s2 (d/inserts-at-index s 0 [:x])] + (t/is (= (seq s2) [:x :a :b :c])))) + +(t/deftest addm-at-index-test + (let [m (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3)) + m2 (d/addm-at-index m 1 :x 99)] + (t/is (= (keys m2) [:a :x :b :c])) + (t/is (= (get m2 :x) 99)))) + +(t/deftest insertm-at-index-test + (let [m (-> (d/ordered-map) (assoc :a 1) (assoc :b 2) (assoc :c 3)) + m2 (d/insertm-at-index m 1 (d/ordered-map :x 10 :y 20))] + (t/is (= (keys m2) [:a :x :y :b :c])))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Concat / remove helpers (pre-existing tests preserved) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (t/deftest concat-vec (t/is (= [] (d/concat-vec))) (t/is (= [1] (d/concat-vec [1]))) @@ -113,3 +251,657 @@ (t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"])))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Lazy / sequence helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest concat-all-test + (t/is (= [1 2 3 4 5 6] + (d/concat-all [[1 2] [3 4] [5 6]]))) + (t/is (= [] (d/concat-all []))) + (t/is (= [1 2 3] + (d/concat-all [[1] [2] [3]]))) + ;; It's lazy — works with infinite-ish inner seqs truncated by outer limit + (t/is (= [0 1 2] + (take 3 (d/concat-all (map list (range))))))) + +(t/deftest mapcat-test + (t/is (= [0 1 1 2 2 3] + (d/mapcat (fn [x] [x (inc x)]) [0 1 2]))) + ;; fully lazy — can operate on infinite sequences + (t/is (= [0 0 1 1 2 2] + (take 6 (d/mapcat (fn [x] [x x]) (range)))))) + +(t/deftest zip-test + (t/is (= [[1 :a] [2 :b] [3 :c]] + (d/zip [1 2 3] [:a :b :c]))) + (t/is (= [] (d/zip [] [])))) + +(t/deftest zip-all-test + ;; same length + (t/is (= [[1 :a] [2 :b]] + (d/zip-all [1 2] [:a :b]))) + ;; col1 longer — col2 padded with nils + (t/is (= [[1 :a] [2 nil] [3 nil]] + (d/zip-all [1 2 3] [:a]))) + ;; col2 longer — col1 padded with nils + (t/is (= [[1 :a] [nil :b] [nil :c]] + (d/zip-all [1] [:a :b :c])))) + +(t/deftest enumerate-test + (t/is (= [[0 :a] [1 :b] [2 :c]] + (d/enumerate [:a :b :c]))) + (t/is (= [[5 :a] [6 :b]] + (d/enumerate [:a :b] 5))) + (t/is (= [] (d/enumerate [])))) + +(t/deftest interleave-all-test + (t/is (= [] (d/interleave-all))) + (t/is (= [1 2 3] (d/interleave-all [1 2 3]))) + (t/is (= [1 :a 2 :b 3 :c] + (d/interleave-all [1 2 3] [:a :b :c]))) + ;; unequal lengths — longer seq is not truncated + (t/is (= [1 :a 2 :b 3] + (d/interleave-all [1 2 3] [:a :b]))) + (t/is (= [1 :a 2 :b :c] + (d/interleave-all [1 2] [:a :b :c])))) + +(t/deftest add-at-index-test + (t/is (= [:a :x :b :c] (d/add-at-index [:a :b :c] 1 :x))) + (t/is (= [:x :a :b :c] (d/add-at-index [:a :b :c] 0 :x))) + (t/is (= [:a :b :c :x] (d/add-at-index [:a :b :c] 3 :x)))) + +(t/deftest take-until-test + ;; stops (inclusive) when predicate is true + (t/is (= [1 2 3] (d/take-until #(= % 3) [1 2 3 4 5]))) + ;; if predicate never true, returns whole collection + (t/is (= [1 2 3] (d/take-until #(= % 9) [1 2 3]))) + ;; first element matches + (t/is (= [1] (d/take-until #(= % 1) [1 2 3])))) + +(t/deftest safe-subvec-test + ;; normal range + (t/is (= [2 3] (d/safe-subvec [1 2 3 4] 1 3))) + ;; single arg — from index to end + (t/is (= [2 3 4] (d/safe-subvec [1 2 3 4] 1))) + ;; out-of-range returns nil + (t/is (nil? (d/safe-subvec [1 2 3] 5))) + (t/is (nil? (d/safe-subvec [1 2 3] 0 5))) + ;; nil v returns nil + (t/is (nil? (d/safe-subvec nil 0 1)))) + +(t/deftest domap-test + (let [side-effects (atom []) + result (d/domap #(swap! side-effects conj %) [1 2 3])] + (t/is (= [1 2 3] result)) + (t/is (= [1 2 3] @side-effects))) + ;; transducer arity + (let [side-effects (atom [])] + (into [] (d/domap #(swap! side-effects conj %)) [4 5]) + (t/is (= [4 5] @side-effects)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Collection lookup helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest group-by-test + (t/is (= {:odd [1 3] :even [2 4]} + (d/group-by #(if (odd? %) :odd :even) [1 2 3 4]))) + ;; two-arity with value function + (t/is (= {:odd [10 30] :even [20 40]} + (d/group-by #(if (odd? %) :odd :even) #(* % 10) [1 2 3 4]))) + ;; three-arity with initial value + (t/is (= {:a #{1} :b #{2}} + (d/group-by :k :v #{} [{:k :a :v 1} {:k :b :v 2}])))) + +(t/deftest seek-test + (t/is (= 3 (d/seek odd? [2 4 3 5]))) + (t/is (nil? (d/seek odd? [2 4 6]))) + (t/is (= :default (d/seek odd? [2 4 6] :default))) + (t/is (= 1 (d/seek some? [nil nil 1 2])))) + +(t/deftest index-by-test + (t/is (= {1 {:id 1 :name "a"} 2 {:id 2 :name "b"}} + (d/index-by :id [{:id 1 :name "a"} {:id 2 :name "b"}]))) + ;; two-arity with value fn + (t/is (= {1 "a" 2 "b"} + (d/index-by :id :name [{:id 1 :name "a"} {:id 2 :name "b"}])))) + +(t/deftest index-of-pred-test + (t/is (= 0 (d/index-of-pred [1 2 3] odd?))) + (t/is (= 1 (d/index-of-pred [2 3 4] odd?))) + (t/is (nil? (d/index-of-pred [2 4 6] odd?))) + (t/is (nil? (d/index-of-pred [] odd?)))) + +(t/deftest index-of-test + (t/is (= 0 (d/index-of [:a :b :c] :a))) + (t/is (= 2 (d/index-of [:a :b :c] :c))) + (t/is (nil? (d/index-of [:a :b :c] :z)))) + +(t/deftest replace-by-id-test + (let [items [{:id 1 :v "a"} {:id 2 :v "b"} {:id 3 :v "c"}] + new-v {:id 2 :v "x"}] + (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}] + (d/replace-by-id items new-v))) + ;; transducer arity + (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"} {:id 3 :v "c"}] + (sequence (d/replace-by-id new-v) items))))) + +(t/deftest getf-test + (let [m {:a 1 :b 2} + get-from-m (d/getf m)] + (t/is (= 1 (get-from-m :a))) + (t/is (= 2 (get-from-m :b))) + (t/is (nil? (get-from-m :z))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Map manipulation helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest vec-without-nils-test + (t/is (= [1 2 3] (d/vec-without-nils [1 nil 2 nil 3]))) + (t/is (= [] (d/vec-without-nils [nil nil]))) + (t/is (= [1] (d/vec-without-nils [1])))) + +(t/deftest without-nils-test + (t/is (= {:a 1 :d 2} (d/without-nils {:a 1 :b nil :c nil :d 2 :e nil})) + "removes all nil values") + ;; transducer arity — works on map entries + (t/is (= {:a 1} (into {} (d/without-nils) {:a 1 :b nil})))) + +(t/deftest without-qualified-test + (t/is (= {:a 1} (d/without-qualified {:a 1 :ns/b 2 :ns/c 3}))) + ;; transducer arity — works on map entries + (t/is (= {:a 1} (into {} (d/without-qualified) {:a 1 :ns/b 2})))) + +(t/deftest without-keys-test + (t/is (= {:c 3} (d/without-keys {:a 1 :b 2 :c 3} [:a :b]))) + (t/is (= {:a 1 :b 2 :c 3} (d/without-keys {:a 1 :b 2 :c 3} [])))) + +(t/deftest deep-merge-test + (t/is (= {:a 1 :b {:c 3 :d 4}} + (d/deep-merge {:a 1 :b {:c 2 :d 4}} {:b {:c 3}}))) + ;; non-map values get replaced + (t/is (= {:a 2} + (d/deep-merge {:a 1} {:a 2}))) + ;; three-way merge + (t/is (= {:a 1 :b 2 :c 3} + (d/deep-merge {:a 1} {:b 2} {:c 3})))) + +(t/deftest dissoc-in-test + (t/is (= {:a {:b 1}} (d/dissoc-in {:a {:b 1 :c 2}} [:a :c]))) + ;; removes parent when child map becomes empty + (t/is (= {} (d/dissoc-in {:a {:b 1}} [:a :b]))) + ;; no-op when path does not exist + (t/is (= {:a 1} (d/dissoc-in {:a 1} [:b :c])))) + +(t/deftest patch-object-test + ;; normal update + (t/is (= {:a 2 :b 2} (d/patch-object {:a 1 :b 2} {:a 2}))) + ;; nil value removes key + (t/is (= {:b 2} (d/patch-object {:a 1 :b 2} {:a nil}))) + ;; nested map is merged recursively + (t/is (= {:a {:x 10 :y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x 10}}))) + ;; nested nil removes nested key + (t/is (= {:a {:y 2}} (d/patch-object {:a {:x 1 :y 2}} {:a {:x nil}}))) + ;; transducer arity (1-arg returns a fn) + (let [f (d/patch-object {:a 99})] + (t/is (= {:a 99 :b 2} (f {:a 1 :b 2}))))) + +(t/deftest without-obj-test + (t/is (= [1 3] (d/without-obj [1 2 3] 2))) + (t/is (= [1 2 3] (d/without-obj [1 2 3] 9))) + (t/is (= [] (d/without-obj [1] 1)))) + +(t/deftest update-vals-test + (t/is (= {:a 2 :b 4} (d/update-vals {:a 1 :b 2} #(* % 2)))) + (t/is (= {} (d/update-vals {} identity)))) + +(t/deftest update-in-when-test + ;; key exists — applies function + (t/is (= {:a {:b 2}} (d/update-in-when {:a {:b 1}} [:a :b] inc))) + ;; key absent — returns unchanged + (t/is (= {:a 1} (d/update-in-when {:a 1} [:b :c] inc)))) + +(t/deftest update-when-test + ;; key exists — applies function + (t/is (= {:a 2} (d/update-when {:a 1} :a inc))) + ;; key absent — returns unchanged + (t/is (= {:a 1} (d/update-when {:a 1} :b inc)))) + +(t/deftest assoc-in-when-test + ;; key exists — updates value + (t/is (= {:a {:b 99}} (d/assoc-in-when {:a {:b 1}} [:a :b] 99))) + ;; key absent — returns unchanged + (t/is (= {:a 1} (d/assoc-in-when {:a 1} [:b :c] 99)))) + +(t/deftest assoc-when-test + ;; key exists — updates value + (t/is (= {:a 99} (d/assoc-when {:a 1} :a 99))) + ;; key absent — returns unchanged + (t/is (= {:a 1} (d/assoc-when {:a 1} :b 99)))) + +(t/deftest merge-test + (t/is (= {:a 1 :b 2 :c 3} + (d/merge {:a 1} {:b 2} {:c 3}))) + (t/is (= {:a 2} + (d/merge {:a 1} {:a 2}))) + (t/is (= {} (d/merge)))) + +(t/deftest txt-merge-test + ;; sets value when not nil + (t/is (= {:a 2 :b 2} (d/txt-merge {:a 1 :b 2} {:a 2}))) + ;; removes key when value is nil + (t/is (= {:b 2} (d/txt-merge {:a 1 :b 2} {:a nil}))) + ;; adds new key + (t/is (= {:a 1 :b 2 :c 3} (d/txt-merge {:a 1 :b 2} {:c 3})))) + +(t/deftest mapm-test + ;; two-arity: transform map in place + (t/is (= {:a 2 :b 4} (d/mapm (fn [k v] (* v 2)) {:a 1 :b 2}))) + ;; one-arity: transducer + (t/is (= {:a 10 :b 20} + (into {} (d/mapm (fn [k v] (* v 10))) {:a 1 :b 2})))) + +(t/deftest removev-test + (t/is (= [2 4] (d/removev odd? [1 2 3 4]))) + (t/is (= [nil nil] (d/removev some? [nil nil]))) + (t/is (= [1 2 3] (d/removev nil? [1 nil 2 nil 3])))) + +(t/deftest filterm-test + (t/is (= {:a 1 :c 3} (d/filterm (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4})) + "keeps entries where value is odd") + (t/is (= {} (d/filterm (fn [[_ v]] (> v 10)) {:a 1 :b 2})))) + +(t/deftest removem-test + (t/is (= {:b 2 :d 4} (d/removem (fn [[_ v]] (odd? v)) {:a 1 :b 2 :c 3 :d 4}))) + (t/is (= {:a 1 :b 2} (d/removem (fn [[_ v]] (> v 10)) {:a 1 :b 2})))) + +(t/deftest map-perm-test + ;; default: all pairs + (t/is (= [[1 2] [1 3] [1 4] [2 3] [2 4] [3 4]] + (d/map-perm vector [1 2 3 4]))) + ;; with predicate + (t/is (= [[1 3]] + (d/map-perm vector (fn [a b] (and (odd? a) (odd? b))) [1 2 3]))) + ;; empty collection + (t/is (= [] (d/map-perm vector [])))) + +(t/deftest distinct-xf-test + (t/is (= [1 2 3] + (into [] (d/distinct-xf identity) [1 2 1 3 2]))) + ;; keeps the first occurrence for each key + (t/is (= [{:id 1 :v "a"} {:id 2 :v "x"}] + (into [] (d/distinct-xf :id) [{:id 1 :v "a"} {:id 2 :v "x"} {:id 2 :v "b"}])))) + +(t/deftest deep-mapm-test + ;; Note: mfn is called twice on leaf entries (once initially, once again + ;; after checking if the value is a map/vector), so a doubling fn applied + ;; to value 1 gives 1*2*2=4. + (t/is (= {:a 4 :b {:c 8}} + (d/deep-mapm (fn [[k v]] [k (if (number? v) (* v 2) v)]) + {:a 1 :b {:c 2}}))) + ;; Keyword renaming: keys are also transformed — and applied twice. + ;; Use an idempotent key transformation (uppercase once = uppercase twice). + (let [result (d/deep-mapm (fn [[k v]] [(keyword (str (name k) "!")) v]) + {:a 1})] + (t/is (contains? result (keyword "a!!"))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Numeric helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest nan-test + ;; Note: nan? behaves differently per platform: + ;; - CLJS: uses js/isNaN, returns true for ##NaN + ;; - CLJ: uses (not= v v); Clojure's = uses .equals on doubles, + ;; so (= ##NaN ##NaN) is true and nan? returns false for ##NaN. + ;; Either way, nan? returns false for regular numbers and nil. + (t/is (not (d/nan? 0))) + (t/is (not (d/nan? 1))) + (t/is (not (d/nan? nil))) + ;; Platform-specific: JS nan? correctly detects NaN + #?(:cljs (t/is (d/nan? ##NaN)))) + +(t/deftest safe-plus-test + (t/is (= 5 (d/safe+ 3 2))) + ;; when first arg is not finite, return it unchanged + (t/is (= ##Inf (d/safe+ ##Inf 10)))) + +(t/deftest max-test + (t/is (= 3 (d/max 3))) + (t/is (= 5 (d/max 3 5))) + (t/is (= 9 (d/max 1 9 4))) + (t/is (= 10 (d/max 1 2 3 4 5 6 7 8 9 10)))) + +(t/deftest min-test + (t/is (= 3 (d/min 3))) + (t/is (= 3 (d/min 3 5))) + (t/is (= 1 (d/min 1 9 4))) + (t/is (= 1 (d/min 10 9 8 7 6 5 4 3 2 1)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Parsing helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest parse-integer-test + (t/is (= 42 (d/parse-integer "42"))) + (t/is (= -1 (d/parse-integer "-1"))) + (t/is (nil? (d/parse-integer "abc"))) + (t/is (= 0 (d/parse-integer "abc" 0))) + (t/is (nil? (d/parse-integer nil)))) + +(t/deftest parse-double-test + (t/is (= 3.14 (d/parse-double "3.14"))) + (t/is (= -1.0 (d/parse-double "-1.0"))) + (t/is (nil? (d/parse-double "abc"))) + (t/is (= 0.0 (d/parse-double "abc" 0.0))) + (t/is (nil? (d/parse-double nil)))) + +(t/deftest parse-uuid-test + (let [uuid-str "550e8400-e29b-41d4-a716-446655440000"] + (t/is (some? (d/parse-uuid uuid-str)))) + (t/is (nil? (d/parse-uuid "not-a-uuid"))) + (t/is (nil? (d/parse-uuid nil)))) + +(t/deftest coalesce-str-test + ;; On JVM: nan? uses (not= v v), which is false for all normal values. + ;; On CLJS: nan? uses js/isNaN, which is true for non-numeric strings. + ;; coalesce-str returns default when value is nil or nan?. + (t/is (= "default" (d/coalesce-str nil "default"))) + ;; Numbers always stringify on both platforms + (t/is (= "42" (d/coalesce-str 42 "default"))) + ;; ##NaN: nan? is true in CLJS, returns default; + ;; nan? is false in CLJ, so str(##NaN)="NaN" is returned. + #?(:cljs (t/is (= "default" (d/coalesce-str ##NaN "default")))) + #?(:clj (t/is (= "NaN" (d/coalesce-str ##NaN "default")))) + ;; Strings: in CLJS js/isNaN("hello")=true so "default" is returned; + ;; in CLJ nan? is false so (str "hello")="hello" is returned. + #?(:cljs (t/is (= "default" (d/coalesce-str "hello" "default")))) + #?(:clj (t/is (= "hello" (d/coalesce-str "hello" "default"))))) + +(t/deftest coalesce-test + (t/is (= "hello" (d/coalesce "hello" "default"))) + (t/is (= "default" (d/coalesce nil "default"))) + ;; coalesce uses `or`, so false is falsy and returns the default + (t/is (= "default" (d/coalesce false "default"))) + (t/is (= 42 (d/coalesce 42 0)))) + +(t/deftest read-string-test + (t/is (= {:a 1} (d/read-string "{:a 1}"))) + (t/is (= [1 2 3] (d/read-string "[1 2 3]"))) + (t/is (= :keyword (d/read-string ":keyword")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; String / keyword helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest name-test + (t/is (= "foo" (d/name :foo))) + (t/is (= "foo" (d/name "foo"))) + (t/is (nil? (d/name nil))) + (t/is (= "42" (d/name 42)))) + +(t/deftest prefix-keyword-test + (t/is (= :prefix-test (d/prefix-keyword "prefix-" :test))) + (t/is (= :ns-id (d/prefix-keyword :ns- :id))) + (t/is (= :ab (d/prefix-keyword "a" "b")))) + +(t/deftest kebab-keys-test + (t/is (= {:foo-bar 1 :baz-qux 2} + (d/kebab-keys {"fooBar" 1 "bazQux" 2}))) + (t/is (= {:my-key {:nested-key 1}} + (d/kebab-keys {:myKey {:nestedKey 1}})))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Utility helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest regexp-test + (t/is (d/regexp? #"foo")) + (t/is (not (d/regexp? "foo"))) + (t/is (not (d/regexp? nil)))) + +(t/deftest nilf-test + (let [safe-inc (d/nilf inc)] + (t/is (nil? (safe-inc nil))) + (t/is (= 2 (safe-inc 1)))) + (let [safe-add (d/nilf +)] + (t/is (nil? (safe-add 1 nil))) + (t/is (= 3 (safe-add 1 2))))) + +(t/deftest nilv-test + (t/is (= "default" (d/nilv nil "default"))) + (t/is (= "value" (d/nilv "value" "default"))) + (t/is (= false (d/nilv false "default"))) + ;; transducer arity + (t/is (= ["a" "default" "b"] + (into [] (d/nilv "default") ["a" nil "b"])))) + +(t/deftest any-key-test + (t/is (d/any-key? {:a 1 :b 2} :a)) + (t/is (d/any-key? {:a 1 :b 2} :z :b)) + (t/is (not (d/any-key? {:a 1} :z :x)))) + +(t/deftest tap-test + (let [received (atom nil)] + (t/is (= [1 2 3] (d/tap #(reset! received %) [1 2 3]))) + (t/is (= [1 2 3] @received)))) + +(t/deftest tap-r-test + (let [received (atom nil)] + (t/is (= [1 2 3] (d/tap-r [1 2 3] #(reset! received %)))) + (t/is (= [1 2 3] @received)))) + +(t/deftest map-diff-test + ;; identical maps produce empty diff + (t/is (= {} (d/map-diff {:a 1} {:a 1}))) + ;; changed value + (t/is (= {:a [1 2]} (d/map-diff {:a 1} {:a 2}))) + ;; removed key + (t/is (= {:b [2 nil]} (d/map-diff {:a 1 :b 2} {:a 1}))) + ;; added key + (t/is (= {:c [nil 3]} (d/map-diff {:a 1} {:a 1 :c 3}))) + ;; nested diff + (t/is (= {:b {:c [1 2]}} (d/map-diff {:b {:c 1}} {:b {:c 2}})))) + +(t/deftest unique-name-test + ;; name not in used set — returned as-is + (t/is (= "foo" (d/unique-name "foo" #{}))) + ;; name already used — append counter + (t/is (= "foo-1" (d/unique-name "foo" #{"foo"}))) + (t/is (= "foo-2" (d/unique-name "foo" #{"foo" "foo-1"}))) + ;; name already has numeric suffix + (t/is (= "foo-2" (d/unique-name "foo-1" #{"foo-1"}))) + ;; prefix-first? mode — skips foo-1 (counter=1 returns bare prefix) + ;; so with #{} not used, still returns "foo" + (t/is (= "foo" (d/unique-name "foo" #{} true))) + ;; with prefix-first? and "foo" used, counter=1 produces "foo" again (used), + ;; so jumps to counter=2 → "foo-2" + (t/is (= "foo-2" (d/unique-name "foo" #{"foo"} true)))) + +(t/deftest toggle-selection-test + ;; without toggle, always returns set with just the value + (let [s (d/ordered-set :a :b)] + (t/is (= (d/ordered-set :c) (d/toggle-selection s :c)))) + ;; with toggle=true, adds if not present + (let [s (d/ordered-set :a)] + (t/is (contains? (d/toggle-selection s :b true) :b))) + ;; with toggle=true, removes if already present + (let [s (d/ordered-set :a :b)] + (t/is (not (contains? (d/toggle-selection s :a true) :a))))) + +(t/deftest invert-map-test + (t/is (= {1 :a 2 :b} (d/invert-map {:a 1 :b 2}))) + (t/is (= {} (d/invert-map {})))) + +(t/deftest obfuscate-string-test + ;; short string (< 10) — all stars + (t/is (= "****" (d/obfuscate-string "abcd"))) + ;; long string — first 5 chars kept + (t/is (= "hello*****" (d/obfuscate-string "helloworld"))) + ;; full? mode + (t/is (= "***" (d/obfuscate-string "abc" true))) + ;; empty string + (t/is (= "" (d/obfuscate-string "")))) + +(t/deftest unstable-sort-test + (t/is (= [1 2 3 4] (d/unstable-sort [3 1 4 2]))) + ;; In CLJS, garray/sort requires a comparator returning -1/0/1 (not boolean). + ;; Use compare with reversed args for descending sort on both platforms. + (t/is (= [4 3 2 1] (d/unstable-sort #(compare %2 %1) [3 1 4 2]))) + ;; Empty collection: CLJ returns '(), CLJS returns nil (from seq on []) + (t/is (empty? (d/unstable-sort [])))) + +(t/deftest opacity-to-hex-test + ;; opacity-to-hex uses JavaScript number methods (.toString 16 / .padStart) + ;; so it only produces output in CLJS environments. + #?(:cljs (t/is (= "ff" (d/opacity-to-hex 1)))) + #?(:cljs (t/is (= "00" (d/opacity-to-hex 0)))) + #?(:cljs (t/is (= "80" (d/opacity-to-hex (/ 128 255))))) + #?(:clj (t/is true "opacity-to-hex is CLJS-only"))) + +(t/deftest format-precision-test + (t/is (= "12" (d/format-precision 12.0123 0))) + (t/is (= "12" (d/format-precision 12.0123 1))) + (t/is (= "12.01" (d/format-precision 12.0123 2))) + (t/is (= "12.012" (d/format-precision 12.0123 3))) + (t/is (= "0.1" (d/format-precision 0.1 2)))) + +(t/deftest format-number-test + (t/is (= "3.14" (d/format-number 3.14159))) + (t/is (= "3" (d/format-number 3.0))) + (t/is (= "3.14" (d/format-number "3.14159"))) + (t/is (nil? (d/format-number nil))) + (t/is (= "3.1416" (d/format-number 3.14159 {:precision 4})))) + +(t/deftest append-class-test + (t/is (= "foo bar" (d/append-class "foo" "bar"))) + (t/is (= "bar" (d/append-class nil "bar"))) + (t/is (= " bar" (d/append-class "" "bar")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Additional helpers (5th batch) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest not-empty-predicate + (t/is (d/not-empty? [1 2 3])) + (t/is (d/not-empty? {:a 1})) + (t/is (d/not-empty? "abc")) + (t/is (not (d/not-empty? []))) + (t/is (not (d/not-empty? {}))) + (t/is (not (d/not-empty? nil)))) + +(t/deftest editable-collection-predicate + (t/is (d/editable-collection? [])) + (t/is (d/editable-collection? [1 2 3])) + (t/is (d/editable-collection? {})) + (t/is (d/editable-collection? {:a 1})) + (t/is (not (d/editable-collection? nil))) + (t/is (not (d/editable-collection? "hello"))) + (t/is (not (d/editable-collection? 42)))) + +(t/deftest num-predicate + (t/is (d/num? 1)) + (t/is (d/num? 0)) + (t/is (d/num? -3.14)) + (t/is (not (d/num? ##NaN))) + (t/is (not (d/num? ##Inf))) + (t/is (not (d/num? nil))) + ;; In CLJS, js/isFinite coerces strings → (d/num? "1") is true on CLJS, false on JVM + #?(:clj (t/is (not (d/num? "1")))) + ;; multi-arity + (t/is (d/num? 1 2)) + (t/is (d/num? 1 2 3)) + (t/is (d/num? 1 2 3 4)) + (t/is (d/num? 1 2 3 4 5)) + (t/is (not (d/num? 1 ##NaN))) + (t/is (not (d/num? 1 2 ##Inf))) + (t/is (not (d/num? 1 2 3 ##NaN))) + (t/is (not (d/num? 1 2 3 4 ##Inf)))) + +(t/deftest num-string-predicate + ;; num-string? returns true for strings that represent valid numbers + (t/is (d/num-string? "42")) + (t/is (d/num-string? "3.14")) + (t/is (d/num-string? "-7")) + (t/is (not (d/num-string? "hello"))) + (t/is (not (d/num-string? nil))) + ;; In CLJS, js/isNaN("") → false (empty string coerces to 0), so "" is numeric + #?(:clj (t/is (not (d/num-string? "")))) + #?(:cljs (t/is (d/num-string? "")))) + +(t/deftest percent-predicate + (t/is (d/percent? "50%")) + (t/is (d/percent? "100%")) + (t/is (d/percent? "0%")) + (t/is (d/percent? "3.5%")) + ;; percent? uses str/rtrim which strips the trailing % then checks numeric, + ;; so a plain numeric string without % also returns true + (t/is (d/percent? "50")) + (t/is (not (d/percent? "abc%"))) + (t/is (not (d/percent? "abc")))) + +(t/deftest parse-percent-test + (t/is (= 0.5 (d/parse-percent "50%"))) + (t/is (= 1.0 (d/parse-percent "100%"))) + (t/is (= 0.0 (d/parse-percent "0%"))) + ;; falls back to parse-double when no % suffix + (t/is (= 0.75 (d/parse-percent "0.75"))) + ;; invalid value returns default + (t/is (nil? (d/parse-percent "abc%"))) + (t/is (= 0.0 (d/parse-percent "abc%" 0.0)))) + +(t/deftest lazy-map-test + (let [calls (atom 0) + m (d/lazy-map [:a :b :c] + (fn [k] + (swap! calls inc) + (name k)))] + ;; The map has the right keys + (t/is (= #{:a :b :c} (set (keys m)))) + ;; Values are delays — force them + (t/is (= "a" @(get m :a))) + (t/is (= "b" @(get m :b))) + (t/is (= "c" @(get m :c))))) + +(t/deftest oreorder-before-test + ;; No ks path: insert k v before before-k in a flat ordered-map + (let [om (d/ordered-map :a 1 :b 2 :c 3) + result (d/oreorder-before om [] :d 4 :b)] + (t/is (= [:a :d :b :c] (vec (keys result))))) + ;; before-k not found → appended at end + (let [om (d/ordered-map :a 1 :b 2) + result (d/oreorder-before om [] :c 3 :z)] + (t/is (= [:a :b :c] (vec (keys result))))) + ;; nil before-k → appended at end + (let [om (d/ordered-map :a 1 :b 2) + result (d/oreorder-before om [] :c 3 nil)] + (t/is (= [:a :b :c] (vec (keys result))))) + ;; existing key k is removed from its old position + (let [om (d/ordered-map :a 1 :b 2 :c 3) + result (d/oreorder-before om [] :c 99 :a)] + (t/is (= [:c :a :b] (vec (keys result)))))) + +(t/deftest oassoc-in-before-test + ;; Simple case: add a new key before an existing key + (let [om (d/ordered-map :a 1 :b 2 :c 3) + result (d/oassoc-in-before om [:b] [:x] 99)] + (t/is (= [:a :x :b :c] (vec (keys result)))) + (t/is (= 99 (get result :x)))) + ;; before-k not found → oassoc-in behaviour (append) + (let [om (d/ordered-map :a 1 :b 2) + result (d/oassoc-in-before om [:z] [:x] 99)] + (t/is (= 99 (get result :x))))) + +(t/deftest reorder-test + ;; Move element from index 0 to position between index 2 and 3 + (t/is (= [:b :c :a :d] (d/reorder [:a :b :c :d] 0 3))) + ;; Move last element to the front + (t/is (= [:d :a :b :c] (d/reorder [:a :b :c :d] 3 0))) + ;; No-op: same logical position (from-pos == to-space-between-pos) + (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] 1 1))) + ;; Clamp out-of-range positions + (t/is (= [:b :c :d :a] (d/reorder [:a :b :c :d] 0 100))) + (t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] -5 0))))