penpot/common/test/common_tests/logic/comp_reset_test.cljc
Andrés Moya a3ea9fbecb
🔧 Add more validations for components, to avoid some crashes (#7820)
* 🔧 Validate only after propagation in tests

* 💄 Enhance some component sync traces

* 🔧 Add fake uuid generator for debugging

* 🐛 Remove old feature of advancing references when reset changes

Since long time ago, we only allow to reset changes in the top copy
shape. In this case the near and the remote shapes are the same, so
the advance-ref has no effect.

* 🐛 Fix some bugs and add validations, repair and migrations

Also added several utilities to debug and to create scripts that
processes files

* 🐛 Fix misplaced parenthesis passing propagate-fn to wrong function

The :propagate-fn keyword argument was incorrectly placed inside the
ths/get-shape call instead of being passed to tho/reset-overrides.
This caused reset-overrides to never propagate component changes,
making the test not validate what it intended.

* 🐛 Accept and forward :include-deleted? in find-near-match

Callers were passing :include-deleted? true but the parameter was not
in the destructuring, so it was silently ignored and the function
always hardcoded true. This made the API misleading and would cause
incorrect behavior if called with :include-deleted? false.

* 💄 Use set/union alias instead of fully-qualified clojure.set/union

The namespace already requires [clojure.set :as set], so use the alias
for consistency.

* 🐛 Add tests for reset-overrides with and without propagate-fn

Add two focused tests to comp_reset_test to cover the propagate-fn
path in reset-overrides:

- test-reset-with-propagation-updates-copies: verifies that resetting
  an override on a nested copy inside a main and supplying propagate-fn
  causes the canonical color to appear in all downstream copies.

- test-reset-without-propagation-does-not-update-copies: regression
  guard for the misplaced-parenthesis bug; confirms that omitting
  propagate-fn leaves copies with the overridden value because the
  component sync never runs.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 08:42:42 +02:00

421 lines
19 KiB
Clojure
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;; 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 INC
(ns common-tests.logic.comp-reset-test
(:require
[app.common.files.changes-builder :as pcb]
[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]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-reset-after-changing-attribute
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main-root
:main-child
:copy-root
:main-child-params {:fills (ths/sample-fills-color
:fill-color "#abcdef")}
:copy-root-params {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-root (ths/get-shape file :copy-root)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
(:objects page)
{})
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy-root' (ths/get-shape file' :copy-root)
copy-child' (ths/get-shape file' :copy-child)
fills' (:fills copy-child')
fill' (first fills')]
;; ==== Check
(t/is (some? copy-root'))
(t/is (some? copy-child'))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#abcdef"))
(t/is (= (:fill-opacity fill') 1))
(t/is (= (:touched copy-root') nil))
(t/is (= (:touched copy-child') nil))))
(t/deftest test-reset-from-library
(let [;; ==== Setup
library (-> (thf/sample-file :library :is-shared true)
(tho/add-simple-component :component1 :main-root :main-child
:child-params {:fills (ths/sample-fills-color
:fill-color "#abcdef")}))
file (-> (thf/sample-file :file)
(thc/instantiate-component :component1 :copy-root
:library library
:children-labels [:copy-child]))
page (thf/current-page file)
copy-root (ths/get-shape file :copy-root)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy-child)}
(fn [shape]
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
(:objects page)
{})
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf
(:id library) library}
page-mdf
(:id copy-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy-root' (ths/get-shape file' :copy-root)
copy-child' (ths/get-shape file' :copy-child)
fills' (:fills copy-child')
fill' (first fills')]
;; ==== Check
(t/is (some? copy-root'))
(t/is (some? copy-child'))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#abcdef"))
(t/is (= (:fill-opacity fill') 1))
(t/is (= (:touched copy-root') nil))
(t/is (= (:touched copy-child') nil))))
(t/deftest test-reset-after-adding-shape
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main-root
:main-child
:copy-root
:copy-root-params {:children-labels [:copy-child]})
(ths/add-sample-shape :free-shape))
page (thf/current-page file)
copy-root (ths/get-shape file :copy-root)
;; ==== Action
;; IMPORTANT: as modifying copies structure is now forbidden, this action
;; will not have any effect, and so the parent shape won't also be touched.
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-objects (:objects page)))
(thi/id :copy-root) ; parent-id
0 ; to-index
#{(thi/id :free-shape)}) ; ids
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy-root' (ths/get-shape file' :copy-root)
copy-child' (ths/get-shape file' :copy-child)]
;; ==== Check
(t/is (some? copy-root'))
(t/is (some? copy-child'))
(t/is (= (:touched copy-root') nil))
(t/is (= (:touched copy-child') nil))))
(t/deftest test-reset-after-deleting-shape
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-simple-component-with-copy :component1
:main-root
:main-child
:copy-root
:copy-root-params {:children-labels [:copy-child]}))
page (thf/current-page file)
copy-root (ths/get-shape file :copy-root)
copy-child (ths/get-shape file :copy-child)
;; ==== Action
;; IMPORTANT: as modifying copies structure is now forbidden, this action will not
;; delete the child shape, but hide it (thus setting the visibility group).
[_all-parents changes]
(cls/generate-delete-shapes (pcb/empty-changes)
file
page
(:objects page)
#{(:id copy-child)}
{:components-v2 true})
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy-root' (ths/get-shape file' :copy-root)
copy-child' (ths/get-shape file' :copy-child)]
;; ==== Check
(t/is (some? copy-root'))
(t/is (some? copy-child'))
(t/is (= (:touched copy-root') nil))
(t/is (= (:touched copy-child') nil))))
(t/deftest test-reset-after-moving-shape
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-component-with-many-children-and-copy :component1
:main-root
[:main-child1 :main-child2 :main-child3]
:copy-root
:copy-root-params {:children-labels [:copy-child]})
(ths/add-sample-shape :free-shape))
page (thf/current-page file)
copy-root (ths/get-shape file :copy-root)
copy-child1 (ths/get-shape file :copy-child)
;; ==== Action
;; IMPORTANT: as modifying copies structure is now forbidden, this action
;; will not have any effect, and so the parent shape won't also be touched.
changes (cls/generate-relocate (-> (pcb/empty-changes nil)
(pcb/with-page-id (:id page))
(pcb/with-objects (:objects page)))
(thi/id :copy-root) ; parent-id
2 ; to-index
#{(:id copy-child1)}) ; ids
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy-root' (ths/get-shape file' :copy-root)
copy-child' (ths/get-shape file' :copy-child)]
;; ==== Check
(t/is (some? copy-root'))
(t/is (some? copy-child'))
(t/is (= (:touched copy-root') nil))
(t/is (= (:touched copy-child') nil))))
(t/deftest test-reset-after-changing-upper
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-nested-component-with-copy :component1
:main1-root
:main1-child
:component2
:main2-root
:main2-nested-head
:copy2-root
:main2-root-params {:fills (ths/sample-fills-color
:fill-color "#abcdef")}))
page (thf/current-page file)
copy2-root (ths/get-shape file :copy2-root)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy2-root)}
(fn [shape]
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
(:objects page)
{})
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy2-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy2-root' (ths/get-shape file' :copy2-root)
fills' (:fills copy2-root')
fill' (first fills')]
;; ==== Check
(t/is (some? copy2-root'))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#abcdef"))
(t/is (= (:fill-opacity fill') 1))
(t/is (= (:touched copy2-root') nil))))
(t/deftest test-reset-after-changing-lower
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(tho/add-nested-component-with-copy :component1
:main1-root
:main1-child
:component2
:main2-root
:main2-nested-head
:copy2-root
:copy2-root-params {:children-labels [:copy2-child]}))
page (thf/current-page file)
copy2-root (ths/get-shape file :copy2-root)
copy2-child (ths/get-shape file :copy2-child)
;; ==== Action
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy2-child)}
(fn [shape]
(assoc shape :fills (ths/sample-fills-color :fill-color "#fabada")))
(:objects page)
{})
file-mdf (thf/apply-changes file changes)
page-mdf (thf/current-page file-mdf)
changes (cll/generate-reset-component (pcb/empty-changes)
file-mdf
{(:id file-mdf) file-mdf}
page-mdf
(:id copy2-root))
file' (thf/apply-changes file changes)
;; ==== Get
copy2-root' (ths/get-shape file' :copy2-root)
copy2-child' (ths/get-shape file' :copy2-child)
fills' (:fills copy2-child')
fill' (first fills')]
;; ==== Check
(t/is (some? copy2-root'))
(t/is (some? copy2-child'))
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#FFFFFF"))
(t/is (= (:fill-opacity fill') 1))
(t/is (= (:touched copy2-root') nil))
(t/is (= (:touched copy2-child') nil))))
(t/deftest test-reset-with-propagation-updates-copies
;; When a nested copy inside a main component has an override and we
;; reset it passing a propagate-fn, the reset must be propagated to
;; all copies of that component so they reflect the canonical color.
(let [;; ==== Setup
file
(-> (thf/sample-file :file1)
;; component1: main1-root / main1-child (fill "#aabbcc")
;; component2: main2-root contains nested-head (instance of component1)
;; copy2-root: copy of component2
(tho/add-nested-component-with-copy
:component1 :main1-root :main1-child
:component2 :main2-root :nested-head
:copy2-root
:main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")}
:copy2-root-params {:children-labels [:copy2-nested-head]}))
propagate-fn (fn [f]
(-> f
(tho/propagate-component-changes :component1)
(tho/propagate-component-changes :component2)))
;; ==== Action override the nested-head color, then reset it with propagation
file'
(-> file
(tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn)
(tho/reset-overrides (ths/get-shape file :nested-head) :propagate-fn propagate-fn))
;; ==== Get
copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)]
;; ==== Check
;; After reset + propagation the copy should mirror the canonical color
(t/is (= copy2-bottom-color "#aabbcc"))))
(t/deftest test-reset-without-propagation-does-not-update-copies
;; This is the regression test for the misplaced-parenthesis bug: when
;; propagate-fn is NOT passed to reset-overrides the copies of the component
;; must still hold the overridden value because the component sync never ran.
(let [;; ==== Setup
file
(-> (thf/sample-file :file1)
(tho/add-nested-component-with-copy
:component1 :main1-root :main1-child
:component2 :main2-root :nested-head
:copy2-root
:main1-child-params {:fills (ths/sample-fills-color :fill-color "#aabbcc")}
:copy2-root-params {:children-labels [:copy2-nested-head]}))
propagate-fn (fn [f]
(-> f
(tho/propagate-component-changes :component1)
(tho/propagate-component-changes :component2)))
;; ==== Action override the nested-head color, then reset WITHOUT propagation
file'
(-> file
(tho/update-bottom-color :nested-head "#fabada" :propagate-fn propagate-fn)
;; Reset without propagate-fn: the component definition is updated but
;; the change is never pushed to the copy.
(tho/reset-overrides (ths/get-shape file :nested-head)))
;; ==== Get
copy2-bottom-color (tho/bottom-fill-color file' :copy2-root)]
;; ==== Check
;; Without propagation the copy still reflects the overridden color
(t/is (= copy2-bottom-color "#fabada"))))