penpot/common/test/common_tests/logic/variants_switch_test.cljc
alonso.torres ac7287d9cd ✨ Give each style of a copy text its own override
A copy text had three override subgroups: its letters, any style, and
its structure (the number of paragraphs or runs). One style override
kept every other style of the main out (F72), letters and a style
together kept everything out (F73), a structure change kept the main's
styles out unless both texts had one style (F74), which the editors'
content never had (F79), and alignment, direction and vertical
alignment overrides were lost at the next sync (F76).

Now a copy text has :text-content-text for its letters and line breaks
and one group per style unit: family, weight (with style and variant),
size, line height, spacing, decoration, case, fills, typography,
alignment, direction and vertical alignment (cttx/style-units). An edit
sets the groups of what it changes: a letter both texts share that
changes its style, or a style that was not in the text before, so typed
letters touch no style. A token in the content sets the groups of the
styles it sets, and token sync of a text follows them.

Sync and the keep-touched variant switch share one merge
(cttx/merge-overrides): the letters of one side, and each unit from its
side placed on them. A unit with one value applies everywhere; else
letters both texts share (stretches of three or more) take the source's
style, and the rest follow the letter before them. The switch carries
an override only when the old and new mains agree on it (F77, F81), and
the copy then holds the groups its result differs in. A typography
reference stays only when every unit it sets comes from its side. The
variant id goes with the weight; where the family comes from the other
side, the frontend has to pick the family's closest variant.

app.common.types.text reads a content as letters, each with its run and
paragraph, and builds the content back from them; it gives the style of
one unit for a letter, compares styles by what they show, and finds the
letters two texts share with a letter diff (Myers). The component
contract memory describes the model.

Files older than this keep :text-content-attribute and
:text-content-structure, read as every group they may cover until the
migration recomputes them.

Tests whose expected values the model changes: text-sync-test (the
copy's weight, letters or new run no longer keep the main's size out;
the letters read whole), text-touched-test (its edits went to the
paragraph node, which shows nothing; they now edit the run),
variants-switch-test (the copies with two runs keep their letters
across a switch, and the size override the mains agree on), the
editor output table, case Y and the notation test (new groups). The 1j
pins of F72-F79 and F81 go; F79's direct test of equal-attrs? becomes a
sync of editor content. A new check holds the production override rule
to the 1j reference rule. New tests: the letter view, and a detached,
duplicated or reset copy text keeping or clearing its override groups.

Benchmark (lower quantile, ms; baseline / now / ratio):
  deepest-edit-through-three-levels    35.2 /  36.6 / 1.04
  edit-through-a-chain-of-dependents   68.9 /  67.2 / 0.98
  library-update-into-copies           83.1 /  72.5 / 0.87
  main-edit-into-10-copies              1.8 /   1.5 / 0.82
  main-edit-into-100-copies            15.4 /  14.9 / 0.97
  main-edit-into-1000-copies          153.1 / 153.4 / 1.00
  variant-switch-into-copies           32.1 /  27.1 / 0.85
The worlds hold no text. A main text edit synced into 100 copy texts
that changed their letters takes 40 ms, against 29 ms before: the merge
aligns the letters of each copy with the main's.

AI-assisted-by: claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:22:21 +02:00

3263 lines
153 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.rect :as grc]
[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.composable.core :as tm]
[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]
[app.common.types.path.segment :as segment]
[app.common.types.text :as txt]
[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)]
;; 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)]
;; 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)]
;; 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))
;; A switch in a main leaves a plain nested copy, not touched
(t/is (empty? (:touched copy01')))
(t/is (not (ctk/swapped? 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)]
;; 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))
;; A switch in a copy is a swap of the position it refers to
(t/is (= #{:swap-group} (:touched child01')))
(t/is (= (thi/id :nested01) (:shape-ref child01')))))
;; ============================================================
;; 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)]
;; 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 a plain nested copy
(t/is (= #{:fill-group} (:touched copy01')))
(t/is (not (ctk/swapped? copy01')))))
(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)]
;; 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 a swap of the position it refers to
(t/is (= #{:fill-group :swap-group} (:touched nested02')))
(t/is (= (thi/id :nested01) (:shape-ref nested02')))))
;; ============================================================
;; 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- runs
"The letters and font size of the text `shape`, by font size."
[shape]
(reduce (fn [acc [letters size]]
(let [[prev-letters prev-size] (peek acc)]
(if (= prev-size size)
(conj (pop acc) [(str prev-letters letters) size])
(conj acc [letters size]))))
[]
(map (juxt :text :font-size) (txt/node-seq txt/is-text-node? (:content shape)))))
(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)])]
;;;;;;;;;;; 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)])]
;;;;;;;;;;; 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)])]
;;;;;;;;;;; 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)])]
;;;;;;;;;;; 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)])]
;;;;;;;;;;; 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, the letters are preserved, and the size is c02's
;; (there was no override)
(t/is (= [["new line 1new line 2" "14"]] (runs copy-structure-clean-t')))
;;;;;;;;;;; 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, the letters and the size are preserved
(t/is (= [["new line 1new line 2" "25"]] (runs copy-structure-unif-t')))
;;;;;;;;;;; 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)])]
;;;;;;;;;;; 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, the letters are preserved and the size is c02's
;; (there was no override)
(t/is (= [["new line 1new line 2" "50"]] (runs copy-structure-clean-t')))
;;;;;;;;;;; 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, the letters are preserved and the size is c02's
;; (the variants differ in it)
(t/is (= [["new line 1new line 2" "50"]] (runs copy-structure-unif-t')))
;;;;;;;;;;; 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, the letters are preserved (the variants have the
;; same) and the size is c02's (the variants differ in it)
(t/is (= [["new line 1new line 2" "50"]] (runs copy-structure-mixed-t')))))
;; ============================================================
;; 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)])]
;;;;;;;;;;; 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, the letters are c02's (the variants differ in them)
;; and the sizes are preserved (the variants agree on them): "bye"
;; shares no letters with the copy, so it takes the size of the copy's
;; first letter
(t/is (= [["bye" "35"]] (runs copy-structure-mixed-t')))))
;; ============================================================
;; 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)])]
;;;;;;;;;;; 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)]
;;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))]
;;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)])]
;; 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))))))
;; ============================================================
;; GEOMETRY OVERRIDES FROM A REAL RESIZE
;; A transform updates :selrect and :points too, so the switch
;; carries them as well as :width and :height.
;; ============================================================
(defn- copy-child
[file label]
(let [page (thf/current-page file)]
(get-in page [:objects (-> (ths/get-shape file label) :shapes first)])))
(defn- resize-copy-child
"Resize the first child of the copy `label` by `sx`, `sy` from its top
left corner, as a transform does."
[file label sx sy]
(let [page (thf/current-page file)
child (copy-child file label)
origin (gpt/point (-> child :selrect :x) (-> child :selrect :y))
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{(:id child)}
#(gsh/transform-shape % (ctm/resize-modifiers (gpt/point sx sy) origin))
(:objects page)
{})]
(thf/apply-changes file changes)))
(defn- size-is
"Assert that `shape`'s :selrect and :points both measure `width` x
`height`."
[shape width height]
(let [points (grc/points->rect (:points shape))]
(t/is (mth/close? width (-> shape :selrect :width)))
(t/is (mth/close? height (-> shape :selrect :height)))
(t/is (mth/close? width (:width points)))
(t/is (mth/close? height (:height points)))))
(t/deftest test-switch-keeps-a-resized-child-of-same-size-variants
(let [file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :type :rect}
:child2-params {:width 100 :height 50 :type :rect}})
(thc/instantiate-component :c01 :copy01 :children-labels [:copy-r01])
(resize-copy-child :copy01 2 1))
;; the switch moves the labels, so read the copy before it
rect (copy-child file :copy01)
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
rect' (copy-child file' :copy02)]
(size-is rect 200 50)
;; the variants agree on the size, so the override is carried
(size-is rect' 200 50)
(t/is (contains? (:touched rect') :geometry-group))))
(t/deftest test-switch-with-h-sizing-fix-keeps-the-resized-size
(let [file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :type :rect :layout-item-h-sizing :fix}
:child2-params {:width 100 :height 50 :type :rect :layout-item-h-sizing :fix}})
(thc/instantiate-component :c01 :copy01 :children-labels [:copy-r01])
(resize-copy-child :copy01 2 2))
rect (copy-child file :copy01)
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})]
(size-is rect 200 100)
;; the variants agree on the size, so the touched size is carried; a
;; fixed sizing takes the new variant's width instead
(let [rect' (copy-child file' :copy02)]
(tm/known-failure "F61" [200 100] [100 100]
(mapv #(mth/round (get-in rect' [:selrect %])) [:width :height])))))
(t/deftest test-switch-keeps-a-resized-path-of-same-size-variants
(let [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}])
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)
(resize-copy-child :copy01 2 2))
path (copy-child file :copy01)
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
path' (copy-child file' :copy02)
content (segment/content->selrect (:content path'))]
(size-is path 48 28)
;; the path keeps its size: its content is scaled like its box
(t/is (= :path (:type path')))
(size-is path' 48 28)
(t/is (mth/close? 48 (:width content)))
(t/is (mth/close? 28 (:height content)))))
(t/deftest test-switch-to-a-main-with-another-text-structure
;; The second variant's text has other letters, so the copy takes them.
;; A style override the mains agree on is carried onto them; when the
;; mains disagree on the style, it is dropped.
(let [setup (fn [main-font-size]
(-> (thf/sample-file :file1)
(thv/add-variant-with-text
:v01 :c01 :m01 :c02 :m02 :t01 :t02 "hello world" "hello world")
(change-structure :t02)
(cond-> main-font-size
(-> (update-attr :t02 font-size-path-0 main-font-size)
(update-attr :t02 font-size-path-1 main-font-size)))
(thc/instantiate-component :c01 :copy01 :children-labels [:copy01-t])
(update-attr :copy01-t font-size-path-0 "25")))
switched (fn [file]
(-> (tho/swap-component-in-shape file :copy01 :c02
{:new-shape-label :copy02 :keep-touched? true})
(copy-child :copy02)))
agree (switched (setup nil))
disagree (switched (setup "50"))]
(t/is (= [["new line 1new line 2" "25"]] (runs agree)))
;; the carried style stays an override
(t/is (contains? (:touched agree) :content-group))
(t/is (= "new line 1" (get-in disagree text-path-0)))
(t/is (= "new line 2" (get-in disagree text-path-1)))
(t/is (= "50" (get-in disagree font-size-path-0)))
(t/is (= "50" (get-in disagree font-size-path-1)))
;; nothing was carried, so nothing is touched
(t/is (not (contains? (:touched disagree) :content-group)))))