;; 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-sync-test (:require [app.common.data :as d] [app.common.files.changes-builder :as pcb] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.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.container :as ctn] [app.common.types.shape-tree :as ctst] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) (t/deftest test-sync-when-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) main-child (ths/get-shape file :main-child) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main-child)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (: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 :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== 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') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy-root') nil)) (t/is (= (:touched copy-child') nil)))) (t/deftest test-sync-when-changing-attribute-from-library (let [;; ==== Setup library (-> (thf/sample-file :file1) (tho/add-simple-component :component1 :main-root :main-child :copy-root :main-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 library) main-child (ths/get-shape library :main-child) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main-child)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (:objects page) {}) updated-library (thf/apply-changes library changes1) changes2 (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id file) (thi/id :component1) (:id updated-library) {(:id updated-library) updated-library (:id file) file} (:id file)) file' (thf/apply-changes file changes2) ;; ==== 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') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy-root') nil)) (t/is (= (:touched copy-child') nil)))) (t/deftest test-sync-when-changing-attribute-preserve-touched (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) main-child (ths/get-shape file :main-child) copy-child (ths/get-shape file :copy-child) ;; ==== Action changes1 (-> (pcb/empty-changes nil (:id page)) (cls/generate-update-shapes #{(:id copy-child)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#aaaaaa"))) (:objects page) {}) (cls/generate-update-shapes #{(:id main-child)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (: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 :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== 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') "#aaaaaa")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy-root') nil)) (t/is (= (:touched copy-child') #{:fill-group})))) (t/deftest test-sync-when-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) ;; ==== Action changes1 (cls/generate-relocate (-> (pcb/empty-changes nil) (pcb/with-page-id (:id page)) (pcb/with-objects (:objects page))) (thi/id :main-root) ; parent-id 0 ; to-index #{(thi/id :free-shape)}) ; ids updated-file (thf/apply-changes file changes1) changes2 (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id updated-file) (thi/id :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== Get main-free-shape' (ths/get-shape file' :free-shape) copy-root' (ths/get-shape file' :copy-root) copy-new-child-id' (d/seek #(not= % (thi/id :copy-child)) (:shapes copy-root')) copy-new-child' (ths/get-shape-by-id file' copy-new-child-id')] ;; ==== Check (t/is (some? copy-root')) (t/is (some? copy-new-child')) (t/is (= (:touched copy-root') nil)) (t/is (= (:touched copy-new-child') nil)) (t/is (ctst/parent-of? copy-root' copy-new-child')) (t/is (ctk/is-main-of? main-free-shape' copy-new-child')))) (t/deftest test-sync-when-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) main-child (ths/get-shape file :main-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 changes1] (cls/generate-delete-shapes (pcb/empty-changes) file page (:objects page) #{(:id main-child)} {:components-v2 true}) updated-file (thf/apply-changes file changes1) changes2 (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id updated-file) (thi/id :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== 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 (nil? copy-child')) (t/is (= (:touched copy-root') nil)) (t/is (empty? (:shapes copy-root'))))) (t/deftest test-sync-when-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-child1 :copy-child2 :copy-child3]}) (ths/add-sample-shape :free-shape)) page (thf/current-page file) main-child1 (ths/get-shape file :main-child1) ;; ==== Action changes1 (cls/generate-relocate (-> (pcb/empty-changes nil) (pcb/with-page-id (:id page)) (pcb/with-objects (:objects page))) (thi/id :main-root) ; parent-id 2 ; to-index #{(:id main-child1)}) ; ids updated-file (thf/apply-changes file changes1) changes2 (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id updated-file) (thi/id :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== Get copy-root' (ths/get-shape file' :copy-root) copy-child1' (ths/get-shape file' :copy-child1) copy-child2' (ths/get-shape file' :copy-child2) copy-child3' (ths/get-shape file' :copy-child3)] ;; ==== Check (t/is (some? copy-root')) (t/is (some? copy-child1')) (t/is (= (:touched copy-root') nil)) (t/is (= (:touched copy-child1') nil)) (t/is (= (:touched copy-child2') nil)) (t/is (= (:touched copy-child3') nil)) (t/is (= (second (:shapes copy-root')) (:id copy-child1'))) (t/is (= (first (:shapes copy-root')) (:id copy-child2'))) (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) (t/deftest test-sync-when-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) main2-root (ths/get-shape file :main2-root) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main2-root)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (: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 :component2) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== 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') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)))) (t/deftest test-sync-when-changing-lower-near (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) main2-nested-head (ths/get-shape file :main2-nested-head) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main2-nested-head)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (: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 :component2) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) file' (thf/apply-changes updated-file changes2) ;; ==== 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') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)) (t/is (= (:touched copy2-child') nil)))) (t/deftest test-sync-when-changing-lower-remote (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) main1-root (ths/get-shape file :main1-root) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main1-root)} (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (: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 :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file)) synced-file (thf/apply-changes updated-file changes2) changes3 (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id synced-file) (thi/id :component2) (:id synced-file) {(:id synced-file) synced-file} (:id synced-file)) file' (thf/apply-changes synced-file changes3) ;; ==== 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') "#fabada")) (t/is (= (:fill-opacity fill') 1)) (t/is (= (:touched copy2-root') nil)) (t/is (= (:touched copy2-child') nil)))) (t/deftest test-no-sync-changes-when-only-position-changes ;; Regression: the library sync dialog was shown even when a library component ;; was only moved (x/y changed). Position changes are normalised by ;; reposition-shape during sync and never propagate to copies, so ;; generate-sync-file-changes must return empty :redo-changes in this case. (let [;; ==== Setup ;; Use integer width/height so that floating-point arithmetic of ;; move(+delta) followed by reposition(-delta) cancels out exactly. file (-> (thf/sample-file :file1) (tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root :main-root-params {:width 100 :height 100} :main-child-params {:width 50 :height 50})) page (thf/current-page file) main-root (ths/get-shape file :main-root) main-child (ths/get-shape file :main-child) ;; ==== Action ;; Move the entire main component (root + child) by a non-zero integer delta. ;; This is a position-only change: no fills, strokes or other ;; attributes are modified. delta (gpt/point 100 150) changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(:id main-root) (:id main-child)} (fn [shape] (gsh/move shape delta)) (:objects page) {}) updated-file (thf/apply-changes file changes1) ;; Run the full sync to check whether any real redo-changes are produced. ;; The fixed frontend code filters out libraries whose sync produces no ;; :redo-changes before showing the "library updated" notification. sync-changes (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id updated-file) (thi/id :component1) (:id updated-file) {(:id updated-file) updated-file} (:id updated-file))] ;; ==== Check ;; A position-only change in the main component must not propagate to copies ;; and therefore must produce no redo-changes. (t/is (empty? (:redo-changes sync-changes))))) (defn- sync-file-changes [file] (cll/generate-sync-file-changes (pcb/empty-changes) nil :components (:id file) (thi/id :component1) (:id file) {(:id file) file} (:id file))) (t/deftest test-sync-many-updated-shapes ;; The undo changes of a sync used to grow one lazy level per updated ;; shape (#12576). Overflowing the stack needs thousands of shapes, which ;; is too slow here; test-update-attr-changes-many-calls covers that. (let [;; ==== Setup n 500 child-labels (mapv #(keyword (str "main-child-" %)) (range n)) file (-> (thf/sample-file :file1) (tho/add-component-with-many-children-and-copy :component1 :main-root child-labels :copy-root :main-child-params-list (repeat n {:fills (ths/sample-fills-color :fill-color "#abcdef")}))) page (thf/current-page file) ;; ==== Action changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) (into #{} (map thi/id) child-labels) (fn [shape] (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) (:objects page) {}) updated-file (thf/apply-changes file changes1) changes2 (sync-file-changes updated-file) file' (thf/apply-changes updated-file changes2) file'' (thf/apply-undo-changes file' changes2) ;; ==== Get copy-fill (fn [file] (let [copy-root (ths/get-shape file :copy-root)] (into #{} (map #(-> (ths/get-shape-by-id file %) :fills first :fill-color)) (:shapes copy-root))))] ;; ==== Check (t/is (= #{"#fabada"} (copy-fill file'))) (t/is (= #{"#abcdef"} (copy-fill file''))))) (t/deftest test-update-attr-changes-many-calls ;; Regression: each call nested the undo list in one more lazy concat, so ;; walking it after many calls overflowed the stack (#12576). (let [;; ==== Setup file (-> (thf/sample-file :file1) (tho/add-rect :rect1)) page (thf/current-page file) rect (ths/get-shape file :rect1) roperations [{:type :set :attr :opacity :val 0.5 :ignore-touched true}] uoperations (list {:type :set :attr :opacity :val 1 :ignore-touched true}) ;; ==== Action changes (reduce (fn [changes _] (#'cll/add-update-attr-changes changes rect page roperations uoperations)) (pcb/empty-changes) (range 50000)) undo (:undo-changes changes)] ;; ==== Check (t/is (= 100000 (count undo))) ;; Undo restores the shape first and then recalculates its parents. (t/is (= [:mod-obj :reg-objects] (map :type (take 2 undo)))))) (defn- geometry [shape] ;; Compare as doubles: a recalculated selrect has doubles where the ;; original one may have longs. {:selrect (update-vals (select-keys (:selrect shape) [:x :y :width :height]) double) :points (mapv #(update-vals (select-keys % [:x :y]) double) (:points shape))}) (defn- add-group-component-with-copy [file] ;; Generated shape tree: ;; :main-root [:name Frame1] # [Component :component1] ;; :main-group [:name Group1] ;; :main-child1 [:name Rect1] ;; :main-child2 [:name Rect2] ;; ;; :copy-root [:name Frame1] #--> [Component :component1] :main-root ;; :copy-group [:name Group1] ---> :main-group ;; :copy-child1 [:name Rect1] ---> :main-child1 ;; :copy-child2 [:name Rect2] ---> :main-child2 (-> file (tho/add-frame :main-root :x 0 :y 0 :width 100 :height 100) (tho/add-group :main-group :parent-label :main-root :x 10 :y 10 :width 30 :height 10) (ths/add-sample-shape :main-child1 :parent-label :main-group :name "Rect1" :x 10 :y 10 :width 10 :height 10 :fills (ths/sample-fills-color :fill-color "#abcdef")) (ths/add-sample-shape :main-child2 :parent-label :main-group :name "Rect2" :x 30 :y 10 :width 10 :height 10 :fills (ths/sample-fills-color :fill-color "#abcdef")) (thc/make-component :component1 :main-root) (thc/instantiate-component :component1 :copy-root :children-labels [:copy-group :copy-child1 :copy-child2]))) (defn- touch-copy-group-geometry ;; With its geometry touched, the sync does not copy the group's own ;; geometry from the main, so only the parents recalculation decides it. [file] (reduce #(ths/update-shape %1 %2 :touched #{:geometry-group}) file [:copy-group :copy-child1 :copy-child2])) (defn- update-fill [file label color] (let [page (thf/current-page file) changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) #{(thi/id label)} #(assoc % :fills (ths/sample-fills-color :fill-color color)) (:objects page) {})] (thf/apply-changes file changes))) (t/deftest test-sync-undo-restores-group-geometry (let [;; ==== Setup file (add-group-component-with-copy (thf/sample-file :file1)) page (thf/current-page file) copy-group (ths/get-shape file :copy-group) ;; ==== Action changes1 (-> (pcb/empty-changes nil (:id page)) (pcb/with-objects (:objects page)) (cls/generate-update-shapes #{(thi/id :main-child2)} #(gsh/absolute-move % (gpt/point 60 60)) (:objects page) {}) (pcb/resize-parents [(thi/id :main-child2)])) updated-file (thf/apply-changes file changes1) changes2 (sync-file-changes updated-file) file' (thf/apply-changes updated-file changes2) file'' (thf/apply-undo-changes file' changes2) ;; ==== Get copy-group' (ths/get-shape file' :copy-group) copy-group'' (ths/get-shape file'' :copy-group)] ;; ==== Check (t/is (not= (geometry copy-group) (geometry copy-group'))) (t/is (= (geometry copy-group) (geometry copy-group''))))) (t/deftest test-sync-undo-group-after-add-and-update (let [;; ==== Setup file (-> (thf/sample-file :file1) (add-group-component-with-copy) (touch-copy-group-geometry) (ths/add-sample-shape :free-shape :x 60 :y 60 :width 10 :height 10)) page (thf/current-page file) copy-group (ths/get-shape file :copy-group) ;; ==== Action ;; The new shape goes first in the group, so the sync handles it before ;; the updated sibling. changes1 (cls/generate-relocate (-> (pcb/empty-changes nil) (pcb/with-page-id (:id page)) (pcb/with-objects (:objects page))) (thi/id :main-group) 0 #{(thi/id :free-shape)}) updated-file (-> (thf/apply-changes file changes1) (update-fill :main-child1 "#fabada")) changes2 (sync-file-changes updated-file) file' (thf/apply-changes updated-file changes2) file'' (thf/apply-undo-changes file' changes2) ;; ==== Get copy-group' (ths/get-shape file' :copy-group) copy-group'' (ths/get-shape file'' :copy-group)] ;; ==== Check (t/is (= 3 (count (:shapes copy-group')))) (t/is (not= (geometry copy-group) (geometry copy-group'))) (t/is (= (:shapes copy-group) (:shapes copy-group''))) (t/is (= (geometry copy-group) (geometry copy-group''))))) (t/deftest test-sync-undo-group-after-remove-and-update (let [;; ==== Setup file (-> (thf/sample-file :file1) (add-group-component-with-copy) (touch-copy-group-geometry)) page (thf/current-page file) copy-group (ths/get-shape file :copy-group) ;; ==== Action ;; The removed shape is the first one in the group, so the sync ;; handles it before the updated sibling. Remove it only from the ;; main, as a library update does, so that the sync removes it from ;; the copy. changes1 (-> (pcb/empty-changes nil (:id page)) (pcb/with-objects (:objects page)) (pcb/remove-objects #{(thi/id :main-child1)})) updated-file (-> (thf/apply-changes file changes1) (update-fill :main-child2 "#fabada")) changes2 (sync-file-changes updated-file) file' (thf/apply-changes updated-file changes2) file'' (thf/apply-undo-changes file' changes2) ;; ==== Get copy-group' (ths/get-shape file' :copy-group) copy-group'' (ths/get-shape file'' :copy-group)] ;; ==== Check (t/is (= 1 (count (:shapes copy-group')))) (t/is (not= (geometry copy-group) (geometry copy-group'))) (t/is (= (:shapes copy-group) (:shapes copy-group''))) (t/is (= (geometry copy-group) (geometry copy-group''))))) (t/deftest test-sync-inverse-undo-group-after-add-and-update (let [;; ==== Setup ;; Penpot does not let users move shapes into a copy, but files can ;; still hold copies with shapes the main lacks, so build the data ;; directly. The new shape goes first in the copy group, so the ;; inverse sync handles it before the updated sibling. file (-> (thf/sample-file :file1) (add-group-component-with-copy) (ths/add-sample-shape :free-shape :parent-label :copy-group :x 160 :y 160 :width 10 :height 10) (ths/update-shape :copy-group :shapes [(thi/id :free-shape) (thi/id :copy-child1) (thi/id :copy-child2)])) main-group (ths/get-shape file :main-group) ;; ==== Action updated-file (update-fill file :copy-child1 "#fabada") page (thf/current-page updated-file) container (ctn/make-container page :page) changes (-> (pcb/empty-changes) (pcb/with-container container) (cll/generate-sync-shape-inverse (:data updated-file) {(:id updated-file) updated-file} container (thi/id :copy-root))) file' (thf/apply-changes updated-file changes) file'' (thf/apply-undo-changes file' changes) ;; ==== Get main-group' (ths/get-shape file' :main-group) main-group'' (ths/get-shape file'' :main-group)] ;; ==== Check (t/is (= 3 (count (:shapes main-group')))) (t/is (not= (geometry main-group) (geometry main-group'))) (t/is (= (:shapes main-group) (:shapes main-group''))) (t/is (= (geometry main-group) (geometry main-group'')))))