Files
penpot/common/test/common_tests/data_test.cljc
Andrey Antukh 3ef100427b 🎉 Add tests for app.common.data namespace (#8750)
*  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).

*  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.

*  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.

*  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.

*  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).
2026-03-24 19:52:52 +01:00

908 lines
32 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.data-test
(:require
[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])))
(t/is (= [1] (d/concat-vec #{1})))
(t/is (= [1 2] (d/concat-vec [1] #{2})))
(t/is (= [1 2] (d/concat-vec '(1) [2]))))
(t/deftest concat-set
(t/is (= #{} (d/concat-set)))
(t/is (= #{1 2}
(d/concat-set [1] [2]))))
(t/deftest remove-at-index
(t/is (= [1 2 3 4]
(d/remove-at-index [1 2 3 4 5] 4)))
(t/is (= [1 2 3 4]
(d/remove-at-index [5 1 2 3 4] 0)))
(t/is (= [1 2 3 4]
(d/remove-at-index [1 5 2 3 4] 1))))
(t/deftest with-next
(t/is (= [[0 1] [1 2] [2 3] [3 4] [4 nil]]
(d/with-next (range 5)))))
(t/deftest with-prev
(t/is (= [[0 nil] [1 0] [2 1] [3 2] [4 3]]
(d/with-prev (range 5)))))
(t/deftest with-prev-next
(t/is (= [[0 nil 1] [1 0 2] [2 1 3] [3 2 4] [4 3 nil]]
(d/with-prev-next (range 5)))))
(t/deftest join
(t/is (= [[1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b]]
(d/join [1 2 3] [:a :b])))
(t/is (= [1 10 100 2 20 200 3 30 300]
(d/join [1 2 3] [1 10 100] *))))
(t/deftest num-predicate
(t/is (not (d/num? ##NaN)))
(t/is (not (d/num? nil)))
(t/is (d/num? 1))
(t/is (d/num? -0.3))
(t/is (not (d/num? {}))))
(t/deftest check-num-helper
(t/is (= 1 (d/check-num 1 0)))
(t/is (= 0 (d/check-num ##NaN 0)))
(t/is (= 0 (d/check-num {} 0)))
(t/is (= 0 (d/check-num [] 0)))
(t/is (= 0 (d/check-num :foo 0)))
(t/is (= 0 (d/check-num nil 0))))
(t/deftest insert-at-index
;; insert different object
(t/is (= (d/insert-at-index [:a :b] 1 [:c :d])
[:a :c :d :b]))
;; insert on the start
(t/is (= (d/insert-at-index [:a :b] 0 [:c])
[:c :a :b]))
;; insert on the end 1
(t/is (= (d/insert-at-index [:a :b] 2 [:c])
[:a :b :c]))
;; insert on the end with not existing index
(t/is (= (d/insert-at-index [:a :b] 10 [:c])
[:a :b :c]))
;; insert existing in a contiguous index
(t/is (= (d/insert-at-index [:a :b] 1 [:a])
[:a :b]))
;; insert existing in the same index
(t/is (= (d/insert-at-index [:a :b] 0 [:a])
[:a :b]))
;; insert existing in other index case 1
(t/is (= (d/insert-at-index [:a :b :c] 2 [:a])
[:b :a :c]))
;; insert existing in other index case 2
(t/is (= (d/insert-at-index [:a :b :c :d] 0 [:d])
[:d :a :b :c]))
;; insert existing in other index case 3
(t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a])
[:a :b :c :d])))
(t/deftest reorder
(let [v ["a" "b" "c" "d"]]
(t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"]))
(t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"]))
(t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"]))
(t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"]))
(t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"]))
(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))))