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))))))