penpot/common/test/common_tests/logic/comp_creation_test.cljc
Eva Marco 743431bd42 🐛 Validate pasted shapes where they are pasted
generate-duplicate-changes looked for the instance roots of the copied
shapes in the page they were copied from, and mapped them to the new
ids. Pasting a main into another main creates a nested copy, but the
copy was validated on its own and reported :nested-copy-not-allowed.
Shapes copied from inside a component and pasted outside were not
validated at all: the unchanged source root was validated instead.

Look for the instance roots from the new shapes, in the objects of the
changes builder, where they already are in their new place.

This fixes three tests in frontend pasting_in_containers_test.

AI-assisted-by: claude-opus-5-5
2026-10-28 12:53:06 +01:00

889 lines
36 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.comp-creation-test
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.variants :as thv]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-add-component-from-single-frame
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(ths/add-sample-shape :frame1 :type :frame))
page (thf/current-page file)
frame1 (ths/get-shape file :frame1)
;; ==== Action
[_ component-id changes]
(cll/generate-add-component (pcb/empty-changes)
[frame1]
(:objects page)
(:id page)
(:id file)
nil)
file' (thf/apply-changes file changes)
;; ==== Get
component (thc/get-component-by-id file' component-id)
root (ths/get-shape-by-id file' (:main-instance-id component))
frame1' (ths/get-shape file' :frame1)]
;; ==== Check
(t/is (some? component))
(t/is (some? root))
(t/is (some? frame1'))
(t/is (= (:id root) (:id frame1')))
(t/is (ctk/main-instance? root))
(t/is (ctk/main-instance-of? (:id root) (:id page) component))))
(t/deftest test-add-component-from-single-shape
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(ths/add-sample-shape :shape1 :type :rect))
page (thf/current-page file)
shape1 (ths/get-shape file :shape1)
;; ==== Action
[_ component-id changes]
(cll/generate-add-component (pcb/empty-changes)
[shape1]
(:objects page)
(:id page)
(:id file)
cfsh/prepare-create-artboard-from-selection)
file' (thf/apply-changes file changes)
;; ==== Get
component (thc/get-component-by-id file' component-id)
root (ths/get-shape-by-id file' (:main-instance-id component))
shape1' (ths/get-shape file' :shape1)]
;; ==== Check
(t/is (some? component))
(t/is (some? root))
(t/is (some? shape1'))
(t/is (ctst/parent-of? root shape1'))
(t/is (= (:type root) :frame))
(t/is (ctk/main-instance? root))
(t/is (ctk/main-instance-of? (:id root) (:id page) component))))
(t/deftest test-add-component-from-several-shapes
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(ths/add-sample-shape :shape1 :type :rect)
(ths/add-sample-shape :shape2 :type :rect))
page (thf/current-page file)
shape1 (ths/get-shape file :shape1)
shape2 (ths/get-shape file :shape2)
;; ==== Action
[_ component-id changes]
(cll/generate-add-component (pcb/empty-changes)
[shape1 shape2]
(:objects page)
(:id page)
(:id file)
cfsh/prepare-create-artboard-from-selection)
file' (thf/apply-changes file changes)
;; ==== Get
component (thc/get-component-by-id file' component-id)
root (ths/get-shape-by-id file' (:main-instance-id component))
shape1' (ths/get-shape file' :shape1)
shape2' (ths/get-shape file' :shape2)]
;; ==== Check
(t/is (some? component))
(t/is (some? root))
(t/is (some? shape1'))
(t/is (some? shape2'))
(t/is (ctst/parent-of? root shape1'))
(t/is (ctst/parent-of? root shape2'))
(t/is (= (:type root) :frame))
(t/is (ctk/main-instance? root))
(t/is (ctk/main-instance-of? (:id root) (:id page) component))))
(t/deftest test-add-component-from-several-frames
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(ths/add-sample-shape :frame1 :type :frame)
(ths/add-sample-shape :frame2 :type :frame))
page (thf/current-page file)
frame1 (ths/get-shape file :frame1)
frame2 (ths/get-shape file :frame2)
;; ==== Action
[_ component-id changes]
(cll/generate-add-component (pcb/empty-changes)
[frame1 frame2]
(:objects page)
(:id page)
(:id file)
cfsh/prepare-create-artboard-from-selection)
file' (thf/apply-changes file changes)
;; ==== Get
component (thc/get-component-by-id file' component-id)
root (ths/get-shape-by-id file' (:main-instance-id component))
frame1' (ths/get-shape file' :frame1)
frame2' (ths/get-shape file' :frame2)]
;; ==== Check
(t/is (some? component))
(t/is (some? root))
(t/is (some? frame1'))
(t/is (some? frame2'))
(t/is (ctst/parent-of? root frame1'))
(t/is (ctst/parent-of? root frame2'))
(t/is (= (:type root) :frame))
(t/is (ctk/main-instance? root))
(t/is (ctk/main-instance-of? (:id root) (:id page) component))))
(t/deftest test-add-component-from-frame-with-children
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(ths/add-sample-shape :frame1 :type :frame)
(ths/add-sample-shape :shape1 :type :rect :parent-label :frame1)
(ths/add-sample-shape :shape2 :type :rect :parent-label :frame1))
page (thf/current-page file)
frame1 (ths/get-shape file :frame1)
;; ==== Action
[_ component-id changes]
(cll/generate-add-component (pcb/empty-changes)
[frame1]
(:objects page)
(:id page)
(:id file)
nil)
file' (thf/apply-changes file changes)
;; ==== Get
component (thc/get-component-by-id file' component-id)
root (ths/get-shape-by-id file' (:main-instance-id component))
frame1' (ths/get-shape file' :frame1)
shape1' (ths/get-shape file' :shape1)
shape2' (ths/get-shape file' :shape2)]
;; ==== Check
(t/is (some? component))
(t/is (some? root))
(t/is (some? frame1'))
(t/is (= (:id root) (:id frame1')))
(t/is (ctst/parent-of? frame1' shape1'))
(t/is (ctst/parent-of? frame1' shape2'))
(t/is (ctk/main-instance? root))
(t/is (ctk/main-instance-of? (:id root) (:id page) component))))
(t/deftest test-add-component-from-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root))
page (thf/current-page file)
copy1-root (ths/get-shape file :copy1-root)
;; ==== Action
[_ component2-id changes]
(cll/generate-add-component (pcb/empty-changes)
[copy1-root]
(:objects page)
(:id page)
(:id file)
cfsh/prepare-create-artboard-from-selection)
file' (thf/apply-changes file changes)
;; ==== Get
component2' (thc/get-component-by-id file' component2-id)
root2' (ths/get-shape-by-id file' (:main-instance-id component2'))
copy1-root' (ths/get-shape file' :copy1-root)]
;; ==== Check
(t/is (some? component2'))
(t/is (some? root2'))
(t/is (some? copy1-root'))
(t/is (ctst/parent-of? root2' copy1-root'))
(t/is (ctk/main-instance? root2'))
(t/is (ctk/main-instance-of? (:id root2') (:id page) component2'))))
(t/deftest test-rename-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1
:main1-root
:main1-child
:name "Test component before"))
component (thc/get-component file :component1)
;; ==== Action
changes (-> (pcb/empty-changes nil (thf/current-page-id file))
(pcb/with-file-data (ctf/file-data file))
(cll/generate-rename-component
(:id component)
"Test component after"
(:data file))
(pcb/update-shapes
[(thi/id :main1-root)]
#(assoc % :name "Test component after")))
file' (thf/apply-changes file changes)
;; ==== Get
component' (thc/get-component file' :component1)
main1-root' (ths/get-shape file' :main1-root)]
;; ==== Check
(t/is (= (:name component') "Test component after"))
(t/is (= (:name main1-root') "Test component after"))))
(t/deftest test-duplicate-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1
:main1-root
:main1-child))
component (thc/get-component file :component1)
;; ==== Action
[_ changes]
(cll/generate-duplicate-component (pcb/empty-changes)
file
(:id component)
(uuid/next)
true)
file' (thf/apply-changes file changes)
;; ==== Get
components' (ctkl/components-seq (:data file'))
component1' (d/seek #(= (:id %) (thi/id :component1)) components')
component2' (d/seek #(not= (:id %) (thi/id :component1)) components')
root1' (ths/get-shape-by-id file' (:main-instance-id component1'))
root2' (ths/get-shape-by-id file' (:main-instance-id component2'))
child1' (ths/get-shape-by-id file' (first (:shapes root1')))
child2' (ths/get-shape-by-id file' (first (:shapes root2')))]
;; ==== Check
(t/is (= 2 (count components')))
(t/is (some? component1'))
(t/is (some? component2'))
(t/is (some? root1'))
(t/is (some? root2'))
(t/is (= (thi/id :main1-root) (:id root1')))
(t/is (not= (thi/id :main1-root) (:id root2')))
(t/is (some? child1'))
(t/is (some? child2'))
(t/is (= (thi/id :main1-child) (:id child1')))
(t/is (not= (thi/id :main1-child) (:id child2')))))
(t/deftest test-duplicate-component-rewrites-component-file-to-destination
;; Regression test for Issue #8144. When a component is duplicated
;; into a different file via `:apply-changes-local-library? true`
;; and `:new-component-file` is provided, the returned main-instance
;; shape must carry `:component-file` equal to the destination file
;; id so the referential-integrity validator
;; (:component-main-external) is satisfied.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1
:main1-root
:main1-child))
component (thc/get-component file :component1)
new-component-file (uuid/next)
;; ==== Action
[new-shape _]
(cll/generate-duplicate-component (pcb/empty-changes)
file
(:id component)
(uuid/next)
{:apply-changes-local-library? true
:new-component-file new-component-file})]
;; ==== Check
(t/is (some? new-shape))
(t/is (ctk/main-instance? new-shape))
(t/is (= new-component-file (:component-file new-shape)))))
(t/deftest test-duplicate-component-keeps-component-file-without-dest
;; Baseline: when no `:new-component-file` is passed (same-file
;; duplication), the main-instance's `:component-file` is left
;; untouched, matching pre-existing behavior.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1
:main1-root
:main1-child))
component (thc/get-component file :component1)
original-source (:component-file
(ths/get-shape-by-id file (:main-instance-id component)))
;; ==== Action
[new-shape _]
(cll/generate-duplicate-component (pcb/empty-changes)
file
(:id component)
(uuid/next)
{:apply-changes-local-library? true})]
;; ==== Check
(t/is (some? new-shape))
(t/is (= original-source (:component-file new-shape)))))
(t/deftest test-delete-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root))
page (thf/current-page file)
root (ths/get-shape file :main1-root)
;; ==== Action
[_ changes]
(cls/generate-delete-shapes (pcb/empty-changes)
file
page
(:objects page)
#{(:id root)}
{:components-v2 true})
file' (thf/apply-changes file changes)
;; ==== Get
component1' (thc/get-component file' :component1 :include-deleted? true)
copy1-root' (ths/get-shape file' :copy1-root)
main1-root' (ths/get-shape file' :main1-root)
main1-child' (ths/get-shape file' :main1-child)
saved-objects (:objects component1')
saved-main1-root' (get saved-objects (thi/id :main1-root))
saved-main1-child' (get saved-objects (thi/id :main1-child))]
;; ==== Check
(t/is (true? (:deleted component1')))
(t/is (some? copy1-root'))
(t/is (nil? main1-root'))
(t/is (nil? main1-child'))
(t/is (some? saved-main1-root'))
(t/is (some? saved-main1-child'))))
(t/deftest test-restore-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root))
page (thf/current-page file)
root (ths/get-shape file :main1-root)
;; ==== Action
[_ changes]
(cls/generate-delete-shapes (pcb/empty-changes)
file
page
(:objects page)
#{(:id root)}
{:components-v2 true})
file-deleted (thf/apply-changes file changes)
page-deleted (thf/current-page file-deleted)
changes (cll/generate-restore-component (-> (pcb/empty-changes)
(pcb/with-library-data (:data file-deleted))
(pcb/with-objects (:objects page-deleted)))
(:data file-deleted)
(thi/id :component1)
(:id file-deleted)
page-deleted
(:objects page-deleted))
file' (thf/apply-changes file changes)
;; ==== Get
component1' (thc/get-component file' :component1 :include-deleted? false)
copy1-root' (ths/get-shape file' :copy1-root)
main1-root' (ths/get-shape file' :main1-root)
main1-child' (ths/get-shape file' :main1-child)
saved-objects' (:objects component1')]
;; ==== Check
(t/is (nil? (:deleted component1')))
(t/is (some? copy1-root'))
(t/is (some? main1-root'))
(t/is (some? main1-child'))
(t/is (ctk/main-instance? main1-root'))
(t/is (ctk/main-instance-of? (:id main1-root') (:id page) component1'))
(t/is (nil? saved-objects'))))
(t/deftest test-instantiate-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1
:main1-root
:main1-child))
page (thf/current-page file)
component (thc/get-component file :component1)
;; ==== Action
[new-shape changes]
(cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) ;; This may not be moved to generate
(pcb/with-objects (:objects page))) ;; because in some cases the objects
(:objects page) ;; not the same as those on the page
(:id file)
(:id component)
(gpt/point 1000 1000)
page
{(:id file) file})
file' (thf/apply-changes file changes)
;; ==== Get
component' (thc/get-component file' :component1)
main1-root' (ths/get-shape file' :main1-root)
main1-child' (ths/get-shape file' :main1-child)
copy1-root' (ths/get-shape-by-id file' (:id new-shape))
copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))]
;; ==== Check
(t/is (some? main1-root'))
(t/is (some? main1-child'))
(t/is (some? copy1-root'))
(t/is (some? copy1-child'))
(t/is (ctk/instance-root? copy1-root'))
(t/is (ctk/instance-of? copy1-root' (:id file') (:id component')))
(t/is (ctk/is-main-of? main1-root' copy1-root'))
(t/is (ctk/is-main-of? main1-child' copy1-child'))
(t/is (ctst/parent-of? copy1-root' copy1-child'))
(t/is (thf/validates-shapes-last? changes (:id page) [(:id new-shape)]))))
(t/deftest test-instantiate-component-inside-main-validates-root
;; A copy nested in a main can't be validated on its own: the root of the
;; main is validated instead
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1 :main1-root :main1-child)
(tho/add-simple-component :component2 :main2-root :main2-child))
page (thf/current-page file)
component (thc/get-component file :component1)
main2-id (thi/id :main2-root)
;; ==== Action
[new-shape changes]
(cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page)))
(:objects page)
(:id file)
(:id component)
(gpt/point 1000 1000)
page
{(:id file) file}
nil
main2-id
main2-id
{})
file' (thf/apply-changes file changes)
copy1' (ths/get-shape-by-id file' (:id new-shape))]
;; ==== Check
(t/is (= main2-id (:parent-id copy1')))
(t/is (not (ctk/instance-root? copy1')))
(t/is (thf/validates-shapes-last? changes (:id page) [main2-id]))))
(t/deftest test-instantiate-component-from-lib
(let [;; ==== Setup
library (-> (thf/sample-file :library1)
(tho/add-simple-component :component1
:main1-root
:main1-child))
file (thf/sample-file :file1)
page (thf/current-page file)
component (thc/get-component library :component1)
;; ==== Action
[new-shape changes]
(cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page)))
(:objects page)
(:id library)
(:id component)
(gpt/point 1000 1000)
page
{(:id file) file
(:id library) library})
file' (thf/apply-changes file changes)
;; ==== Get
component' (thc/get-component library :component1)
main1-root' (ths/get-shape library :main1-root)
main1-child' (ths/get-shape library :main1-child)
copy1-root' (ths/get-shape-by-id file' (:id new-shape))
copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))]
;; ==== Check
(t/is (some? main1-root'))
(t/is (some? main1-child'))
(t/is (some? copy1-root'))
(t/is (some? copy1-child'))
(t/is (ctk/instance-root? copy1-root'))
(t/is (ctk/instance-of? copy1-root' (:id library) (:id component')))
(t/is (ctk/is-main-of? main1-root' copy1-root'))
(t/is (ctk/is-main-of? main1-child' copy1-child'))
(t/is (ctst/parent-of? copy1-root' copy1-child'))))
(t/deftest test-instantiate-nested-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-nested-component :component1
:main1-root
:main1-child
:component2
:main2-root
:main2-nested-head))
page (thf/current-page file)
component (thc/get-component file :component1)
;; ==== Action
[new-shape changes]
(cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page)))
(:objects page)
(:id file)
(:id component)
(gpt/point 1000 1000)
page
{(:id file) file})
file' (thf/apply-changes file changes)
;; ==== Get
component' (thc/get-component file' :component1)
main1-root' (ths/get-shape file' :main1-root)
main1-child' (ths/get-shape file' :main1-child)
copy1-root' (ths/get-shape-by-id file' (:id new-shape))
copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))]
;; ==== Check
(t/is (some? main1-root'))
(t/is (some? main1-child'))
(t/is (some? copy1-root'))
(t/is (some? copy1-child'))
(t/is (ctk/instance-root? copy1-root'))
(t/is (ctk/instance-of? copy1-root' (:id file') (:id component')))
(t/is (ctk/is-main-of? main1-root' copy1-root'))
(t/is (ctk/is-main-of? main1-child' copy1-child'))
(t/is (ctst/parent-of? copy1-root' copy1-child'))))
(t/deftest test-instantiate-nested-component-from-lib
(let [;; ==== Setup
library (-> (thf/sample-file :file1)
(tho/add-nested-component :component1
:main1-root
:main1-child
:component2
:main2-root
:main2-nested-head))
file (thf/sample-file :file1)
page (thf/current-page file)
component (thc/get-component library :component1)
;; ==== Action
[new-shape changes]
(cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page))
(pcb/with-objects (:objects page)))
(:objects page)
(:id library)
(:id component)
(gpt/point 1000 1000)
page
{(:id file) file
(:id library) library})
file' (thf/apply-changes file changes)
;; ==== Get
component' (thc/get-component library :component1)
main1-root' (ths/get-shape library :main1-root)
main1-child' (ths/get-shape library :main1-child)
copy1-root' (ths/get-shape-by-id file' (:id new-shape))
copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))]
;; ==== Check
(t/is (some? main1-root'))
(t/is (some? main1-child'))
(t/is (some? copy1-root'))
(t/is (some? copy1-child'))
(t/is (ctk/instance-root? copy1-root'))
(t/is (ctk/instance-of? copy1-root' (:id library) (:id component')))
(t/is (ctk/is-main-of? main1-root' copy1-root'))
(t/is (ctk/is-main-of? main1-child' copy1-child'))
(t/is (ctst/parent-of? copy1-root' copy1-child'))))
(t/deftest test-detach-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root))
page (thf/current-page file)
copy1-root (ths/get-shape file :copy1-root)
;; ==== Action
changes (cll/generate-detach-component (pcb/empty-changes)
(:id copy1-root)
(:data file)
(:id page)
{(:id file) file})
file' (thf/apply-changes file changes)
;; ==== Get
copy1-root' (ths/get-shape file' :copy1-root)]
;; ==== Check
(t/is (some? copy1-root'))
(t/is (not (ctk/instance-head? copy1-root')))
(t/is (not (ctk/in-component-copy? copy1-root')))))
(defn- delete-shapes
[file labels]
(let [page (thf/current-page file)
[_ changes]
(cls/generate-delete-shapes (pcb/empty-changes)
file
page
(:objects page)
(into #{} (map thi/id) labels)
{:components-v2 true})]
(thf/apply-changes file changes)))
(defn- restore-changes
"Build the changes of restoring a deleted component as the workspace does,
without any library data or objects in the changes builder."
[file component-label]
(let [page (thf/current-page file)]
(cll/generate-restore-component (pcb/empty-changes)
(:data file)
(thi/id component-label)
(:id file)
page
(:objects page))))
(t/deftest test-restore-component-without-library-data
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root)
(delete-shapes [:main1-root]))
;; ==== Action
changes (restore-changes file :component1)
file' (thf/apply-changes file changes)
;; ==== Get
component1' (thc/get-component file' :component1)
main1-root' (ths/get-shape file' :main1-root)]
;; ==== Check
(t/is (some? component1'))
(t/is (some? main1-root'))))
(t/deftest test-restore-variant-after-deleting-its-container
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(delete-shapes [:v01]))
;; ==== Action
changes (restore-changes file :c01)
file' (thf/apply-changes file changes)
;; ==== Get
c01' (thc/get-component file' :c01)
m01' (ths/get-shape file' :m01)]
;; ==== Check
;; The container no longer exists, so the main is restored as a
;; component that is not a variant
(t/is (some? c01'))
(t/is (some? m01'))
(t/is (nil? (:variant-id c01')))
(t/is (nil? (:variant-id m01')))))
(defn- validates-shapes?
"Check that some :validate-shapes change validates exactly `shape-ids`."
[changes shape-ids]
(some #(and (= :validate-shapes (:type %))
(= (set shape-ids) (set (:shape-ids %))))
(:redo-changes changes)))
(t/deftest test-restore-variant-into-its-container-validates-container
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(delete-shapes [:m02]))
;; ==== Action
changes (restore-changes file :c02)
file' (thf/apply-changes file changes)]
;; ==== Check
(t/is (= (thi/id :v01) (:parent-id (ths/get-shape file' :m02))))
(t/is (validates-shapes? changes [(thi/id :v01)]))))
(t/deftest test-paste-variant-into-other-variant-validates-container
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)
(thv/add-variant :v02 :c03 :m03 :c04 :m04)
(ths/update-shape :v02 :name "Other")
(ths/update-shape :m03 :name "Other")
(ths/update-shape :m04 :name "Other")
(thc/update-component :c03 {:name "Other"})
(thc/update-component :c04 {:name "Other"}))
page (thf/current-page file)
shape-id (thi/id :m01)
v02-id (thi/id :v02)
;; ==== Action
;; Paste a copy of m01 into v02, as the workspace does
objects (update (:objects page) shape-id assoc :parent-id v02-id :frame-id v02-id)
changes (-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-library-data (:data file))
(pcb/with-objects (:objects page))
(cll/generate-duplicate-changes objects page #{shape-id} (gpt/point 0 0)
{(:id file) file} (:data file) (:id file)))
file' (thf/apply-changes file changes)
v02' (ths/get-shape file' :v02)]
;; ==== Check
(t/is (= 3 (count (:shapes v02'))))
(t/is (validates-shapes? changes [v02-id]))))
(defn- paste-changes
"Build the changes of pasting a copy of the shape into the parent, as the
workspace does."
[file shape-label parent-label]
(let [page (thf/current-page file)
shape-id (thi/id shape-label)
parent-id (thi/id parent-label)
objects (update (:objects page) shape-id assoc :parent-id parent-id :frame-id parent-id)]
(-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-library-data (:data file))
(pcb/with-objects (:objects page))
(cll/generate-duplicate-changes objects page #{shape-id} (gpt/point 0 0)
{(:id file) file} (:data file) (:id file)))))
(defn- pasted-id
[changes shape-label]
(->> (:redo-changes changes)
(filter #(and (= :add-obj (:type %))
(= (thi/id shape-label) (:old-id %))))
first
:obj
:id))
(t/deftest test-paste-main-into-main-validates-enclosing-root
;; Pasting a main into another main creates a nested copy, that can't be
;; validated on its own: the root of the enclosing main is validated
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component :component1 :main1-root :main1-child)
(tho/add-simple-component :component2 :main2-root :main2-child))
;; ==== Action
changes (paste-changes file :main2-root :main1-root)
file' (thf/apply-changes file changes)
copy' (ths/get-shape-by-id file' (pasted-id changes :main2-root))]
;; ==== Check
(t/is (= (thi/id :main1-root) (:parent-id copy')))
(t/is (not (ctk/instance-root? copy')))
(t/is (validates-shapes? changes [(thi/id :main1-root)]))))
(t/deftest test-paste-copy-at-root-validates-new-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main1-root
:main1-child
:copy1-root))
page (thf/current-page file)
;; ==== Action
changes (-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-library-data (:data file))
(pcb/with-objects (:objects page))
(cll/generate-duplicate-changes (:objects page) page #{(thi/id :copy1-root)}
(gpt/point 0 0)
{(:id file) file} (:data file) (:id file)))
new-id (pasted-id changes :copy1-root)
file' (thf/apply-changes file changes)]
;; ==== Check
(t/is (ctk/instance-root? (ths/get-shape-by-id file' new-id)))
(t/is (validates-shapes? changes [new-id]))))