penpot/common/test/common_tests/logic/variants_switch_test.cljc
Alonso Torres cb8f563bb2
🐛 Keep swapped and moved children in place when a component syncs (#12716)
* 🐛 Keep swapped and moved children in place when a component syncs

A sync resets an untouched shape of a copy to the main's geometry and the
layout then moves it back. Children the sync keeps, because they are
swapped or the user moved them, did not take the first move but did take
the second, so each sync pushed them away from their parent by the gap
between the copy and the main (e.g. a swapped icon in a copy that fills a
wider layout).

The sync now moves those children by the offset it applies to their
parent. Flex-child attributes still sync as before, so swapped children
keep their absolute position.

Closes #12657

* 🐛 Place swapped children where the swap puts them on sync and switch

A swapped child that the user never moved now goes to the top-left of the
shape it replaced, as a swap does: on a sync, the place of the main's slot
occupant; on a variant switch, the place of the new variant's occupant.
A swapped child the user moved keeps its offset inside its parent.

This stops the variant switch from leaving the swapped child at its old
position while the new variant is created at the main's size, and puts
back children that drifted before the sync fix the next time their copy
syncs.

* 🐛 Make kept children follow their parent's constraints on component sync

When a sync resets the size of a copy's shape, for example a copy root
that fills a wider layout, the layout then grows it back and moves its
children by their constraints. Children the sync keeps (moved by the
user, or swapped) only followed the parent's top-left, so with a right,
center or scale constraint they drifted on every sync, and a swapped
child with a scale constraint grew on every sync.

The sync now passes the parent's geometry before and after down the
tree, and kept children follow that change through their constraints.
Untouched swapped children also undo the change through their
constraints before they go to the swap place, so they keep their size.

The default constraint modifier returns empty modifiers instead of an
empty vector, which add-modifiers could not take.

* 🐛 Make swapped children follow their parent's constraints on switch

A variant switch only moved a swapped child by the parent's position
change. When the new variant's root had a different size, a user-moved
child with a right constraint drifted, and a scale-constrained one kept
its grown size.

The switch now applies the parent change from the old parent to the new
one through the child's constraints. An untouched swapped child then
takes the place of the shape it replaces, as a swap does.

* 🐛 Sync nested swapped copies before the copies that hold them

A swapped child is a copy of its own component, so one sync handles it
twice: once on its own, and once when the copy that holds it places it.
Each step read the shape from the data as it was before the sync, so
the step that ran last won, and the order came from the order of shape
ids. When the swapped child came last, it wrote its old size and
position back over the placement, leaving its x and selrect apart, and
a swapped child with a scale constraint grew on every sync.

The sync now visits the most deeply nested shapes first, and each step
reads the shapes as the earlier steps left them, so the placement
always comes last and builds on the child's own sync.

* 🐛 Keep moved children with a scale constraint at their size on switch

A variant switch carries the size of a child the user moved as an
override. In a copy that fills a wider layout, a child with a scale
constraint already has the size the layout gave it, so the switch
carried that size onto the new variant, created at the main's size, and
the layout then scaled it again: the child grew on every switch.

Before comparing, the switch now resizes the previous child the way its
constraints follow the change from its old parent to its new one. A
child that only grew with the layout then matches the main and is not
carried as an override.

* 🐛 Keep the content of swapped children at its size on sync and switch

When a sync or a variant switch undid the parent change on a swapped
child through its constraints, it applied the child's resize to every
shape inside it. The layout then grows the child back through its own
constraint, but its content follows its own constraints (left and top
by default), so it stayed shrunk: the shape inside a swapped icon with
a scale constraint lost most of its width on every switch.

The change of the swapped child now goes down its subtree the way a
layout update does, each shape through its own constraints.

* 🐛 Accept mixed and empty values in the layout size constraints menu

With several shapes or a group selected, the sidebar merges the layout
item attributes and tokens of the shapes: nil when a shape lacks one,
:multiple when they differ. The props schema of layout-size-constraints*
only accepted the values of a single shape, so selecting such shapes
with fill sizing raised "invalid props".

The schema now accepts nil and :multiple, as the numeric inputs those
values go to do.

* ♻️ Share the swapped child follow-parent step between sync and switch

The component sync and the variant switch both make a swapped child and
its children follow the parent change through their constraints and then
move them to the swap place. Both now use generate-follow-parent, so the
two paths stay the same. The helper that writes geometry without marking
it touched is renamed to update-geometry-untouched.

Adds a test for a swapped child without absolute position in a slot
without layout, and fixes the test comments on why constraints are set
again after a swap.
2026-10-29 17:30:16 +01:00

3328 lines
156 KiB
Clojure

;; 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 SUBSIDIARY SL
(ns common-tests.logic.variants-switch-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.math :as mth]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.variants :as thv]
[app.common.types.component :as ctk]
[app.common.types.modifiers :as ctm]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
;; ============================================================
;; BASIC SWITCH TESTS (no overrides)
;; ============================================================
(t/deftest test-basic-switch
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(thc/instantiate-component :c01
:copy01))
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
copy01' (ths/get-shape file' :copy01)]
(thf/dump-file file :keys [:width])
;; The copy had width 5 before the switch
(t/is (= (:width copy01) 5))
;; The copy has width 15 after the switch
(t/is (= (:width copy01') 15))))
(t/deftest test-simple-switch
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 15}})
(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-in-shape file :copy01 :c02 {:keep-touched? true})
page' (thf/current-page file')
copy01' (ths/get-shape file' :copy01)
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
;; The rect had width 5 before the switch
(t/is (= (:width rect01) 5))
;; The rect has width 15 after the switch
(t/is (= (:width rect01') 15))))
;; ============================================================
;; SIMPLE ATTRIBUTE OVERRIDES (identical variants)
;; ============================================================
(t/deftest test-basic-switch-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 5}})
(thc/instantiate-component :c01
:copy01))
copy01 (ths/get-shape file :copy01)
;; Change width of copy
page (thf/current-page file)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy01)}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
copy01' (ths/get-shape file' :copy01)]
(thf/dump-file file :keys [:width])
;; The copy had width 25 before the switch
(t/is (= (:width copy01) 25))
;; The override is keept: The copy still has width 25 after the switch
(t/is (= (:width copy01') 25))))
(t/deftest test-switch-with-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 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)])
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(assoc shape :width 25))
(: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-in-shape file :copy01 :c02 {:keep-touched? true})
page' (thf/current-page file')
copy01' (ths/get-shape file' :copy01)
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
;; The rect had width 25 before the switch
(t/is (= (:width rect01) 25))
;; The override is keept: The rect still has width 25 after the switch
(t/is (= (:width rect01') 25))))
;; ============================================================
;; SIMPLE ATTRIBUTE OVERRIDES (different variants)
;; ============================================================
(t/deftest test-switch-with-no-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 15}})
(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)])
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(assoc shape :width 25))
(: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-in-shape file :copy01 :c02 {:keep-touched? true})
page' (thf/current-page file')
copy01' (ths/get-shape file' :copy01)
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
;; The rect had width 25 before the switch
(t/is (= (:width rect01) 25))
;; The override isn't keept, because the property is different in the mains
;; The rect has width 15 after the switch
(t/is (= (:width rect01') 15))))
;; ============================================================
;; NESTED COPY SWITCH (no overrides)
;; ============================================================
(t/deftest test-nested-switch-in-main
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(tho/add-frame :m03)
(thc/instantiate-component :c01
:copy01
:parent-label :m03)
(thc/make-component :c03 :m03))
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
copy01' (ths/get-shape file' :copy01)]
(thf/dump-file file :keys [:width])
;; The copy had width 5 before the switch
(t/is (= (:width copy01) 5))
;; The copy has width 15 after the switch
(t/is (= (:width copy01') 15))
;; The copy is not touched but has swap slot
(t/is (= (count (:touched copy01')) 1))
(t/is (= (ctk/get-swap-slot copy01') (thi/id :copy01)))))
(t/deftest test-nested-switch-in-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(tho/add-frame :m03)
(thc/instantiate-component :c01
:nested01
:parent-label :m03)
(thc/make-component :c03 :m03)
(thc/instantiate-component :c03
:nested02
:children-labels [:child01]))
child01 (ths/get-shape file :child01)
;; ==== Action
file' (tho/swap-component-in-shape file :child01 :c02 {:keep-touched? true})
child01' (ths/get-shape file' :child01)]
(thf/dump-file file :keys [:width])
;; The copy had width 5 before the switch
(t/is (= (:width child01) 5))
;; The copy has width 15 after the switch
(t/is (= (:width child01') 15))
;; The copy is not touched but has swap slot
(t/is (= (count (:touched child01')) 1))
(t/is (= (ctk/get-swap-slot child01') (thi/id :nested01)))))
;; ============================================================
;; NESTED COPY SWITCH (with overrides)
;; ============================================================
(t/deftest test-nested-switch-in-main-with-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(tho/add-frame :m03)
(thc/instantiate-component :c01
:copy01
:parent-label :m03)
(thc/make-component :c03 :m03))
page (thf/current-page file)
fills (ths/sample-fills-color :fill-color "#fabada")
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(thi/id :copy01)}
(fn [shape]
(assoc shape
:width 25
:fills fills))
(:objects page)
{})
file (thf/apply-changes file changes)
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
copy01' (ths/get-shape file' :copy01)]
(thf/dump-file file :keys [:width :touched])
;; The copy had fill color before the switch
(t/is (= (:fills copy01) fills))
;; The copy still has fill color after the switch
(t/is (= (:fills copy01') fills))
;; The copy had width 25 before the switch
(t/is (= (:width copy01) 25))
;; The copy gets the switched variant width 15, because this is the value changed in the variant
(t/is (= (:width copy01') 15))
;; The copy is fills touched and has swap slot
(t/is (= (count (:touched copy01')) 2))
(t/is (= (ctk/get-swap-slot copy01') (thi/id :copy01)))
(t/is (contains? (:touched copy01') :fill-group))))
(t/deftest test-nested-switch-in-copy-with-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(tho/add-frame :m03)
(thc/instantiate-component :c01
:nested01
:parent-label :m03)
(thc/make-component :c03 :m03)
(thc/instantiate-component :c03
:copy02
:children-labels [:nested02]))
page (thf/current-page file)
fills (ths/sample-fills-color :fill-color "#fabada")
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(thi/id :nested02)}
(fn [shape]
(assoc shape
:width 25
:fills fills))
(:objects page)
{})
file (thf/apply-changes file changes)
nested02 (ths/get-shape file :nested02)
;; ==== Action
file' (tho/swap-component-in-shape file :nested02 :c02 {:keep-touched? true})
nested02' (ths/get-shape file' :nested02)]
(thf/dump-file file :keys [:width])
;; The copy had fill color before the switch
(t/is (= (:fills nested02) fills))
;; The copy still has fill color after the switch
(t/is (= (:fills nested02') fills))
;; The copy had width 5 before the switch
(t/is (not= (:width nested02) 5))
;; The copy gets the switched variant width 15, because this is the value changed in the variant
(t/is (= (:width nested02') 15))
;; The copy is fills touched and has swap slot
(t/is (= (count (:touched nested02')) 2))
(t/is (= (ctk/get-swap-slot nested02') (thi/id :nested01)))
(t/is (contains? (:touched nested02') :fill-group))))
;; ============================================================
;; 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])
(def text-path-0 [:content :children 0 :children 0 :children 0 :text])
(def text-path-1 [:content :children 0 :children 0 :children 1 :text])
(def text-lines-path [:content :children 0 :children 0 :children])
(defn- update-attr
[file label path value]
(let [page (thf/current-page file)
shape (ths/get-shape file label)
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{(:id shape)}
(fn [shape]
(cond-> (assoc-in shape path value)
(or (= path font-size-path-0) (= path font-size-path-1))
(assoc-in font-size-path-paragraph value)))
(:objects page)
{})]
(thf/apply-changes file changes)))
(defn- change-structure
[file label]
(let [page (thf/current-page file)
shape (ths/get-shape file label)
line1 (-> (get-in shape text-lines-path)
first
(assoc :text "new line 1"))
line2 (assoc line1 :text "new line 2")
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{(:id shape)}
(fn [shape]
(assoc-in shape text-lines-path [line1 line2]))
(:objects page)
{})]
(thf/apply-changes file changes)))
(t/deftest test-switch-with-identical-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
;; Both components are identical: have the same text and props
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(thc/instantiate-component :c01
:copy-clean
:children-labels [:copy-clean-t])
(thc/instantiate-component :c01
:copy-font-size
:children-labels [:copy-font-size-t])
(thc/instantiate-component :c01
:copy-text
:children-labels [:copy-text-t])
(thc/instantiate-component :c01
:copy-both
:children-labels [:copy-both-t]))
;; The copy clean has no overrides
copy-clean-t (ths/get-shape file :copy-clean-t)
;; Override font size on copy-font-size
file (update-attr file :copy-font-size-t font-size-path-0 "25")
copy-font-size-t (ths/get-shape file :copy-font-size-t)
;; Override text on copy-text
file (update-attr file :copy-text-t text-path-0 "text overriden")
copy-text-t (ths/get-shape file :copy-text-t)
;; Override both on copy-both
file (update-attr file :copy-both-t font-size-path-0 "25")
file (update-attr file :copy-both-t text-path-0 "text overriden")
copy-both-t (ths/get-shape file :copy-both-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
page' (thf/current-page file')
copy-clean' (ths/get-shape file' :copy-clean-2)
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
copy-font-size' (ths/get-shape file' :copy-font-size-2)
copy-font-size-t' (get-in page' [:objects (-> copy-font-size' :shapes first)])
copy-text' (ths/get-shape file' :copy-text-2)
copy-text-t' (get-in page' [:objects (-> copy-text' :shapes first)])
copy-both' (ths/get-shape file' :copy-both-2)
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Clean copy
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
;; After the switch:
;; * font size 25 (value of c02, because there was no override)
;; * text "hello world" (value of c02, because there was no override)
(t/is (= (get-in copy-clean-t' font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t' text-path-0) "hello world"))
;;;;;;;;;;; Font size copy
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
;; After the switch:
;; * font size 25 (the override is preserved)
;; * text "hello world" (value of c02, because there was no override)
(t/is (= (get-in copy-font-size-t' font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t' text-path-0) "hello world"))
;;;;;;;;;;; Text copy
;; Before the switch:
;; * font size 14
;; * text "text overriden"
(t/is (= (get-in copy-text-t font-size-path-0) "14"))
(t/is (= (get-in copy-text-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 14 (value of c02, because there was no override)
;; * text "text overriden" (the override is preserved)
(t/is (= (get-in copy-text-t' font-size-path-0) "14"))
(t/is (= (get-in copy-text-t' text-path-0) "text overriden"))
;;;;;;;;;;; Both copy
;; Before the switch:
;; * font size 25
;; * text "text overriden"
(t/is (= (get-in copy-both-t font-size-path-0) "25"))
(t/is (= (get-in copy-both-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 25 (the override is preserved)
;; * text "text overriden" (the override is preserved)
(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)
;; The second component has a different prop
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(update-attr :t02 font-size-path-0 "50")
(thc/instantiate-component :c01
:copy-clean
:children-labels [:copy-clean-t])
(thc/instantiate-component :c01
:copy-font-size
:children-labels [:copy-font-size-t])
(thc/instantiate-component :c01
:copy-text
:children-labels [:copy-text-t])
(thc/instantiate-component :c01
:copy-both
:children-labels [:copy-both-t]))
;; The copy clean has no overrides
copy-clean-t (ths/get-shape file :copy-clean-t)
;; Override font size on copy-font-size
file (update-attr file :copy-font-size-t font-size-path-0 "25")
copy-font-size-t (ths/get-shape file :copy-font-size-t)
;; Override text on copy-text
file (update-attr file :copy-text-t text-path-0 "text overriden")
copy-text-t (ths/get-shape file :copy-text-t)
;; Override both on copy-both
file (update-attr file :copy-both-t font-size-path-0 "25")
file (update-attr file :copy-both-t text-path-0 "text overriden")
copy-both-t (ths/get-shape file :copy-both-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
page' (thf/current-page file')
copy-clean' (ths/get-shape file' :copy-clean-2)
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
copy-font-size' (ths/get-shape file' :copy-font-size-2)
copy-font-size-t' (get-in page' [:objects (-> copy-font-size' :shapes first)])
copy-text' (ths/get-shape file' :copy-text-2)
copy-text-t' (get-in page' [:objects (-> copy-text' :shapes first)])
copy-both' (ths/get-shape file' :copy-both-2)
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Clean copy
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
;; After the switch:
;; * font size 50 (value of c02, because there was no override)
;; * text "hello world" (value of c02, because there was no override)
(t/is (= (get-in copy-clean-t' font-size-path-0) "50"))
(t/is (= (get-in copy-clean-t' text-path-0) "hello world"))
;;;;;;;;;;; Font size copy
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
;; After the switch:
;; * font size 50 (value of c02: the override is not preserved)
;; * text "hello world" (value of c02, because there was no override)
(t/is (= (get-in copy-font-size-t' font-size-path-0) "50"))
(t/is (= (get-in copy-font-size-t' text-path-0) "hello world"))
;;;;;;;;;;; Text copy
;; Before the switch:
;; * font size 14
;; * text "text overriden"
(t/is (= (get-in copy-text-t font-size-path-0) "14"))
(t/is (= (get-in copy-text-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 50 (value of c02, because there was no override)
;; * text "text overriden" (the override is preserved)
(t/is (= (get-in copy-text-t' font-size-path-0) "50"))
(t/is (= (get-in copy-text-t' text-path-0) "text overriden"))
;;;;;;;;;;; Both copy
;; Before the switch:
;; * font size 25
;; * text "text overriden"
(t/is (= (get-in copy-both-t font-size-path-0) "25"))
(t/is (= (get-in copy-both-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 50 (value of c02: the override is not preserved)
;; * text "text overriden" (the override is preserved)
(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)
;; Second comp has different text
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "bye")
(thc/instantiate-component :c01
:copy-clean
:children-labels [:copy-clean-t])
(thc/instantiate-component :c01
:copy-font-size
:children-labels [:copy-font-size-t])
(thc/instantiate-component :c01
:copy-text
:children-labels [:copy-text-t])
(thc/instantiate-component :c01
:copy-both
:children-labels [:copy-both-t]))
;; The copy clean has no overrides
copy-clean-t (ths/get-shape file :copy-clean-t)
;; Override font size on copy-font-size
file (update-attr file :copy-font-size-t font-size-path-0 "25")
copy-font-size-t (ths/get-shape file :copy-font-size-t)
;; Override text on copy-text
file (update-attr file :copy-text-t text-path-0 "text overriden")
copy-text-t (ths/get-shape file :copy-text-t)
;; Override both on copy-both
file (update-attr file :copy-both-t font-size-path-0 "25")
file (update-attr file :copy-both-t text-path-0 "text overriden")
copy-both-t (ths/get-shape file :copy-both-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
page' (thf/current-page file')
copy-clean' (ths/get-shape file' :copy-clean-2)
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
copy-font-size' (ths/get-shape file' :copy-font-size-2)
copy-font-size-t' (get-in page' [:objects (-> copy-font-size' :shapes first)])
copy-text' (ths/get-shape file' :copy-text-2)
copy-text-t' (get-in page' [:objects (-> copy-text' :shapes first)])
copy-both' (ths/get-shape file' :copy-both-2)
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Clean copy
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
;; After the switch:
;; * font size 25 (value of c02, because there was no override)
;; * text "bye" (value of c02, because there was no override)
(t/is (= (get-in copy-clean-t' font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t' text-path-0) "bye"))
;;;;;;;;;;; Font size copy
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
;; After the switch:
;; * font size 25 (the override is preserved)
;; * text "bye" (value of c02, because there was no override)
(t/is (= (get-in copy-font-size-t' font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t' text-path-0) "bye"))
;;;;;;;;;;; Text copy
;; Before the switch:
;; * font size 14
;; * text "text overriden"
(t/is (= (get-in copy-text-t font-size-path-0) "14"))
(t/is (= (get-in copy-text-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 14 (value of c02, because there was no override)
;; * text "text overriden" (value of c02: the override is not preserved)
(t/is (= (get-in copy-text-t' font-size-path-0) "14"))
(t/is (= (get-in copy-text-t' text-path-0) "bye"))
;;;;;;;;;;; Both copy
;; Before the switch:
;; * font size 25
;; * text "text overriden"
(t/is (= (get-in copy-both-t font-size-path-0) "25"))
(t/is (= (get-in copy-both-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 25 (the override is preserved)
;; * text "text overriden" (value of c02: the override is not preserved)
(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)
;; The second component has a different text and prop
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "bye")
(update-attr :t02 font-size-path-0 "50")
(thc/instantiate-component :c01
:copy-clean
:children-labels [:copy-clean-t])
(thc/instantiate-component :c01
:copy-font-size
:children-labels [:copy-font-size-t])
(thc/instantiate-component :c01
:copy-text
:children-labels [:copy-text-t])
(thc/instantiate-component :c01
:copy-both
:children-labels [:copy-both-t]))
;; The copy clean has no overrides
copy-clean-t (ths/get-shape file :copy-clean-t)
;; Override font size on copy-font-size
file (update-attr file :copy-font-size-t font-size-path-0 "25")
copy-font-size-t (ths/get-shape file :copy-font-size-t)
;; Override text on copy-text
file (update-attr file :copy-text-t text-path-0 "text overriden")
copy-text-t (ths/get-shape file :copy-text-t)
;; Override both on copy-both
file (update-attr file :copy-both-t font-size-path-0 "25")
file (update-attr file :copy-both-t text-path-0 "text overriden")
copy-both-t (ths/get-shape file :copy-both-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-text :c02 {:new-shape-label :copy-text-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-both :c02 {:new-shape-label :copy-both-2 :keep-touched? true}))
page' (thf/current-page file')
copy-clean' (ths/get-shape file' :copy-clean-2)
copy-clean-t' (get-in page' [:objects (-> copy-clean' :shapes first)])
copy-font-size' (ths/get-shape file' :copy-font-size-2)
copy-font-size-t' (get-in page' [:objects (-> copy-font-size' :shapes first)])
copy-text' (ths/get-shape file' :copy-text-2)
copy-text-t' (get-in page' [:objects (-> copy-text' :shapes first)])
copy-both' (ths/get-shape file' :copy-both-2)
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Clean copy
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
;; After the switch:
;; * font size 50 (value of c02, because there was no override)
;; * text "bye" (value of c02, because there was no override)
(t/is (= (get-in copy-clean-t' font-size-path-0) "50"))
(t/is (= (get-in copy-clean-t' text-path-0) "bye"))
;;;;;;;;;;; Font size copy
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
;; After the switch:
;; * font size 50 (value of c02: the override is not preserved)
;; * text "bye" (value of c02, because there was no override)
(t/is (= (get-in copy-font-size-t' font-size-path-0) "50"))
(t/is (= (get-in copy-font-size-t' text-path-0) "bye"))
;;;;;;;;;;; Text copy
;; Before the switch:
;; * font size 14
;; * text "text overriden"
(t/is (= (get-in copy-text-t font-size-path-0) "14"))
(t/is (= (get-in copy-text-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 50 (value of c02, because there was no override)
;; * text "bye" (value of c02: the override is not preserved)
(t/is (= (get-in copy-text-t' font-size-path-0) "50"))
(t/is (= (get-in copy-text-t' text-path-0) "bye"))
;;;;;;;;;;; Both copy
;; Before the switch:
;; * font size 25
;; * text "text overriden"
(t/is (= (get-in copy-both-t font-size-path-0) "25"))
(t/is (= (get-in copy-both-t text-path-0) "text overriden"))
;; After the switch:
;; * font size 50 (value of c02: the override is not preserved)
;; * text "bye" (value of c02: the override is not preserved)
(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)
;; Both components are identical: have the same text and props
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(thc/instantiate-component :c01
:copy-structure-clean
:children-labels [:copy-structure-clean-t])
(thc/instantiate-component :c01
:copy-structure-unif
:children-labels [:copy-structure-unif-t])
(thc/instantiate-component :c01
:copy-structure-mixed
:children-labels [:copy-structure-mixed-t]))
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
;; Duplicate a text line in copy-structure-clean, updating
;; both lines with the same attrs
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
(change-structure :copy-structure-unif-t))
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
;; Duplicate a text line in copy-structure-clean, updating
;; each line with a different attr
file (-> (change-structure file :copy-structure-mixed-t)
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
page' (thf/current-page file')
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
copy-structure-unif' (ths/get-shape file' :copy-structure-unif-2)
copy-structure-unif-t' (get-in page' [:objects (-> copy-structure-unif' :shapes first)])
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Copy structure clean
;; Before the switch, first line:
;; * font size 14
;; * text "new line 1"
;; Second line:
;; * font size 14
;; * text "new line 2"
(t/is (= (get-in copy-structure-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t font-size-path-1) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 14 (value of c02, because there was no override)
;; * text "new line 1" (the override is preserved)
;; Second line:
;; * font size 14 (value of c02, because there was no override)
;; * text "new line 2" (the override is preserved)
(t/is (= (get-in copy-structure-clean-t' font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t' text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t' font-size-path-1) "14"))
(t/is (= (get-in copy-structure-clean-t' text-path-1) "new line 2"))
;;;;;;;;;;; Copy structure unif
;; Before the switch, first line:
;; * font size 25
;; * text "new line 1"
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 25 (the override is preserved)
;; * text "new line 1" (the override is preserved)
;; Second line:
;; * font size 25 (the override is preserved)
;; * text "new line 2" (the override is preserved)
(t/is (= (get-in copy-structure-unif-t' font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t' text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t' font-size-path-1) "25"))
(t/is (= (get-in copy-structure-unif-t' text-path-1) "new line 2"))
;;;;;;;;;;; Copy structure mixed
;; Before the switch, first line:
;; * font size 35
;; * text "new line 1"
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(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"))
;; After the switch, first line:
;; * font size 35 (the override is preserved)
;; * text "new line 1" (the override is preserved)
;; Second line:
;; * font size 40 (the override is preserved)
;; * text "new line 2" (the override is preserved)
(t/is (= (get-in copy-structure-mixed-t' font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "new line 1"))
(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)
;; The second component has a different prop
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(update-attr :t02 font-size-path-0 "50")
(thc/instantiate-component :c01
:copy-structure-clean
:children-labels [:copy-structure-clean-t])
(thc/instantiate-component :c01
:copy-structure-unif
:children-labels [:copy-structure-unif-t])
(thc/instantiate-component :c01
:copy-structure-mixed
:children-labels [:copy-structure-mixed-t]))
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
;; Duplicate a text line in copy-structure-clean, updating
;; both lines with the same attrs
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
(change-structure :copy-structure-unif-t))
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
;; Duplicate a text line in copy-structure-clean, updating
;; each line with a different attr
file (-> (change-structure file :copy-structure-mixed-t)
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
page' (thf/current-page file')
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
copy-structure-unif' (ths/get-shape file' :copy-structure-unif-2)
copy-structure-unif-t' (get-in page' [:objects (-> copy-structure-unif' :shapes first)])
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Copy structure clean
;; Before the switch, first line:
;; * font size 14
;; * text "new line 1"
;; Second line:
;; * font size 14
;; * text "new line 2"
(t/is (= (get-in copy-structure-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t font-size-path-1) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 50 (value of c02, because there was no override)
;; * text "new line 1" (the override is preserved)
;; Second line:
;; * font size 50 (value of c02, because there was no override)
;; * text "new line 2" (the override is preserved)
(t/is (= (get-in copy-structure-clean-t' font-size-path-0) "50"))
(t/is (= (get-in copy-structure-clean-t' text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t' font-size-path-1) "50"))
(t/is (= (get-in copy-structure-clean-t' text-path-1) "new line 2"))
;;;;;;;;;;; Copy structure unif
;; Before the switch, first line:
;; * font size 25
;; * text "new line 1"
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 50 (the override is not preserved)
;; * text "new line 1" (the override is preserved)
;; Second line:
;; * font size 50 (the override is not preserved)
;; * text "new line 2" (the override is preserved)
(t/is (= (get-in copy-structure-unif-t' font-size-path-0) "50"))
(t/is (= (get-in copy-structure-unif-t' text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t' font-size-path-1) "50"))
(t/is (= (get-in copy-structure-unif-t' text-path-1) "new line 2"))
;;;;;;;;;;; Copy structure mixed
;; Before the switch, first line:
;; * font size 35
;; * text "new line 1"
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(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"))
;; After the switch, first line:
;; * font size 50 (the override is not preserved)
;; * text "hello world" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-mixed-t' font-size-path-0) "50"))
(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)
;; Second comp has different text
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "bye")
(thc/instantiate-component :c01
:copy-structure-clean
:children-labels [:copy-structure-clean-t])
(thc/instantiate-component :c01
:copy-structure-unif
:children-labels [:copy-structure-unif-t])
(thc/instantiate-component :c01
:copy-structure-mixed
:children-labels [:copy-structure-mixed-t]))
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
;; Duplicate a text line in copy-structure-clean, updating
;; both lines with the same attrs
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
(change-structure :copy-structure-unif-t))
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
;; Duplicate a text line in copy-structure-clean, updating
;; each line with a different attr
file (-> (change-structure file :copy-structure-mixed-t)
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
page' (thf/current-page file')
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
copy-structure-unif' (ths/get-shape file' :copy-structure-unif-2)
copy-structure-unif-t' (get-in page' [:objects (-> copy-structure-unif' :shapes first)])
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Copy structure clean
;; Before the switch, first line:
;; * font size 14
;; * text "new line 1"
;; Second line:
;; * font size 14
;; * text "new line 2"
(t/is (= (get-in copy-structure-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t font-size-path-1) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 14 (value of c02, because there was no override)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-clean-t' font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-clean-t' font-size-path-1)))
;;;;;;;;;;; Copy structure unif
;; Before the switch, first line:
;; * font size 25
;; * text "new line 1"
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 25 (the override is preserved)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-unif-t' font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-unif-t' font-size-path-1)))
;;;;;;;;;;; Copy structure mixed
;; Before the switch, first line:
;; * font size 35
;; * text "new line 1"
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(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"))
;; After the switch, first line:
;; * font size 14 (the override is not preserved)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-mixed-t' font-size-path-0) "14"))
(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)
;; The second component has a different text and prop
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "bye")
(update-attr :t02 font-size-path-0 "50")
(thc/instantiate-component :c01
:copy-structure-clean
:children-labels [:copy-structure-clean-t])
(thc/instantiate-component :c01
:copy-structure-unif
:children-labels [:copy-structure-unif-t])
(thc/instantiate-component :c01
:copy-structure-mixed
:children-labels [:copy-structure-mixed-t]))
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
;; Duplicate a text line in copy-structure-clean, updating
;; both lines with the same attrs
file (-> (update-attr file :copy-structure-unif-t font-size-path-0 "25")
(change-structure :copy-structure-unif-t))
copy-structure-unif-t (ths/get-shape file :copy-structure-unif-t)
;; Duplicate a text line in copy-structure-clean, updating
;; each line with a different attr
file (-> (change-structure file :copy-structure-mixed-t)
(update-attr :copy-structure-mixed-t font-size-path-0 "35")
(update-attr :copy-structure-mixed-t font-size-path-1 "40"))
copy-structure-mixed-t (ths/get-shape file :copy-structure-mixed-t)
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component-in-shape :copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
(tho/swap-component-in-shape :copy-structure-mixed :c02 {:new-shape-label :copy-structure-mixed-2 :keep-touched? true}))
page' (thf/current-page file')
copy-structure-clean' (ths/get-shape file' :copy-structure-clean-2)
copy-structure-clean-t' (get-in page' [:objects (-> copy-structure-clean' :shapes first)])
copy-structure-unif' (ths/get-shape file' :copy-structure-unif-2)
copy-structure-unif-t' (get-in page' [:objects (-> copy-structure-unif' :shapes first)])
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
(thf/dump-file file' {:keys [:name #_:content]})
;;;;;;;;;;; Copy structure clean
;; Before the switch, first line:
;; * font size 14
;; * text "new line 1"
;; Second line:
;; * font size 14
;; * text "new line 2"
(t/is (= (get-in copy-structure-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-clean-t font-size-path-1) "14"))
(t/is (= (get-in copy-structure-clean-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 50 (value of c02, because there was no override)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-clean-t' font-size-path-0) "50"))
(t/is (= (get-in copy-structure-clean-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-clean-t' font-size-path-1)))
;;;;;;;;;;; Copy structure unif
;; Before the switch, first line:
;; * font size 25
;; * text "new line 1"
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-1) "new line 2"))
;; After the switch, first line:
;; * font size 50 (the override is not preserved)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-unif-t' font-size-path-0) "50"))
(t/is (= (get-in copy-structure-unif-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-unif-t' font-size-path-1)))
;;;;;;;;;;; Copy structure mixed
;; Before the switch, first line:
;; * font size 35
;; * text "new line 1"
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(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"))
;; After the switch, first line:
;; * font size 50 (the override is not preserved)
;; * text "bye" (the override is not preserved)
;; No second line
(t/is (= (get-in copy-structure-mixed-t' font-size-path-0) "50"))
(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)
(tho/add-simple-component :external01 :external01-root :external01-child)
(tho/add-simple-component :external02 :external02-root :external02-child)
(thv/add-variant-with-copy
:v01 :c01 :m01 :c02 :m02 :cp01 :cp02 :external01)
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-cp01]))
page (thf/current-page file)
copy-cp01 (ths/get-shape file :copy-cp01)
copy-cp01-rect-id (-> copy-cp01 :shapes first)
;; On :copy-cp01, change the width of the rect
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{copy-cp01-rect-id}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
copy-cp01-rect (ths/get-shape-by-id file copy-cp01-rect-id)
;; ==== Action
;; Switch :c01 for :c02
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02 (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape-by-id file' (-> copy02 :shapes first))
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
;; The width of copy-cp01-rect was 25
(t/is (= (:width copy-cp01-rect) 25))
;; 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)
(tho/add-simple-component :external01 :external01-root :external01-child)
(tho/add-simple-component :external02 :external02-root :external02-child)
(thv/add-variant-with-copy
:v01 :c01 :m01 :c02 :m02 :cp01 :cp02 :external01)
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-cp01]))
copy01 (ths/get-shape file :copy01)
external02 (thc/get-component file :external02)
;; On :c01, swap the copy of :external01 for a copy of :external02
file (-> file
(tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
copy-cp02 (ths/get-shape file :copy-cp02)
;; ==== Action
;; Switch :c01 for :c02
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)]
(thf/dump-file file')
;;copy-cp02 is a copy of external02
(t/is (= (:component-id copy-cp02) (:id external02)))
;;copy-01 had copy-cp02 as child
(t/is (= (-> copy01 :shapes first) (:id copy-cp02)))
;;copy-cp02' is a copy of external02
(t/is (= (:component-id copy-cp02') (:id external02)))
;;copy-02' had copy-cp02' as child
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))))
(t/deftest test-switch-variant-that-has-swaped-copy-with-changed-attr
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :external01 :external01-root :external01-child)
(tho/add-simple-component :external02 :external02-root :external02-child)
(thv/add-variant-with-copy
:v01 :c01 :m01 :c02 :m02 :cp01 :cp02 :external01)
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-cp01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
external02 (thc/get-component file :external02)
;; On :c01, swap the copy of :external01 for a copy of :external02
file (-> file
(tho/swap-component-in-shape :copy-cp01 :external02 {:new-shape-label :copy-cp02 :keep-touched? false}))
copy-cp02 (ths/get-shape file :copy-cp02)
copy-cp02-rect-id (-> copy-cp02 :shapes first)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{copy-cp02-rect-id}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
copy-cp02-rect (ths/get-shape-by-id file copy-cp02-rect-id)
;; ==== Action
;; Switch :c01 for :c02
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
(thf/dump-file file')
;;copy-cp02 is a copy of external02
(t/is (= (:component-id copy-cp02) (:id external02)))
;;copy-01 had copy-cp02 as child
(t/is (= (-> copy01 :shapes first) (:id copy-cp02)))
;; The width of copy-cp02-rect was 25
(t/is (= (:width copy-cp02-rect) 25))
;;copy-cp02' is a copy of external02
(t/is (= (:component-id copy-cp02') (:id external02)))
;;copy-02' had copy-cp02' as child
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))
;; 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)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 5}})
(tho/add-simple-component :external01 :external01-root :external01-child)
(thc/instantiate-component :c01
:c01-in-root
:children-labels [:r01-in-c01-in-root]
:parent-label :external01-root))
;; Make a change on r01-in-c01-in-root so it is touched
page (thf/current-page file)
r01-in-c01-in-root (ths/get-shape file :r01-in-c01-in-root)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id r01-in-c01-in-root)}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
;; Instantiate the component :external01
file (thc/instantiate-component file
:external01
:external-copy01
:children-labels [:external-copy01-rect :c01-in-copy])
page (thf/current-page file)
c01-in-copy (ths/get-shape file :c01-in-copy)
rect01 (get-in page [:objects (-> c01-in-copy :shapes first)])
;; ==== Action
file' (tho/swap-component-in-shape file :c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
page' (thf/current-page file')
c02-in-copy' (ths/get-shape file' :c02-in-copy)
rect02' (get-in page' [:objects (-> c02-in-copy' :shapes first)])]
(thf/dump-file file :keys [:width :touched])
;; The rect had width 25 before the switch
(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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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-in-shape 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)))))
;; ============================================================
;; SELRECT CONSISTENCY TESTS
;; These tests verify that after a variant switch, the composite
;; geometry attributes (:selrect, :points) stay consistent with
;; the scalar attributes (:width, :height) that are kept.
;; ============================================================
(t/deftest test-switch-selrect-consistent-no-sizing-different-widths
;; When no :fix sizing and variants have different widths,
;; :width is correctly skipped (stays at new component width),
;; but :selrect was being copied from the old shape, leaving
;; selrect.width inconsistent with :width. This test verifies the fix.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)])
;; Override width AND selrect consistently (simulating a real resize)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(: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-in-shape 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 the width override before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Since the variants have different widths (100 vs 200), the override is not preserved
(t/is (= (:width rect02') 200))
;; The selrect must be consistent with :width
(t/is (= (get-in rect02' [:selrect :width]) 200))))
(t/deftest test-switch-selrect-consistent-no-sizing-different-heights
;; Same as above but for height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)])
;; Override height AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 150
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(: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-in-shape 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 the height override before the switch
(t/is (= (:height rect01) 150))
(t/is (= (get-in rect01 [:selrect :height]) 150))
;; Since the variants have different heights (100 vs 200), the override is not preserved
(t/is (= (:height rect02') 200))
;; The selrect must be consistent with :height
(t/is (= (get-in rect02' [:selrect :height]) 200))))
(t/deftest test-switch-with-v-sizing-fix-selrect-consistent-different-widths
;; mixed-sizing scenario: v-sizing=:fix but variants differ in WIDTH.
;; switch-fixed-layout-geom-change-value is triggered (because v-sizing=:fix).
;; Without the fix, the function returned prev-width for the non-:fix dimension,
;; leaving selrect.width inconsistent with :width.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :layout-item-v-sizing :fix}
:child2-params {:width 200 :height 50 :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)])
;; Override width AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(: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-in-shape 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 the width override before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Since the variants have different widths (100 vs 200), the override is not preserved
;; (v-sizing=:fix does not affect the horizontal dimension)
(t/is (= (:width rect02') 200))
;; The selrect must be consistent with :width
(t/is (= (get-in rect02' [:selrect :width]) 200))
;; v-sizing is preserved
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-with-h-sizing-fix-selrect-consistent-different-heights
;; mixed-sizing scenario: h-sizing=:fix but variants differ in HEIGHT.
;; switch-fixed-layout-geom-change-value is triggered (because h-sizing=:fix).
;; Without the fix, the function returned prev-height for the non-:fix dimension,
;; leaving selrect.height inconsistent with :height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 50 :height 100 :layout-item-h-sizing :fix}
:child2-params {:width 50 :height 200 :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)])
;; Override height AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 150
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(: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-in-shape 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 the height override before the switch
(t/is (= (:height rect01) 150))
(t/is (= (get-in rect01 [:selrect :height]) 150))
;; Since the variants have different heights (100 vs 200), the override is not preserved
;; (h-sizing=:fix does not affect the vertical dimension)
(t/is (= (:height rect02') 200))
;; The selrect must be consistent with :height
(t/is (= (get-in rect02' [:selrect :height]) 200))
;; h-sizing is preserved
(t/is (= (:layout-item-h-sizing rect02') :fix))))
;; ============================================================
;; FIXED-SIZING: "SAME-SIZE → PRESERVE OVERRIDE" PATH TESTS
;; These tests exercise the branch inside switch-fixed-layout-geom-change-value
;; where variants share the same value in the non-:fix dimension:
;; (if (= origin-dim current-dim) prev-dim current-dim)
;; When origin-dim == current-dim the user's override for that dimension
;; must be preserved after the switch.
;; ============================================================
(t/deftest test-switch-with-h-sizing-fix-same-height-override-preserved
;; h-sizing=:fix, variants have SAME height (non-:fix dim, same-size).
;; switch-fixed-layout-geom-change-value must return prev-height for the
;; non-:fix dimension because origin-height == current-height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)])
;; Override height (the non-:fix dimension) and selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 75
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(: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-in-shape 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 the height override 75 before the switch
(t/is (= (:height rect01) 75))
;; h-sizing=:fix means width always takes the new component's value
(t/is (= (:width rect02') 200))
;; Height (non-:fix dim) is preserved because both variants have same height (50)
(t/is (= (:height rect02') 75))
;; selrect must be consistent with the preserved height
(t/is (= (get-in rect02' [:selrect :height]) 75))
(t/is (= (get-in rect02' [:selrect :width]) 200))
;; h-sizing is preserved
(t/is (= (:layout-item-h-sizing rect02') :fix))))
(t/deftest test-switch-with-v-sizing-fix-same-width-override-preserved
;; v-sizing=:fix, variants have SAME width (non-:fix dim, same-size).
;; switch-fixed-layout-geom-change-value must return prev-width for the
;; non-:fix dimension because origin-width == current-width.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :layout-item-v-sizing :fix}
:child2-params {:width 100 :height 100 :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)])
;; Override width (the non-:fix dimension) and selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(: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-in-shape 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 the width override 150 before the switch
(t/is (= (:width rect01) 150))
;; Width (non-:fix dim) is preserved because both variants have same width (100)
(t/is (= (:width rect02') 150))
;; selrect must be consistent with the preserved width
(t/is (= (get-in rect02' [:selrect :width]) 150))
;; v-sizing=:fix means height always takes the new component's value
(t/is (= (:height rect02') 100))
(t/is (= (get-in rect02' [:selrect :height]) 100))
;; v-sizing is preserved
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-with-both-sizing-fix-overrides-discarded
;; When both h-sizing=:fix and v-sizing=:fix, switch-fixed-layout-geom-change-value
;; always uses current-width and current-height (the new component's values).
;; Both width and height overrides are discarded because :fix always
;; defers to the new component's dimension regardless of same-size or not.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50
:layout-item-h-sizing :fix
:layout-item-v-sizing :fix}
:child2-params {:width 200 :height 100
:layout-item-h-sizing :fix
: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)])
;; Override both width and height (and selrect) consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
new-height 75
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :height new-height)
(assoc :x2 (+ (:x1 sr) new-width))
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :width new-width)
(assoc :height new-height)
(assoc :selrect new-sr))))
(: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-in-shape 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 both overrides before the switch
(t/is (= (:width rect01) 150))
(t/is (= (:height rect01) 75))
;; With both sizing :fix, both dimensions take the new component's values
(t/is (= (:width rect02') 200))
(t/is (= (:height rect02') 100))
;; selrect must be consistent
(t/is (= (get-in rect02' [:selrect :width]) 200))
(t/is (= (get-in rect02' [:selrect :height]) 100))
(t/is (= (:layout-item-h-sizing rect02') :fix))
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-same-size-variants-geometry-override-preserved
;; When both variants have IDENTICAL dimensions (width=100, height=50),
;; the guard that skips :selrect/:points must NOT fire
;; (its condition `(or (not= origin.width current.width) ...)` is false).
;; A geometry override should therefore be carried through correctly.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50}
:child2-params {:width 100 :height 50}}) ; same size!
(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)])
;; Override width AND selrect consistently (simulating a real resize)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(: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-in-shape 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 the width override 150 before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Both variants are identical in size (100x50), so the override IS preserved
(t/is (= (:width rect02') 150))
;; The guard must not have suppressed :selrect — it should be consistent
(t/is (= (get-in rect02' [:selrect :width]) 150))))
(t/deftest test-switch-skips-composite-geometry-with-subpixel-drift
;; Regression: when the previous-shape's geometry only has sub-pixel drift
;; relative to its source master (a state produced by interactive transform
;; modifiers, e.g. alt-drag duplicate of a variant whose children are
;; component copies), equal-geometry? must classify it as unchanged and skip
;; copying composite geometry. Otherwise, :selrect/:points can carry stale
;; absolute positions from the source variant onto the freshly-instantiated
;; target, producing the visible "cut off" symptom.
(let [;; ==== Setup
;; A self-contained Input/Button-like component, plus a variant
;; container whose two variants each instance that component
;; at different y positions. This mirrors the production setup
;; where the dragged variant's children are themselves component
;; copies (and thus carry :touched on geometry within the master).
file (-> (thf/sample-file :file1)
(tho/add-simple-component :btn-comp :btn-root :btn-rect)
(thv/add-variant-with-copy
:v01 :c01 :m01 :c02 :m02 :child1 :child2 :btn-comp))
;; Position child1 at y=101 (in m01) and child2 at y=73 (in m02).
;; Use gsh/absolute-move so :selrect/:points stay consistent with
;; :y — a plain (assoc :y …) would leave them out of sync and
;; produce a different (artificial) failure mode.
page (thf/current-page file)
child1 (ths/get-shape file :child1)
child2 (ths/get-shape file :child2)
changes (-> (pcb/empty-changes nil (:id page))
(cls/generate-update-shapes
#{(:id child1)}
#(gsh/absolute-move % (gpt/point (:x %) 101))
(:objects page) {})
(cls/generate-update-shapes
#{(:id child2)}
#(gsh/absolute-move % (gpt/point (:x %) 73))
(:objects page) {}))
file (thf/apply-changes file changes)
file (thc/instantiate-component file :c01 :copy01)
;; The copy carries an Input/Button instance (Frame1). Introduce
;; sub-pixel drift in its :width and :selrect.width — the kind of
;; floating-point error produced by the alt-drag modifier path in
;; production. The drift is small enough to be treated as unchanged
;; geometry by equal-geometry?.
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
copy-btn-id (->> (cfh/get-children-ids-with-self (:objects page) (:id copy01))
(map #(get-in page [:objects %]))
(filter #(= "Frame1" (:name %)))
first :id)
drift 0.00001
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{copy-btn-id}
(fn [shape]
(let [w (+ (:width shape) drift)
sr (:selrect shape)]
(-> shape
(assoc :width w)
(assoc :selrect (-> sr
(assoc :width w)
(assoc :x2 (+ (:x1 sr) w)))))))
(:objects page) {})
file (thf/apply-changes file changes)
m02 (ths/get-shape file :m02)
child2 (ths/get-shape file :child2)
target-rel-y (- (:y child2) (:y m02))
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02
{:new-shape-label :copy02
:keep-touched? true})
page' (thf/current-page file')
copy02 (ths/get-shape file' :copy02)
post-btn (->> (cfh/get-children-ids-with-self (:objects page') (:id copy02))
(map #(get-in page' [:objects %]))
(filter #(= "Frame1" (:name %)))
first)
post-btn-rel-y (- (:y post-btn) (:y copy02))
post-btn-selrect-rel-y (- (get-in post-btn [:selrect :y])
(get-in copy02 [:selrect :y]))]
;; The post-switch button must sit at the target master's relative y.
;; Its :y field already does (the per-attr :y skip handles that
;; correctly); the failure is on :selrect.
(t/is (= target-rel-y post-btn-rel-y)
(str "Child's :y should match target master layout (" target-rel-y ")"))
;; The bug: :selrect.y is overwritten with the previous shape's value,
;; not regenerated from the target master's layout. After fix, this
;; assertion should pass.
(t/is (= target-rel-y post-btn-selrect-rel-y)
(str ":selrect.y should match target master layout (expected "
target-rel-y " got " post-btn-selrect-rel-y ")"))
;; And :y must agree with :selrect.y — a shape whose :y disagrees with
;; its :selrect.y is internally inconsistent and renders incorrectly.
(t/is (= post-btn-rel-y post-btn-selrect-rel-y)
":y and :selrect.y must agree after switch")))
(t/deftest test-switch-does-not-override-path-content-when-only-repositioned
;; Regression: when a path shape inside a variant has :geometry-group touched
;; (e.g. because auto-layout repositioned it after the copy's parent was
;; resized), switching variants must NOT copy the old variant's path position
;; to the new variant. The path should stay at the new variant's default position.
;;
;; Root cause: equal-geometry? did not handle the :content attr for path shapes,
;; so switch-path-change-value was always invoked and placed the new path at the
;; pre-switch absolute position instead of the target master's default position.
(let [;; A small closed triangle path whose bounding box is 24x14 px,
;; anchored at absolute position (x0, y0).
triangle (fn [x0 y0]
[{:command :move-to :params {:x x0 :y y0}}
{:command :line-to :params {:x (+ x0 24) :y y0}}
{:command :line-to :params {:x (+ x0 12) :y (+ y0 14)}}
{:command :close-path}])
;; V1 has the path at y=10; V2 has the same-shape path at y=30.
file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 100 :height 100}
:variant2-params {:width 100 :height 100}})
(ths/add-sample-shape :path1 :type :path
:parent-label :m01
:content (triangle 0 10))
(ths/add-sample-shape :path2 :type :path
:parent-label :m02
:content (triangle 0 30))
(thc/instantiate-component :c01 :copy01))
;; Simulate auto-layout repositioning the path inside the copy by
;; moving it to y=50. This touches :geometry-group on the copy's path.
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
copy-path (->> (cfh/get-children-with-self (:objects page) (:id copy01))
(filter #(= :path (:type %)))
first)
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{(:id copy-path)}
#(gsh/absolute-move % (gpt/point (:x %) 50))
(:objects page) {})
file (thf/apply-changes file changes)
;; Switch copy01 from V1 (c01) to V2 (c02).
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
page' (thf/current-page file')
copy01' (ths/get-shape file' :copy01)
copy-path' (->> (cfh/get-children-with-self (:objects page') (:id copy01'))
(filter #(= :path (:type %)))
first)
;; Expected: V2's path sits at y=30 (its master default), not y=50
;; (the pre-switch repositioned position).
m02 (ths/get-shape file :m02)
path2 (ths/get-shape file :path2)
target-rel-y (- (-> path2 :selrect :y) (-> m02 :selrect :y))
actual-rel-y (- (-> copy-path' :selrect :y) (-> copy01' :selrect :y))]
(t/is (some? copy-path') "path should exist in switched copy")
(t/is (= target-rel-y actual-rel-y)
(str "path :selrect.y should match target master layout (expected "
target-rel-y " got " actual-rel-y ")"))))
(t/deftest test-switch-preserves-size-override-at-target-position
(let [move-to (fn [shape x y]
(gsh/move shape (gpt/point (- x (:x shape))
(- y (:y shape)))))
;; ==== Setup: each variant contains the same nested component instance.
;; The nested instance has identical size in both variants, but a different
;; position relative to the variant root.
file (-> (thf/sample-file :file1)
(tho/add-simple-component
:nested-component :nested-main :nested-label
:root-params {:width 100 :height 50}
:child-params {:width 30 :height 10})
(thv/add-variant-with-copy
:v01 :c01 :m01 :c02 :m02 :r01 :r02 :nested-component))
page (thf/current-page file)
r01 (ths/get-shape file :r01)
r02 (ths/get-shape file :r02)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id r01) (:id r02)}
(fn [shape]
(cond
(= (:id shape) (:id r01)) (move-to shape 20 100)
(= (:id shape) (:id r02)) (move-to shape 20 70)
:else shape))
(:objects page)
{})
file (thf/apply-changes file changes)
file (thc/instantiate-component file :c01
:copy01
:children-labels [:copy-r01])
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
copy-r01 (get-in page [:objects (-> copy01 :shapes first)])
;; This is a real geometry override, not float drift. The switch should
;; preserve the overridden size while anchoring composite geometry to
;; the target variant's position.
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-r01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr)
(assoc :touched #{:geometry-group}))))
(:objects page)
{})
file (thf/apply-changes file changes)
;; ==== Action
file' (tho/swap-component-in-shape 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 width override is preserved, but the target variant position remains
;; authoritative for absolute composite geometry.
(t/is (= 150 (:width rect02')))
(t/is (= (+ (:y copy02') 70) (:y rect02')))
(t/is (= (:y rect02') (get-in rect02' [:selrect :y])))))
;; ============================================================
;; PRESERVE TEXT SUB-TOUCHED FLAGS ACROSS VARIANT SWITCH
;; ============================================================
(t/deftest test-switch-preserves-text-sub-touched-flags
;; 1. Creates a component with text "hello world" + font-size "14", variant with font-size "20"
;; 2. Overrides only text on the copy → verifies :text-content-text in touched
;; 3. Switches to variant → verifies text override preserved, font-size updated, :text-content-text preserved
;; 4. Updates main font-size to "30" and syncs → verifies font-size synced but text override preserved
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
;; c01 has text "hello world" font-size "14"
;; c02 has text "hello world" font-size "20" (same text, different font-size)
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(update-attr :t02 font-size-path-0 "20")
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-t01]))
;; Override only the TEXT on the copy (not font-size)
file (update-attr file :copy-t01 text-path-0 "custom text")
copy-t01 (ths/get-shape file :copy-t01)]
;; Verify the copy has the text override and correct touched flags
(t/is (= (get-in copy-t01 text-path-0) "custom text"))
(t/is (= (get-in copy-t01 font-size-path-0) "14"))
(t/is (contains? (:touched copy-t01) :content-group))
(t/is (contains? (:touched copy-t01) :text-content-text))
(t/is (not (contains? (:touched copy-t01) :text-content-attribute)))
(t/is (not (contains? (:touched copy-t01) :text-content-structure)))
;; ==== Action: Switch copy to c02 variant (same text, different font-size)
(let [file' (tho/swap-component-in-shape file :copy01 :c02
{:new-shape-label :copy02
:keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
copy-t02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; After switch: text override preserved (same text between variants),
;; font-size updated from variant, touched preserves text-content-text
(t/is (= (get-in copy-t02' text-path-0) "custom text"))
(t/is (= (get-in copy-t02' font-size-path-0) "20"))
(t/is (contains? (:touched copy-t02') :content-group))
(t/is (contains? (:touched copy-t02') :text-content-text))
(t/is (not (contains? (:touched copy-t02') :text-content-attribute)))
;; ==== Now test subsequent component sync
;; Modify the main component's font-size to "30" (keeping text "hello world")
(let [main-text (ths/get-shape file' :t02)
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page'))
#{(:id main-text)}
(fn [shape]
(assoc-in shape font-size-path-0 "30"))
(:objects page')
{})
updated-file (thf/apply-changes file' changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :c02)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
synced-file (thf/apply-changes updated-file changes2)
synced-copy (ths/get-shape synced-file :copy02)
synced-t (get-in (thf/current-page synced-file)
[:objects (-> synced-copy :shapes first)])]
;; The text override is preserved and font-size is synced
(t/is (= (get-in synced-t text-path-0) "custom text"))
(t/is (= (get-in synced-t font-size-path-0) "30"))
(t/is (contains? (:touched synced-t) :content-group))
(t/is (contains? (:touched synced-t) :text-content-text))))))
;; ============================================================
;; SWAPPED CHILD POSITION
;; ============================================================
(defn- setup-variant-with-swapped-icon
"Variants whose roots hold an icon A copy, placed 8px lower in the second
variant, and a copy of the first variant with its icon swapped to icon B."
[]
(-> (thf/sample-file :file1)
(tho/add-simple-component :icon-a :icon-a-root :icon-a-child)
(tho/add-simple-component :icon-b :icon-b-root :icon-b-child)
(thv/add-variant-with-copy :v01 :c01 :m01 :c02 :m02 :m01-icon :m02-icon :icon-a)
(tho/move-shape-tree :m02-icon (gpt/point 0 8))
(thc/instantiate-component :c01 :copy01 :children-labels [:copy-icon])
(thc/component-swap :copy-icon :icon-b :copy-icon-b)))
(defn- offset-in-root
[file root-label child-label]
(let [root (ths/get-shape file root-label)
child (ths/get-shape file child-label)]
(gpt/point (- (:x child) (:x root))
(- (:y child) (:y root)))))
(t/deftest test-switch-places-swapped-child-where-new-variant-occupant-is
(let [;; ==== Setup
file (setup-variant-with-swapped-icon)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02
:keep-touched? true})]
;; ==== Check
(t/is (gpt/close? (offset-in-root file :m02 :m02-icon)
(offset-in-root file' :copy02 :copy-icon-b)))))
(defn- resize-width
[shape width]
(gsh/transform-shape shape (ctm/change-size shape width nil)))
(defn- setup-card-copy
"Variants of a 300x48 flex card whose `:m01-icon` / `:m02-icon` is an
absolute icon A copy placed directly in the root at x 270, with the given
horizontal constraint. A copy of the first variant fills 800px more, the
way the layout grows it."
[constraints-h]
(let [place-icon (fn [file root-label icon-label]
(let [root (ths/get-shape file root-label)
icon (ths/get-shape file icon-label)]
(-> file
(tho/move-shape-tree icon-label (gpt/point (- (+ (:x root) 270) (:x icon))
(- (+ (:y root) 12) (:y icon))))
(tho/change-shape icon-label #(assoc % :layout-item-absolute true
:constraints-h constraints-h)))))
card (fn [file root-label]
(tho/change-shape file root-label #(-> %
(resize-width 300)
(gsh/transform-shape (ctm/change-size % nil 48))
(assoc :layout :flex :layout-flex-dir :row))))]
(-> (thf/sample-file :file1)
(tho/add-simple-component :icon-a :icon-a-root :icon-a-child
:root-params {:width 24 :height 24}
:child-params {:width 24 :height 24})
(tho/add-simple-component :icon-b :icon-b-root :icon-b-child
:root-params {:width 24 :height 24}
:child-params {:width 24 :height 24})
(thv/add-variant-with-copy :v01 :c01 :m01 :c02 :m02 :m01-icon :m02-icon :icon-a)
(card :m01)
(card :m02)
(place-icon :m01 :m01-icon)
(place-icon :m02 :m02-icon)
(thc/instantiate-component :c01 :copy01 :children-labels [:copy-icon])
(tho/change-shape :copy01 #(resize-width % 1100) :ignore-touched true)
(tho/move-shape-tree :copy-icon (gpt/point (case constraints-h :right 800 0) 0) :ignore-touched true))))
(defn- setup-card-variant
"Same as `setup-card-copy`, with the icon of the copy swapped to icon B."
[constraints-h]
(-> (setup-card-copy constraints-h)
(thc/component-swap :copy-icon :icon-b :copy-icon-b)
;; The swap drops the constraints; set them again so this path runs
(tho/change-shape :copy-icon-b #(assoc % :constraints-h constraints-h))))
(defn- distance-to-right-edge
[file root-label child-label]
(let [root (ths/get-shape file root-label)
child (ths/get-shape file child-label)]
(- (+ (:x root) (:width root)) (:x child))))
(t/deftest test-switch-moved-swapped-child-follows-right-constraint
(let [;; ==== Setup
file (-> (setup-card-variant :right)
(tho/move-shape-tree :copy-icon-b (gpt/point 5 0)))
before (distance-to-right-edge file :copy01 :copy-icon-b)
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02
:keep-touched? true})]
;; ==== Check
(t/is (mth/close? before (distance-to-right-edge file' :copy02 :copy-icon-b)))))
(t/deftest test-switch-scales-swapped-child-back-with-root
(let [;; ==== Setup
file (setup-card-variant :scale)
width (:width (ths/get-shape file :copy-icon-b))
root-w (:width (ths/get-shape file :copy01))
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02
:keep-touched? true})
scale (/ (:width (ths/get-shape file' :copy02)) root-w)
inner #(ths/get-shape-by-id % (first (:shapes (ths/get-shape % :copy-icon-b))))]
;; ==== Check
(t/is (mth/close? (* width scale)
(:width (ths/get-shape file' :copy-icon-b))))
;; The icon content follows the icon through its own (left) constraint
(t/is (mth/close? (:width (inner file)) (:width (inner file'))))))
(t/deftest test-switch-scales-moved-child-back-with-root
(let [;; ==== Setup
card (setup-card-copy :scale)
grow (/ (:width (ths/get-shape card :copy01))
(:width (ths/get-shape card :m01)))
file (-> card
;; The layout scales the icon with the copy
(tho/change-shape :copy-icon #(resize-width % (* grow (:width %))) :ignore-touched true)
(tho/move-shape-tree :copy-icon (gpt/point 5 0)))
width (:width (ths/get-shape file :copy-icon))
root-w (:width (ths/get-shape file :copy01))
;; ==== Action
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02
:keep-touched? true})
;; ==== Get
copy02' (ths/get-shape file' :copy02)
icon' (get-in (thf/current-page file') [:objects (-> copy02' :shapes first)])]
;; ==== Check
(t/is (mth/close? (* width (/ (:width copy02') root-w)) (:width icon')))))