Add tests for app.common.geom namespaces

This commit is contained in:
Andrey Antukh 2026-04-15 08:56:08 +02:00
parent fa89790fd6
commit 146219a439
7 changed files with 331 additions and 0 deletions

View File

@ -0,0 +1,106 @@
;; 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.geom-flex-layout-test
(:require
[app.common.geom.rect :as grc]
[app.common.geom.shapes.flex-layout.positions :as flp]
[app.common.math :as mth]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[clojure.test :as t]))
;; ---- helpers ----
(defn- make-col-frame
"Minimal col? flex frame with wrap enabled.
wrap is required for the content-around? predicate to activate."
[& {:as opts}]
(cts/setup-shape (merge {:type :frame
:layout :flex
:layout-flex-dir :column
:layout-wrap-type :wrap
:x 0 :y 0 :width 200 :height 200}
opts)))
(defn- rect->bounds
"Convert a rect to the 4-point layout-bounds vector expected by gpo/*."
[rect]
(grc/rect->points rect))
;; ---- get-base-line (around? branch) ----
;;
;; Bug: in positions.cljc the col? + around? branch had a mis-parenthesised
;; expression `(/ free-width num-lines) 2`, which was parsed as three
;; arguments to `max`:
;; (max lines-gap-col (/ free-width num-lines) 2)
;; instead of the intended two-argument max with a nested division:
;; (max lines-gap-col (/ free-width num-lines 2))
;;
;; For a col? layout the cross-axis is horizontal (hv), so the around? offset
;; is applied as hv(delta) — i.e. the delta ends up in (:x base-p).
(t/deftest get-base-line-around-uses-half-per-line-free-width
(t/testing "col? + content-around? offset is free-width / num-lines / 2"
;; Layout: col? wrap, width=200, 3 lines each 20px wide → free-width=140
;; lines-gap-col = 0 (no gap defined)
;; Expected horizontal offset = max(0, 140/3/2) ≈ 23.33
;; Before the bug fix the formula was (max ... (/ 140 3) 2) ≈ 46.67.
(let [frame (make-col-frame :layout-align-content :space-around)
bounds (rect->bounds (grc/make-rect 0 0 200 200))
;; 3 lines of 20px each (widths); no row gap
num-lines 3
total-width 60
total-height 0
base-p (flp/get-base-line frame bounds total-width total-height num-lines)
free-width (- 200 total-width)
;; lines-gap-col = (dec 3) * 0 = 0; max(0, free-width/num-lines/2)
expected-x (/ free-width num-lines 2)]
;; The base point x-coordinate (hv offset) should equal half per-line free space.
(t/is (mth/close? expected-x (:x base-p) 0.01))))
(t/testing "col? + content-around? offset respects lines-gap-col minimum"
;; When the accumulated column gap exceeds the computed half-per-line value
;; max(lines-gap-col, free-width/num-lines/2) returns the gap.
(let [frame (make-col-frame :layout-align-content :space-around
:layout-gap {:column-gap 50 :row-gap 0})
bounds (rect->bounds (grc/make-rect 0 0 200 200))
;; 4 lines × 20px = 80px used; free-width=120; half-per-line = 120/4/2 = 15
;; lines-gap-col = (dec 4)*50 = 150 → max(150, 15) = 150
num-lines 4
total-width 80
total-height 0
base-p (flp/get-base-line frame bounds total-width total-height num-lines)
lines-gap-col (* (dec num-lines) 50)]
(t/is (mth/close? lines-gap-col (:x base-p) 0.01)))))
;; ---- v-end? guard (drop-line-area) ----
;;
;; Bug: `v-end?` inside `drop-line-area` was guarded by `row?` instead of
;; `col?`, so vertical-end alignment in a column layout was never triggered.
;; We verify the predicate behaviour directly via ctl/v-end?.
(t/deftest v-end-guard-uses-col-not-row
(t/testing "v-end? is true for col? frame with justify-content :end"
;; col? + justify-content=:end → ctl/v-end? must be true
(let [frame (cts/setup-shape {:type :frame
:layout :flex
:layout-flex-dir :column
:layout-justify-content :end
:x 0 :y 0 :width 100 :height 100})]
(t/is (true? (ctl/v-end? frame)))))
(t/testing "v-end? is false for row? frame with only justify-content :end"
;; row? + justify-content=:end alone does NOT set v-end?; for row layouts
;; v-end? checks align-items, not justify-content.
(let [frame (cts/setup-shape {:type :frame
:layout :flex
:layout-flex-dir :row
:layout-justify-content :end
:x 0 :y 0 :width 100 :height 100})]
(t/is (not (ctl/v-end? frame))))))

View File

@ -289,3 +289,33 @@
(t/is (mth/close? 1.2091818119288809 (:x rs)))
(t/is (mth/close? 1.8275638211757912 (:y rs)))))
;; ---- gpt/abs ----
(t/deftest abs-point-returns-point-instance
(t/testing "abs of a point with negative coordinates returns a Point record"
(let [p (gpt/point -3 -4)
rs (gpt/abs p)]
(t/is (gpt/point? rs))
(t/is (mth/close? 3 (:x rs)))
(t/is (mth/close? 4 (:y rs)))))
(t/testing "abs of a point with mixed-sign coordinates"
(let [p (gpt/point -5 7)
rs (gpt/abs p)]
(t/is (gpt/point? rs))
(t/is (mth/close? 5 (:x rs)))
(t/is (mth/close? 7 (:y rs)))))
(t/testing "abs of a point already positive is unchanged"
(let [p (gpt/point 2 9)
rs (gpt/abs p)]
(t/is (gpt/point? rs))
(t/is (mth/close? 2 (:x rs)))
(t/is (mth/close? 9 (:y rs)))))
(t/testing "abs of a zero point stays zero"
(let [rs (gpt/abs (gpt/point 0 0))]
(t/is (gpt/point? rs))
(t/is (mth/close? 0 (:x rs)))
(t/is (mth/close? 0 (:y rs))))))

View File

@ -0,0 +1,94 @@
;; 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.geom-rect-test
(:require
[app.common.geom.rect :as grc]
[app.common.math :as mth]
[clojure.test :as t]))
;; ---- update-rect :size ----
(t/deftest update-rect-size-sets-all-corners
(t/testing ":size updates x1/y1 as well as x2/y2 from x/y/width/height"
(let [r (grc/make-rect 10 20 30 40)
r' (grc/update-rect r :size)]
;; x1/y1 must mirror x/y
(t/is (mth/close? (:x r) (:x1 r')))
(t/is (mth/close? (:y r) (:y1 r')))
;; x2/y2 must be x+width / y+height
(t/is (mth/close? (+ (:x r) (:width r)) (:x2 r')))
(t/is (mth/close? (+ (:y r) (:height r)) (:y2 r')))))
(t/testing ":size is consistent with :corners round-trip"
;; Applying :size then :corners should recover the original x/y/w/h
(let [r (grc/make-rect 5 15 100 50)
r' (-> r (grc/update-rect :size) (grc/update-rect :corners))]
(t/is (mth/close? (:x r) (:x r')))
(t/is (mth/close? (:y r) (:y r')))
(t/is (mth/close? (:width r) (:width r')))
(t/is (mth/close? (:height r) (:height r')))))
(t/testing ":size works for a rect at the origin"
(let [r (grc/make-rect 0 0 200 100)
r' (grc/update-rect r :size)]
(t/is (mth/close? 0 (:x1 r')))
(t/is (mth/close? 0 (:y1 r')))
(t/is (mth/close? 200 (:x2 r')))
(t/is (mth/close? 100 (:y2 r'))))))
;; ---- corners->rect ----
(t/deftest corners->rect-normal-order
(t/testing "p1 top-left, p2 bottom-right yields a valid rect"
(let [r (grc/corners->rect 0 0 10 20)]
(t/is (grc/rect? r))
(t/is (mth/close? 0 (:x r)))
(t/is (mth/close? 0 (:y r)))
(t/is (mth/close? 10 (:width r)))
(t/is (mth/close? 20 (:height r))))))
(t/deftest corners->rect-reversed-corners
(t/testing "reversed x-coordinates still produce a positive-width rect"
(let [r (grc/corners->rect 10 0 0 20)]
(t/is (grc/rect? r))
(t/is (mth/close? 0 (:x r)))
(t/is (mth/close? 10 (:width r)))))
(t/testing "reversed y-coordinates still produce a positive-height rect"
(let [r (grc/corners->rect 0 20 10 0)]
(t/is (grc/rect? r))
(t/is (mth/close? 0 (:y r)))
(t/is (mth/close? 20 (:height r)))))
(t/testing "both axes reversed yield the same rect as normal order"
(let [r-normal (grc/corners->rect 0 0 10 20)
r-reversed (grc/corners->rect 10 20 0 0)]
(t/is (mth/close? (:x r-normal) (:x r-reversed)))
(t/is (mth/close? (:y r-normal) (:y r-reversed)))
(t/is (mth/close? (:width r-normal) (:width r-reversed)))
(t/is (mth/close? (:height r-normal) (:height r-reversed))))))
(t/deftest corners->rect-from-points
(t/testing "two-arity overload taking point maps works identically"
(let [p1 {:x 5 :y 10}
p2 {:x 15 :y 30}
r (grc/corners->rect p1 p2)]
(t/is (grc/rect? r))
(t/is (mth/close? 5 (:x r)))
(t/is (mth/close? 10 (:y r)))
(t/is (mth/close? 10 (:width r)))
(t/is (mth/close? 20 (:height r)))))
(t/testing "two-arity overload with reversed points"
(let [p1 {:x 15 :y 30}
p2 {:x 5 :y 10}
r (grc/corners->rect p1 p2)]
(t/is (grc/rect? r))
(t/is (mth/close? 5 (:x r)))
(t/is (mth/close? 10 (:y r)))
(t/is (mth/close? 10 (:width r)))
(t/is (mth/close? 20 (:height r))))))

View File

@ -0,0 +1,27 @@
;; 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.geom-shapes-constraints-test
(:require
[app.common.geom.shapes.constraints :as gsc]
[clojure.test :as t]))
;; ---- constraint-modifier :default ----
(t/deftest constraint-modifier-default-returns-empty-vector
(t/testing ":default method accepts 6 args and returns an empty vector"
;; Before the fix the :default method only accepted 5 positional args
;; (plus the dispatch value), so calling it with 6 args would throw an
;; arity error. After the fix it takes [_ _ _ _ _ _] and returns [].
(let [result (gsc/constraint-modifier :unknown-constraint-type
:x nil nil nil nil)]
(t/is (vector? result))
(t/is (empty? result))))
(t/testing ":default method returns [] for :scale-like unknown type on :y axis"
(let [result (gsc/constraint-modifier :some-other-unknown
:y nil nil nil nil)]
(t/is (= [] result)))))

View File

@ -230,3 +230,44 @@
(t/is (true? (gsin/slow-has-point? shape point1)))
(t/is (false? (gsin/fast-has-point? shape point2)))
(t/is (false? (gsin/fast-has-point? shape point2)))))
;; ---- adjust-shape-flips (via apply-transform / transform-shape) ----
(t/deftest flip-x-only-toggles-flip-x-and-negates-rotation
(t/testing "Flipping only X axis toggles flip-x and negates rotation"
;; Build a rect with a known rotation, then apply a scale(-1, 1)
;; from the left edge to simulate an X-axis flip.
(let [shape (create-test-shape :rect {:rotation 30})
;; Flip horizontally about x=0 (left edge of shape)
origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y]))
mods (ctm/resize-modifiers (gpt/point -1 1) origin)
result (gsh/transform-shape shape mods)]
;; flip-x should have been toggled (from nil/false to true)
(t/is (true? (:flip-x result)))
;; flip-y should NOT be set
(t/is (not (true? (:flip-y result))))
;; rotation is negated then normalised into [0,360): -30 mod 360 = 330
(t/is (mth/close? 330 (:rotation result))))))
(t/deftest flip-y-only-toggles-flip-y-and-negates-rotation
(t/testing "Flipping only Y axis toggles flip-y and negates rotation"
(let [shape (create-test-shape :rect {:rotation 45})
origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y]))
mods (ctm/resize-modifiers (gpt/point 1 -1) origin)
result (gsh/transform-shape shape mods)]
(t/is (not (true? (:flip-x result))))
(t/is (true? (:flip-y result)))
;; -45 mod 360 = 315
(t/is (mth/close? 315 (:rotation result))))))
(t/deftest flip-both-axes-toggles-both-flags-but-preserves-rotation
(t/testing "Flipping both axes toggles flip-x and flip-y, but does NOT negate rotation"
;; Two simultaneous axis flips = 180° rotation, so stored rotation is unchanged.
(let [shape (create-test-shape :rect {:rotation 30})
origin (gpt/point (get-in shape [:selrect :x]) (get-in shape [:selrect :y]))
mods (ctm/resize-modifiers (gpt/point -1 -1) origin)
result (gsh/transform-shape shape mods)]
(t/is (true? (:flip-x result)))
(t/is (true? (:flip-y result)))
;; rotation must not be negated when both axes are flipped
(t/is (mth/close? 30 (:rotation result))))))

View File

@ -9,6 +9,7 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
[app.common.schema :as sm]
[clojure.test :as t]))
(t/deftest point-constructors-test
@ -100,3 +101,28 @@
(let [m (-> (gmt/matrix)
(gmt/rotate 10))]
(t/is (= m (gmt/matrix 0.984807753012208 0.17364817766693033 -0.17364817766693033 0.984807753012208 0 0)))))
;; ---- matrix->str (no trailing comma) ----
(t/deftest matrix-str-roundtrip-test
(t/testing "Identity matrix encodes and decodes back to equal matrix"
(let [m (gmt/matrix)
enc (sm/encode gmt/schema:matrix m (sm/string-transformer))
dec (sm/decode gmt/schema:matrix enc (sm/string-transformer))]
(t/is (string? enc))
;; Must not end with a comma
(t/is (not= \, (last enc)))
(t/is (gmt/close? m dec))))
(t/testing "Arbitrary matrix encodes without trailing comma and round-trips"
(let [m (gmt/matrix 2 0.5 -0.5 3 10 20)
enc (sm/encode gmt/schema:matrix m (sm/string-transformer))
dec (sm/decode gmt/schema:matrix enc (sm/string-transformer))]
(t/is (string? enc))
(t/is (not= \, (last enc)))
(t/is (gmt/close? m dec))))
(t/testing "Encoded string contains exactly 5 commas (6 fields)"
(let [m (gmt/matrix 1 0 0 1 0 0)
enc (sm/encode gmt/schema:matrix m (sm/string-transformer))]
(t/is (= 5 (count (filter #(= \, %) enc)))))))

View File

@ -15,6 +15,7 @@
[common-tests.files-migrations-test]
[common-tests.geom-align-test]
[common-tests.geom-bounds-map-test]
[common-tests.geom-flex-layout-test]
[common-tests.geom-grid-layout-test]
[common-tests.geom-grid-test]
[common-tests.geom-line-test]
@ -22,7 +23,9 @@
[common-tests.geom-modifiers-test]
[common-tests.geom-point-test]
[common-tests.geom-proportions-test]
[common-tests.geom-rect-test]
[common-tests.geom-shapes-common-test]
[common-tests.geom-shapes-constraints-test]
[common-tests.geom-shapes-corners-test]
[common-tests.geom-shapes-effects-test]
[common-tests.geom-shapes-intersect-test]
@ -88,13 +91,17 @@
'common-tests.files-migrations-test
'common-tests.geom-align-test
'common-tests.geom-bounds-map-test
'common-tests.geom-flex-layout-test
'common-tests.geom-grid-layout-test
'common-tests.geom-grid-test
'common-tests.geom-line-test
'common-tests.geom-modif-tree-test
'common-tests.geom-modifiers-test
'common-tests.geom-point-test
'common-tests.geom-proportions-test
'common-tests.geom-rect-test
'common-tests.geom-shapes-common-test
'common-tests.geom-shapes-constraints-test
'common-tests.geom-shapes-corners-test
'common-tests.geom-shapes-effects-test
'common-tests.geom-shapes-intersect-test