penpot/common/test/common_tests/logic/comp_creation_test.cljc
Andrés Moya a5f75c7394
🐛 Fix repair bad variants (#11983)
* 🐛 Ensure normalized names in components and variants

We have a rule of that shape names that include / separator should have
it normalized, with one space before and one after. But this rule is
being enforced only in the UI and the plugins API. There may be other
ways of creating shapes that does not follow it.

For normal shapes this is not really a problem. But for main instances
it may cause that the name of the shape does not match the joined
path+name of the component, and this may generate some problems.

We have decided to add a migration to fix this in all main instances and
variant containers and enforce it in the future with integrity
validations (normal shapes may be left as is).

* 🐛 Fix bad variant-id in the component and the main instance

* 🔧 Fix some issues detected in PR review

* 🐛 Add nil guard
2026-10-16 10:59:57 +02:00

665 lines
26 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.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)
(: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/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')))))