From 58969fc01eeb6598ba36d6b3bfadccd252eeb033 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 15:44:00 +0000 Subject: [PATCH] :sparkles: 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. --- .../types/shape_interactions_test.cljc | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/common/test/common_tests/types/shape_interactions_test.cljc b/common/test/common_tests/types/shape_interactions_test.cljc index 48eea94834..d6a072f036 100644 --- a/common/test/common_tests/types/shape_interactions_test.cljc +++ b/common/test/common_tests/types/shape_interactions_test.cljc @@ -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))))))