Cover interaction validation edge cases

Exercise the remaining interaction guards and overlay
positioning edge cases, including invalid state
transitions and nested manual offsets. Keep the test
comments focused on why each branch matters for editor
 behavior.
This commit is contained in:
Andrey Antukh
2026-03-24 15:44:00 +00:00
parent 9ba28c088f
commit 58969fc01e

View File

@@ -975,3 +975,106 @@
(t/testing "Remove all interactions returns nil"
(t/is (nil? (ctsi/remove-interactions (constantly true) interactions))))))
(t/deftest validation-guards
(let [frame (cts/setup-shape {:type :frame})
rect (cts/setup-shape {:type :rect})
frame-id (uuid/next)
overlay-frame (cts/setup-shape {:type :frame :width 30 :height 20})
base-frame (cts/setup-shape {:type :frame :width 100 :height 100})
objects {(:id base-frame) base-frame
(:id overlay-frame) overlay-frame}
after-delay (ctsi/set-event-type ctsi/default-interaction :after-delay frame)
overlay (-> ctsi/default-interaction
(ctsi/set-action-type :open-overlay)
(ctsi/set-destination (:id overlay-frame)))
open-url (ctsi/set-action-type ctsi/default-interaction :open-url)
dissolve (ctsi/set-animation-type ctsi/default-interaction :dissolve)
slide (ctsi/set-animation-type ctsi/default-interaction :slide)
push (ctsi/set-animation-type ctsi/default-interaction :push)]
;; These checks protect editor state from invalid combinations, so every public mutator should reject bad input.
(t/testing "Reject invalid event and action updates"
(t/is (ex/exception? (ex/try! (ctsi/set-event-type ctsi/default-interaction :bad-event rect))))
(t/is (ex/exception? (ex/try! (ctsi/set-action-type ctsi/default-interaction :bad-action)))))
(t/testing "Reject invalid delay, destination and preserve-scroll updates"
(t/is (ex/exception? (ex/try! (ctsi/set-delay ctsi/default-interaction 10))))
(t/is (ex/exception? (ex/try! (ctsi/set-delay after-delay :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-destination (ctsi/set-action-type ctsi/default-interaction :prev-screen) frame-id))))
(t/is (ex/exception? (ex/try! (ctsi/set-preserve-scroll (ctsi/set-action-type ctsi/default-interaction :prev-screen) true))))
(t/is (ex/exception? (ex/try! (ctsi/set-preserve-scroll ctsi/default-interaction :bad)))))
(t/testing "Reject invalid url and overlay option updates"
(t/is (ex/exception? (ex/try! (ctsi/set-url ctsi/default-interaction "https://example.com"))))
(t/is (ex/exception? (ex/try! (ctsi/set-url open-url :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-overlay-pos-type ctsi/default-interaction :center base-frame objects))))
(t/is (ex/exception? (ex/try! (ctsi/set-overlay-pos-type overlay :bad base-frame objects))))
(t/is (ex/exception? (ex/try! (ctsi/toggle-overlay-pos-type ctsi/default-interaction :center base-frame objects))))
(t/is (ex/exception? (ex/try! (ctsi/toggle-overlay-pos-type overlay :bad base-frame objects))))
(t/is (ex/exception? (ex/try! (ctsi/set-overlay-position ctsi/default-interaction (gpt/point 1 2)))))
(t/is (ex/exception? (ex/try! (ctsi/set-overlay-position overlay {:x 1 :y 2}))))
(t/is (ex/exception? (ex/try! (ctsi/set-close-click-outside ctsi/default-interaction true))))
(t/is (ex/exception? (ex/try! (ctsi/set-close-click-outside overlay :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-background-overlay ctsi/default-interaction true))))
(t/is (ex/exception? (ex/try! (ctsi/set-background-overlay overlay :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-position-relative-to ctsi/default-interaction frame-id))))
(t/is (ex/exception? (ex/try! (ctsi/set-position-relative-to overlay :bad)))))
(t/testing "Reject invalid animation updates"
(t/is (ex/exception? (ex/try! (ctsi/set-animation-type (ctsi/set-action-type ctsi/default-interaction :open-overlay) :push))))
(t/is (ex/exception? (ex/try! (ctsi/set-animation-type ctsi/default-interaction :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-animation-type (ctsi/set-action-type ctsi/default-interaction :prev-screen) :dissolve))))
(t/is (ex/exception? (ex/try! (ctsi/set-duration ctsi/default-interaction 100))))
(t/is (ex/exception? (ex/try! (ctsi/set-duration dissolve :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-easing ctsi/default-interaction :ease-in))))
(t/is (ex/exception? (ex/try! (ctsi/set-easing dissolve :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-way ctsi/default-interaction :in))))
(t/is (ex/exception? (ex/try! (ctsi/set-way slide :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-direction ctsi/default-interaction :left))))
(t/is (ex/exception? (ex/try! (ctsi/set-direction push :bad))))
(t/is (ex/exception? (ex/try! (ctsi/set-offset-effect ctsi/default-interaction true))))
(t/is (ex/exception? (ex/try! (ctsi/set-offset-effect slide :bad))))
(t/is (ex/exception? (ex/try! (ctsi/invert-direction {:direction :left})))))))
(t/deftest calc-overlay-position-edge-cases
(let [root-frame (cts/setup-shape {:id uuid/zero :type :frame :width 500 :height 500})
base-frame (cts/setup-shape {:type :frame :width 120 :height 120 :frame-id uuid/zero})
popup-frame (cts/setup-shape {:type :frame :width 80 :height 70 :x 20 :y 15 :frame-id (:id base-frame)})
trigger (cts/setup-shape {:type :rect :width 40 :height 30 :x 25 :y 35 :frame-id (:id popup-frame) :parent-id (:id popup-frame)})
overlay-frame (cts/setup-shape {:type :frame :width 30 :height 20})
objects {uuid/zero root-frame
(:id base-frame) base-frame
(:id popup-frame) popup-frame
(:id trigger) trigger
(:id overlay-frame) overlay-frame}
interaction (-> ctsi/default-interaction
(ctsi/set-action-type :open-overlay)
(ctsi/set-destination (:id overlay-frame))
(ctsi/set-position-relative-to (:id popup-frame)))
frame-offset (gpt/point 7 9)]
;; When the destination is missing we should return a harmless fallback instead of trying to measure a nil frame.
(t/testing "Missing destination frame falls back to origin"
(let [[overlay-pos snap] (ctsi/calc-overlay-position interaction trigger objects popup-frame base-frame nil frame-offset)]
(t/is (= (gpt/point 0 0) overlay-pos))
(t/is (= [:top :left] snap))))
;; Manual positions inside nested frames must include the parent frame offset to match the rendered viewport coordinates.
(t/testing "Nested frame manual positions add parent frame offset"
(let [manual-interaction (-> interaction
(ctsi/set-overlay-pos-type :manual trigger objects)
(ctsi/set-overlay-position (gpt/point 12 18)))
[overlay-pos snap] (ctsi/calc-overlay-position manual-interaction trigger objects popup-frame base-frame overlay-frame frame-offset)]
(t/is (= (gpt/point 59 57) overlay-pos))
(t/is (= [:top :left] snap))))
;; If the trigger itself is a frame, manual coordinates are already expressed in the correct local space and should not be adjusted.
(t/testing "Frame relative manual positions keep their local coordinates"
(let [frame-relative (-> interaction
(ctsi/set-position-relative-to (:id base-frame))
(ctsi/set-overlay-pos-type :manual base-frame objects)
(ctsi/set-overlay-position (gpt/point 11 13)))
[overlay-pos snap] (ctsi/calc-overlay-position frame-relative base-frame objects base-frame base-frame overlay-frame frame-offset)]
(t/is (= (gpt/point 18 22) overlay-pos))
(t/is (= [:top :left] snap))))))