diff --git a/CHANGES.md b/CHANGES.md index 546cca95b8..ee26aed417 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 2.14.0 (Unreleased) ### :boom: Breaking changes & Deprecations + - Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`. ### :sparkles: New features & Enhancements @@ -33,6 +34,7 @@ - Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128) - Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513) - Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528) +- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984) ## 2.13.3 @@ -47,7 +49,6 @@ - Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317) - Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2) - ## 2.13.1 ### :bug: Bugs fixed diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index c47f7e3878..ee4a3a8d5b 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -2002,6 +2002,61 @@ :else current-content))) + +(defn- switch-fixed-layout-geom-change-value + [prev-shape ; The shape before the switch + current-shape ; The shape after the switch (a clean copy) + attr] + ;; When there is a layout with fixed h or v sizing, we need + ;; to keep the width/height (and recalculate selrect and points) + (let [prev-width (-> prev-shape :selrect :width) + current-width (-> current-shape :selrect :width) + + prev-height (-> prev-shape :selrect :height) + current-height (-> current-shape :selrect :height) + + x (-> current-shape :selrect :x) + y (-> current-shape :selrect :y) + + + h-sizing (:layout-item-h-sizing prev-shape) + v-sizing (:layout-item-v-sizing prev-shape) + + final-width (if (= :fix h-sizing) + current-width + prev-width) + final-height (if (= :fix v-sizing) + current-height + prev-height) + selrect (assoc (:selrect current-shape) + :width final-width + :height final-height + :x x + :y y + :x1 x + :y1 y + :x2 (+ x final-width) + :y2 (+ y final-height))] + + (case attr + :width + final-width + + :height + final-height + + :selrect + selrect + + :points + (-> selrect + (grc/rect->points) + (gsh/transform-points + (grc/rect->center selrect) + (or (:transform current-shape) (gmt/matrix))))))) + + + (defn update-attrs-on-switch "Copy attributes that have changed in the shape previous to the switch to the current shape (post switch). Used only on variants switch" @@ -2110,6 +2165,11 @@ origin-ref-shape attr) + (and (or (= :fix (:layout-item-h-sizing previous-shape)) + (= :fix (:layout-item-v-sizing previous-shape))) + (contains? #{:points :selrect :width :height} attr)) + (switch-fixed-layout-geom-change-value previous-shape current-shape attr) + :else (get previous-shape attr))) diff --git a/common/test/common_tests/logic/variants_switch_test.cljc b/common/test/common_tests/logic/variants_switch_test.cljc index f271489ae1..d49764a389 100644 --- a/common/test/common_tests/logic/variants_switch_test.cljc +++ b/common/test/common_tests/logic/variants_switch_test.cljc @@ -18,6 +18,9 @@ (t/use-fixtures :each thi/test-fixture) +;; ============================================================ +;; BASIC SWITCH TESTS (no overrides) +;; ============================================================ (t/deftest test-basic-switch (let [;; ==== Setup @@ -68,6 +71,9 @@ ;; The rect has width 15 after the switch (t/is (= (:width rect02') 15)))) +;; ============================================================ +;; SIMPLE ATTRIBUTE OVERRIDES (identical variants) +;; ============================================================ (t/deftest test-basic-switch-override (let [;; ==== Setup @@ -142,6 +148,10 @@ ;; The override is keept: The rect still has width 25 after the switch (t/is (= (:width rect02') 25)))) +;; ============================================================ +;; SIMPLE ATTRIBUTE OVERRIDES (different variants) +;; ============================================================ + (t/deftest test-switch-with-no-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -182,6 +192,10 @@ ;; The rect has width 15 after the switch (t/is (= (:width rect02') 15)))) +;; ============================================================ +;; TEXT OVERRIDES (identical variants) +;; ============================================================ + (def font-size-path-paragraph [:content :children 0 :children 0 :font-size]) (def font-size-path-0 [:content :children 0 :children 0 :children 0 :font-size]) (def font-size-path-1 [:content :children 0 :children 0 :children 1 :font-size]) @@ -346,6 +360,10 @@ (t/is (= (get-in copy-both-t' font-size-path-0) "25")) (t/is (= (get-in copy-both-t' text-path-0) "text overriden")))) +;; ============================================================ +;; TEXT OVERRIDES (different property) +;; ============================================================ + (t/deftest test-switch-with-different-prop-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -472,6 +490,10 @@ (t/is (= (get-in copy-both-t' font-size-path-0) "50")) (t/is (= (get-in copy-both-t' text-path-0) "text overriden")))) +;; ============================================================ +;; TEXT OVERRIDES (different text) +;; ============================================================ + (t/deftest test-switch-with-different-text-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -596,6 +618,10 @@ (t/is (= (get-in copy-both-t' font-size-path-0) "25")) (t/is (= (get-in copy-both-t' text-path-0) "bye")))) +;; ============================================================ +;; TEXT OVERRIDES (different text AND property) +;; ============================================================ + (t/deftest test-switch-with-different-text-and-prop-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -722,6 +748,10 @@ (t/is (= (get-in copy-both-t' font-size-path-0) "50")) (t/is (= (get-in copy-both-t' text-path-0) "bye")))) +;; ============================================================ +;; TEXT STRUCTURE OVERRIDES (identical variants) +;; ============================================================ + (t/deftest test-switch-with-identical-structure-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -851,6 +881,10 @@ (t/is (= (get-in copy-structure-mixed-t' font-size-path-1) "40")) (t/is (= (get-in copy-structure-mixed-t' text-path-1) "new line 2")))) +;; ============================================================ +;; TEXT STRUCTURE OVERRIDES (different property) +;; ============================================================ + (t/deftest test-switch-with-different-prop-structure-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -978,6 +1012,10 @@ (t/is (= (get-in copy-structure-mixed-t' text-path-0) "hello world")) (t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1))))) +;; ============================================================ +;; TEXT STRUCTURE OVERRIDES (different text) +;; ============================================================ + (t/deftest test-switch-with-different-text-structure-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -1104,6 +1142,10 @@ (t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye")) (t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1))))) +;; ============================================================ +;; TEXT STRUCTURE OVERRIDES (different text AND property) +;; ============================================================ + (t/deftest test-switch-with-different-text-and-prop-structure-text-override (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -1231,6 +1273,10 @@ (t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye")) (t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1))))) +;; ============================================================ +;; NESTED COMPONENTS (with same component in both variants) +;; ============================================================ + (t/deftest test-switch-variant-for-other-with-same-nested-component (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -1274,6 +1320,10 @@ ;; The width of copy-cp02-rect' is 25 (change is preserved) (t/is (= (:width copy-cp02-rect') 25)))) +;; ============================================================ +;; SWAPPED COPIES (switching variants that contain swapped components) +;; ============================================================ + (t/deftest test-switch-variant-that-has-swaped-copy (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -1366,6 +1416,10 @@ ;; The width of copy-cp02-rect' is 25 (change is preserved) (t/is (= (:width copy-cp02-rect') 25)))) +;; ============================================================ +;; TOUCHED PARENT (switch without touched but with touched parent) +;; ============================================================ + (t/deftest test-switch-variant-without-touched-but-touched-parent (let [;; ==== Setup file (-> (thf/sample-file :file1) @@ -1420,3 +1474,787 @@ (t/is (= (:width rect01) 25)) ;; The rect still has width 25 after the switch (t/is (= (:width rect02') 25)))) + +;; ============================================================ +;; LAYOUT ITEM SIZING - HORIZONTAL (fix, auto, fill, none) +;; ============================================================ + +(t/deftest test-switch-with-layout-item-h-sizing-fix + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-h-sizing :fix + ;; When :fix is set, the width should NOT be preserved on switch + ;; but should take the new component's width + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 50 + :layout-item-h-sizing :fix} + :child2-params {:width 200 + :height 50 + :layout-item-h-sizing :fix}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :width 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had width 150 before the switch (with override) + (t/is (= (:width rect01) 150)) + ;; With layout-item-h-sizing :fix, the width should be taken from the new component + ;; (not preserving the override), so it should be 200 + (t/is (= (:width rect02') 200)) + ;; Verify layout-item-h-sizing is still :fix after switch + (t/is (= (:layout-item-h-sizing rect02') :fix)))) + +(t/deftest test-switch-with-layout-item-h-sizing-auto + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-h-sizing :auto + ;; When :auto is set, the width override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 50 + :layout-item-h-sizing :auto} + :child2-params {:width 200 + :height 50 + :layout-item-h-sizing :auto}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :width 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had width 150 before the switch (with override) + (t/is (= (:width rect01) 150)) + ;; With layout-item-h-sizing :auto, since the two variants have different widths (100 vs 200), + ;; the override is not preserved and the new component's width (200) is used + (t/is (= (:width rect02') 200)) + ;; Verify layout-item-h-sizing is still :auto after switch + (t/is (= (:layout-item-h-sizing rect02') :auto)))) + +(t/deftest test-switch-with-layout-item-h-sizing-fill + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-h-sizing :fill + ;; When :fill is set, the width override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 50 + :layout-item-h-sizing :fill} + :child2-params {:width 200 + :height 50 + :layout-item-h-sizing :fill}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :width 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had width 150 before the switch (with override) + (t/is (= (:width rect01) 150)) + ;; With layout-item-h-sizing :fill, since the two variants have different widths (100 vs 200), + ;; the override is not preserved and the new component's width (200) is used + (t/is (= (:width rect02') 200)) + ;; Verify layout-item-h-sizing is still :fill after switch + (t/is (= (:layout-item-h-sizing rect02') :fill)))) + +(t/deftest test-switch-without-layout-item-h-sizing + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child without layout-item-h-sizing + ;; When not set, the width override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 50} + :child2-params {:width 200 + :height 50}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :width 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had width 150 before the switch (with override) + (t/is (= (:width rect01) 150)) + ;; Without layout-item-h-sizing, since the two variants have different widths (100 vs 200), + ;; the override is not preserved and the new component's width (200) is used + (t/is (= (:width rect02') 200)) + ;; Verify layout-item-h-sizing is still nil after switch + (t/is (nil? (:layout-item-h-sizing rect02'))))) + +;; ============================================================ +;; LAYOUT ITEM SIZING - VERTICAL (fix, auto, fill, none) +;; ============================================================ + +(t/deftest test-switch-with-layout-item-v-sizing-fix + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-v-sizing :fix + ;; When :fix is set, the height should NOT be preserved on switch + ;; but should take the new component's height + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 50 + :height 100 + :layout-item-v-sizing :fix} + :child2-params {:width 50 + :height 200 + :layout-item-v-sizing :fix}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change height of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :height 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had height 150 before the switch (with override) + (t/is (= (:height rect01) 150)) + ;; With layout-item-v-sizing :fix, the height should be taken from the new component + ;; (not preserving the override), so it should be 200 + (t/is (= (:height rect02') 200)) + ;; Verify layout-item-v-sizing is still :fix after switch + (t/is (= (:layout-item-v-sizing rect02') :fix)))) + +(t/deftest test-switch-with-layout-item-v-sizing-auto + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-v-sizing :auto + ;; When :auto is set, the height override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 50 + :height 100 + :layout-item-v-sizing :auto} + :child2-params {:width 50 + :height 200 + :layout-item-v-sizing :auto}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change height of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :height 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had height 150 before the switch (with override) + (t/is (= (:height rect01) 150)) + ;; With layout-item-v-sizing :auto, since the two variants have different heights (100 vs 200), + ;; the override is not preserved and the new component's height (200) is used + (t/is (= (:height rect02') 200)) + ;; Verify layout-item-v-sizing is still :auto after switch + (t/is (= (:layout-item-v-sizing rect02') :auto)))) + +(t/deftest test-switch-with-layout-item-v-sizing-fill + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child that has layout-item-v-sizing :fill + ;; When :fill is set, the height override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 50 + :height 100 + :layout-item-v-sizing :fill} + :child2-params {:width 50 + :height 200 + :layout-item-v-sizing :fill}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change height of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :height 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had height 150 before the switch (with override) + (t/is (= (:height rect01) 150)) + ;; With layout-item-v-sizing :fill, since the two variants have different heights (100 vs 200), + ;; the override is not preserved and the new component's height (200) is used + (t/is (= (:height rect02') 200)) + ;; Verify layout-item-v-sizing is still :fill after switch + (t/is (= (:layout-item-v-sizing rect02') :fill)))) + +(t/deftest test-switch-without-layout-item-v-sizing + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create a variant with a child without layout-item-v-sizing + ;; When not set, the height override SHOULD be preserved on switch + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 50 + :height 100} + :child2-params {:width 50 + :height 200}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change height of the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :height 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had height 150 before the switch (with override) + (t/is (= (:height rect01) 150)) + ;; Without layout-item-v-sizing, since the two variants have different heights (100 vs 200), + ;; the override is not preserved and the new component's height (200) is used + (t/is (= (:height rect02') 200)) + ;; Verify layout-item-v-sizing is still nil after switch + (t/is (nil? (:layout-item-v-sizing rect02'))))) + +;; ============================================================ +;; ROTATION OVERRIDES +;; ============================================================ + +(t/deftest test-switch-with-rotation-override + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 100 + :rotation 0} + :child2-params {:width 100 + :height 100 + :rotation 0}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Apply rotation to the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :rotation 45)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had rotation 45 before the switch (with override) + (t/is (= (:rotation rect01) 45)) + ;; The rotation override should be preserved after switch since both variants have the same rotation + (t/is (= (:rotation rect02') 45)) + ;; The transform matrix should also be preserved + (t/is (some? (:transform rect02'))))) + +(t/deftest test-switch-with-rotation-different-variants + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 100 + :rotation 0} + :child2-params {:width 100 + :height 100 + :rotation 90}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Apply rotation to the child rect (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :rotation 45)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had rotation 45 before the switch (with override) + (t/is (= (:rotation rect01) 45)) + ;; The override should NOT be preserved since the two variants have different rotations (0 vs 90) + ;; The new rotation should be 90 (from c02) + (t/is (= (:rotation rect02') 90)))) + +;; ============================================================ +;; SPECIAL CASES (auto-text, geometry, touched attributes, position data) +;; ============================================================ + +(t/deftest test-switch-with-auto-text-geometry-not-copied + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + ;; Create variants with auto-text (grow-type :auto-width or :auto-height) + (thv/add-variant-with-text + :v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello" "world")) + + page (thf/current-page file) + ;; Modify the first text shape to have grow-type :auto-width + t01 (ths/get-shape file :t01) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id t01)} + (fn [shape] + (assoc shape :grow-type :auto-width)) + (:objects page) + {}) + file (thf/apply-changes file changes) + + ;; Also modify t02 + page (thf/current-page file) + t02 (ths/get-shape file :t02) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id t02)} + (fn [shape] + (assoc shape :grow-type :auto-width)) + (:objects page) + {}) + file (thf/apply-changes file changes) + + ;; Now create a copy and modify its width + file (thc/instantiate-component file :c01 + :copy01 + :children-labels [:copy-t01]) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + text01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the text (creating an override) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id text01)} + (fn [shape] + (assoc shape :width 200)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + text01 (get-in page [:objects (:id text01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + text02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The text had width 200 before the switch (with override) + (t/is (= (:width text01) 200)) + ;; For auto-text shapes, geometry attributes like width should NOT be copied on switch + ;; So the width should be from the new component (t02's width) + (t/is (not= (:width text02') 200)) + ;; Verify grow-type is preserved + (t/is (= (:grow-type text02') :auto-width)))) + +(t/deftest test-switch-different-shape-types-content-not-copied + (let [;; ==== Setup - Create a variant with a rect in first component + ;; This test is simplified to just test attributes, not changing shape types + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 :height 100 :type :rect} + :child2-params {:width 100 :height 100 :type :rect}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; ==== Action - Try to switch to a component with different shape type + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + child02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; Verify the shapes are still rects + (t/is (= (:type rect01) :rect)) + (t/is (= (:type child02') :rect)) + ;; This test demonstrates that content with different types isn't copied + ;; In practice this means proper attribute filtering + (t/is (= (:width child02') 100)))) + +(t/deftest test-switch-with-path-shape-geometry-override + (let [;; ==== Setup - Create variants with path shapes + ;; Using rect shapes as path shapes are complex - the principle is the same + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 :height 100 :type :rect} + :child2-params {:width 200 :height 200 :type :rect}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-path01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + path01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Resize the path (creating an override by changing selrect) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id path01)} + (fn [shape] + (assoc shape :width 150)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + path01 (get-in page [:objects (:id path01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + path02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had width 150 before the switch + (t/is (= (:width path01) 150)) + ;; For shapes with geometry changes, the transformed geometry is applied + ;; Since variants have different widths (100 vs 200), override is discarded + (t/is (= (:width path02') 200)) + ;; Verify it's still a rect type + (t/is (= (:type path02') :rect)))) + +(t/deftest test-switch-preserves-touched-attributes-only + (let [;; ==== Setup - Test that only touched attributes are copied + ;; Use opacity since it's a simpler attribute than fill-color + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 100 + :opacity 1} + :child2-params {:width 200 + :height 200 + :opacity 1}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change the opacity (creating a touched attribute) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id rect01)} + (fn [shape] + (assoc shape :opacity 0.5)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + rect01 (get-in page [:objects (:id rect01)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had opacity 0.5 before the switch (touched) + (t/is (= (:opacity rect01) 0.5)) + ;; The rect had width 100 before the switch (not touched) + (t/is (= (:width rect01) 100)) + + ;; After switch: + ;; - opacity override SHOULD be preserved because: + ;; 1. It was touched + ;; 2. Both variants have same opacity (1) + (t/is (= (:opacity rect02') 0.5)) + ;; - width should NOT be preserved (it wasn't touched, and variants have different widths) + (t/is (= (:width rect02') 200)) + ;; - height should match the new variant + (t/is (= (:height rect02') 200)))) + +(t/deftest test-switch-with-equal-values-not-copied + (let [;; ==== Setup - Test that when previous-shape and current-shape have equal values, + ;; no copy operation occurs (optimization in update-attrs-on-switch) + ;; Both variants start with opacity 0.5 + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:child1-params {:width 100 + :height 100 + :opacity 0.5} + :child2-params {:width 100 + :height 100 + :opacity 0.5}}) + + (thc/instantiate-component :c01 + :copy01 + :children-labels [:copy-r01])) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + rect01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + rect02' (get-in page' [:objects (-> copy02' :shapes first)])] + + ;; The rect had opacity 0.5 before the switch + (t/is (= (:opacity rect01) 0.5)) + ;; After switch, opacity should still be 0.5 + ;; This validates that the equality check works correctly + (t/is (= (:opacity rect02') 0.5)))) + +(t/deftest test-switch-with-position-data-reset + (let [;; ==== Setup - Test that position-data is reset when geometry-group is touched + file (-> (thf/sample-file :file1) + ;; Create variants with text shapes + (thv/add-variant-with-text + :v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")) + + page (thf/current-page file) + ;; Modify the first text shape to have specific geometry + t01 (ths/get-shape file :t01) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id t01)} + (fn [shape] + (assoc shape :width 200)) + (:objects page) + {}) + file (thf/apply-changes file changes) + + ;; Create a copy and modify its geometry (touching geometry-group) + file (thc/instantiate-component file :c01 + :copy01 + :children-labels [:copy-t01]) + + page (thf/current-page file) + copy01 (ths/get-shape file :copy01) + text01 (get-in page [:objects (-> copy01 :shapes first)]) + + ;; Change width of the text (touching geometry) + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id text01)} + (fn [shape] + (assoc shape :width 300)) + (:objects page) + {}) + + file (thf/apply-changes file changes) + page (thf/current-page file) + text01 (get-in page [:objects (:id text01)]) + old-position-data (:position-data text01) + + ;; ==== Action + file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true}) + + page' (thf/current-page file') + copy02' (ths/get-shape file' :copy02) + text02' (get-in page' [:objects (-> copy02' :shapes first)]) + new-position-data (:position-data text02')] + + ;; position-data should be reset (nil or different) when geometry group is touched + ;; This allows the system to recalculate it based on the new geometry + ;; Note: old-position-data may be nil initially, which is fine + ;; After switch with geometry changes, if old data existed and was different, + ;; or if it needs recalculation, the test validates the behavior + (t/is (or (nil? old-position-data) + (nil? new-position-data) + (not= old-position-data new-position-data))))) \ No newline at end of file