From d2212414511090c14620c85c857342318f78bf9c Mon Sep 17 00:00:00 2001
From: Pablo Alba <pablo.alba@kaleidos.net>
Date: Mon, 13 May 2024 18:02:21 +0200
Subject: [PATCH] :white_check_mark: Components refactor: Add tests for remove
 swap slot on copy-paste

---
 .../frontend_tests/basic_shapes_test.cljs     |   2 -
 .../test/frontend_tests/helpers/pages.cljs    |  85 ++
 .../test/frontend_tests/helpers/state.cljs    |   5 +
 .../logic/comp_remove_swap_slots_test.cljs    | 815 ++++++++++++++++++
 4 files changed, 905 insertions(+), 2 deletions(-)
 create mode 100644 frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs

diff --git a/frontend/test/frontend_tests/basic_shapes_test.cljs b/frontend/test/frontend_tests/basic_shapes_test.cljs
index 7f3188620..9204ab29a 100644
--- a/frontend/test/frontend_tests/basic_shapes_test.cljs
+++ b/frontend/test/frontend_tests/basic_shapes_test.cljs
@@ -40,8 +40,6 @@
                fills'      (:fills shape1')
                fill'       (first fills')]
 
-           (cthf/dump-shape shape1')
-
           ;; ==== Check
            (t/is (some? shape1'))
            (t/is (= (count fills') 1))
diff --git a/frontend/test/frontend_tests/helpers/pages.cljs b/frontend/test/frontend_tests/helpers/pages.cljs
index 939d06a07..74ad1c9f9 100644
--- a/frontend/test/frontend_tests/helpers/pages.cljs
+++ b/frontend/test/frontend_tests/helpers/pages.cljs
@@ -6,13 +6,19 @@
 
 (ns frontend-tests.helpers.pages
   (:require
+   [app.common.data :as d]
    [app.common.files.changes :as cp]
    [app.common.files.changes-builder :as pcb]
    [app.common.files.helpers :as cfh]
    [app.common.files.shapes-helpers :as cfsh]
    [app.common.geom.point :as gpt]
+   [app.common.geom.shapes :as gsh]
    [app.common.logic.libraries :as cll]
+   [app.common.types.component :as ctk]
+   [app.common.types.container :as ctn]
+   [app.common.types.file :as ctf]
    [app.common.types.shape :as cts]
+   [app.common.types.shape-tree :as ctst]
    [app.common.uuid :as uuid]
    [app.main.data.workspace.groups :as dwg]
    [app.main.data.workspace.layout :as layout]
@@ -186,3 +192,82 @@
                                          :components (:components data)}})
         (update :workspace-data
                 assoc :components {} :pages [] :pages-index {}))))
+
+
+(defn simulate-copy-shape
+  [selected objects libraries page file features version]
+  (letfn [(sort-selected [data]
+            (let [;; Narrow the objects map so it contains only relevant data for
+                  ;; selected and its parents
+                  objects  (cfh/selected-subtree objects selected)
+                  selected (->> (ctst/sort-z-index objects selected)
+                                (reverse)
+                                (into (d/ordered-set)))]
+
+              (assoc data :selected selected)))
+
+          ;; Prepare the shape object.
+          (prepare-object [objects parent-frame-id obj]
+            (maybe-translate obj objects parent-frame-id))
+
+          ;; Collects all the items together and split images into a
+          ;; separated data structure for a more easy paste process.
+          (collect-data [result {:keys [id ::images] :as item}]
+            (cond-> result
+              :always
+              (update :objects assoc id (dissoc item ::images))
+
+              (some? images)
+              (update :images into images)))
+
+          (maybe-translate [shape objects parent-frame-id]
+            (if (= parent-frame-id uuid/zero)
+              shape
+              (let [frame (get objects parent-frame-id)]
+                (gsh/translate-to-frame shape frame))))
+
+          ;; When copying an instance that is nested inside another one, we need to
+          ;; advance the shape refs to one or more levels of remote mains.
+          (advance-copies [data]
+            (let [heads     (mapcat #(ctn/get-child-heads (:objects data) %) selected)]
+              (update data :objects
+                      #(reduce (partial advance-copy file libraries page)
+                               %
+                               heads))))
+
+          (advance-copy [file libraries page objects shape]
+            (if (and (ctk/instance-head? shape) (not (ctk/main-instance? shape)))
+              (let [level-delta (ctn/get-nesting-level-delta (:objects page) shape uuid/zero)]
+                (if (pos? level-delta)
+                  (reduce (partial advance-shape file libraries page level-delta)
+                          objects
+                          (cfh/get-children-with-self objects (:id shape)))
+                  objects))
+              objects))
+
+          (advance-shape [file libraries page level-delta objects shape]
+            (let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})]
+              (cond-> objects
+                (and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape)))
+                (assoc-in [(:id shape) :shape-ref] new-shape-ref))))]
+
+    (let [file-id  (:id file)
+          frame-id (cfh/common-parent-frame objects selected)
+
+          initial  {:type :copied-shapes
+                    :features features
+                    :version version
+                    :file-id file-id
+                    :selected selected
+                    :objects {}
+                    :images #{}
+                    :in-viewport false}
+
+          shapes   (->> (cfh/selected-with-children objects selected)
+                        (keep (d/getf objects)))]
+
+      (->> shapes
+           (map (partial prepare-object objects frame-id))
+           (reduce collect-data initial)
+           sort-selected
+           advance-copies))))
diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs
index 3b5fd501f..6c3c7cb2c 100644
--- a/frontend/test/frontend_tests/helpers/state.cljs
+++ b/frontend/test/frontend_tests/helpers/state.cljs
@@ -54,3 +54,8 @@
     (doall (for [event events]
              (ptk/emit! store event)))
     (ptk/emit! store :the/end)))
+
+(defn get-file-from-store
+  [store]
+  (-> (:workspace-file store)
+      (assoc :data (:workspace-data store))))
diff --git a/frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs b/frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs
new file mode 100644
index 000000000..87ec95532
--- /dev/null
+++ b/frontend/test/frontend_tests/logic/comp_remove_swap_slots_test.cljs
@@ -0,0 +1,815 @@
+;; 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 frontend-tests.logic.comp-remove-swap-slots-test
+  (:require
+   [app.common.test-helpers.components :as cthc]
+   [app.common.test-helpers.compositions :as ctho]
+   [app.common.test-helpers.files :as cthf]
+   [app.common.test-helpers.ids-map :as cthi]
+   [app.common.test-helpers.shapes :as cths]
+   [app.common.types.component :as ctk]
+   [app.common.uuid :as uuid]
+   [app.main.data.workspace :as dw]
+   [app.main.data.workspace.selection :as dws]
+   [cljs.test :as t :include-macros true]
+   [cuerdas.core :as str]
+   [frontend-tests.helpers.pages :as thp]
+   [frontend-tests.helpers.state :as ths]))
+
+;; Related .penpot file: common/test/cases/remove-swap-slots.penpot
+(defn- setup-file
+  []
+  ;; {:frame-red} [:name Frame1]                           # [Component :red]
+  ;; {:frame-blue} [:name Frame1]                          # [Component :blue]
+  ;; {:frame-green} [:name Frame1]                         # [Component :green]
+  ;;     :red-copy-green [:name Frame1]                    @--> :frame-red
+  ;; {:frame-b1} [:name Frame1]                            # [Component :b1]
+  ;;     :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue
+  ;;     :frame-yellow [:name Frame1]
+  ;;     :green-copy [:name Frame1]                        @--> :frame-green
+  ;;         :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue
+  ;; {:frame-b2} [:name Frame1]                            # [Component :b2]
+  (-> (cthf/sample-file :file1)
+      (ctho/add-frame :frame-red)
+      (cthc/make-component :red :frame-red)
+      (ctho/add-frame :frame-blue :name "frame-blue")
+      (cthc/make-component :blue :frame-blue)
+      (ctho/add-frame :frame-green :name "frame-green")
+      (cthc/make-component :green :frame-green)
+      (cthc/instantiate-component :red :red-copy-green :parent-label :frame-green)
+      (ctho/add-frame :frame-b1)
+      (cthc/make-component :b1 :frame-b1)
+      (ctho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow")
+      (cthc/instantiate-component :red :red-copy :parent-label :frame-b1)
+      (cthc/component-swap :red-copy :blue :blue1)
+      (cthc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy])
+      (cthc/component-swap :red-copy-in-green-copy :blue :blue-copy-in-green-copy)
+      (ctho/add-frame :frame-b2)
+      (cthc/make-component :b2 :frame-b2)))
+
+
+(defn- setup-file-blue1-in-yellow
+  []
+  ;; {:frame-red} [:name Frame1]                           # [Component :red]
+  ;; {:frame-blue} [:name Frame1]                          # [Component :blue]
+  ;; {:frame-green} [:name Frame1]                         # [Component :green]
+  ;;     :red-copy-green [:name Frame1]                    @--> :frame-red
+  ;; {:frame-b1} [:name Frame1]                            # [Component :b1]
+  ;;     :frame-yellow [:name Frame1]
+  ;;         :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue
+  ;;     :green-copy [:name Frame1]                        @--> :frame-green
+  ;;         :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue
+  ;; {:frame-b2} [:name Frame1]                            # [Component :b2]
+  (-> (cthf/sample-file :file1)
+      (ctho/add-frame :frame-red)
+      (cthc/make-component :red :frame-red)
+      (ctho/add-frame :frame-blue :name "frame-blue")
+      (cthc/make-component :blue :frame-blue)
+      (ctho/add-frame :frame-green)
+      (cthc/make-component :green :frame-green)
+      (cthc/instantiate-component :red :red-copy-green :parent-label :frame-green)
+      (ctho/add-frame :frame-b1)
+      (cthc/make-component :b1 :frame-b1)
+      (ctho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow")
+      (cthc/instantiate-component :red :red-copy :parent-label :frame-yellow)
+      (cthc/component-swap :red-copy :blue :blue1)
+      (cthc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy])
+      (cthc/component-swap :red-copy-in-green-copy :blue :blue-copy-in-green-copy)
+      (ctho/add-frame :frame-b2)
+      (cthc/make-component :b2 :frame-b2)))
+
+(defn- find-copied-shape
+  [original-shape page parent-id]
+  ;; copied shape has the same name, is in the specified parent, and doesn't have a label
+  (->> (vals (:objects page))
+       (filter #(and (= (:name %) (:name original-shape))
+                     (= (:parent-id %) parent-id)
+                     (str/starts-with? (cthi/label (:id %)) "<no-label")))
+       first))
+
+(t/deftest test-remove-swap-slot-copy-paste-blue1-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               blue1'        (cths/get-shape file' :blue1)
+               copied-blue1' (find-copied-shape blue1' page' uuid/zero)]
+
+          ;; ==== Check
+
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+(t/deftest test-remove-swap-slot-copy-paste-blue1-to-b1
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b1       (cths/get-shape file :frame-b1)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b1))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               b1'           (cths/get-shape file' :frame-b1)
+               blue1'        (cths/get-shape file' :blue1)
+               copied-blue1' (find-copied-shape blue1' page' (:id b1'))]
+
+          ;; ==== Check
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-copy-paste-blue1-to-yellow
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id yellow))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               yellow'       (cths/get-shape file' :frame-yellow)
+               blue1'        (cths/get-shape file' :blue1)
+               copied-blue1' (find-copied-shape blue1' page' (:id yellow'))]
+
+          ;; ==== Check
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+(t/deftest test-remove-swap-slot-copy-paste-blue1-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b2       (cths/get-shape file :frame-b2)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               b2'           (cths/get-shape file' :frame-b2)
+               blue1'        (cths/get-shape file' :blue1)
+               copied-blue1' (find-copied-shape blue1' page' (:id b2'))]
+
+          ;; ==== Check
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+(t/deftest test-remove-swap-slot-cut-paste-blue1-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id blue1))
+           (dw/delete-selected)
+           (dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               copied-blue1' (find-copied-shape blue1 page' uuid/zero)]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+
+(t/deftest test-remove-swap-slot-cut-paste-blue1-to-yellow
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id blue1))
+           (dw/delete-selected)
+           (dws/select-shape (:id yellow))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               yellow'       (cths/get-shape file' :frame-yellow)
+               copied-blue1' (find-copied-shape blue1 page' (:id yellow'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+(t/deftest test-remove-swap-slot-cut-paste-blue1-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b2       (cths/get-shape file :frame-b2)
+          blue1    (cths/get-shape file :blue1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id blue1))
+           (dw/delete-selected)
+           (dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'         (ths/get-file-from-store new-state)
+               page'         (cthf/current-page file')
+               b2'           (cths/get-shape file' :frame-b2)
+               copied-blue1' (find-copied-shape blue1 page' (:id b2'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+
+(t/deftest test-remove-swap-slot-copy-paste-yellow-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               copied-yellow' (find-copied-shape yellow page' uuid/zero)
+               blue1'         (cths/get-shape file' :blue1)
+               copied-blue1'  (find-copied-shape blue1' page' (:id copied-yellow'))]
+
+          ;; ==== Check
+
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-copy-paste-yellow-to-b1
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          b1       (cths/get-shape file :frame-b1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b1))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b1'            (cths/get-shape file' :frame-b1)
+               copied-yellow' (find-copied-shape yellow page' (:id b1'))
+               blue1'         (cths/get-shape file' :blue1)
+               copied-blue1'  (find-copied-shape blue1' page' (:id copied-yellow'))]
+
+          ;; ==== Check
+
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-copy-paste-yellow-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          b2       (cths/get-shape file :frame-b2)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b2'            (cths/get-shape file' :frame-b2)
+               copied-yellow' (find-copied-shape yellow page' (:id b2'))
+               blue1'         (cths/get-shape file' :blue1)
+               copied-blue1'  (find-copied-shape blue1' page' (:id copied-yellow'))]
+
+          ;; ==== Check
+
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue1')))
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-cut-paste-yellow-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+          blue1    (cths/get-shape file :blue1)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id yellow))
+           (dw/delete-selected)
+           (dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               copied-yellow' (find-copied-shape yellow page' uuid/zero)
+               copied-blue1'  (find-copied-shape blue1 page' (:id copied-yellow'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-cut-paste-yellow-to-b1
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+          blue1    (cths/get-shape file :blue1)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          b1       (cths/get-shape file :frame-b1)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id yellow))
+           (dw/delete-selected)
+           (dws/select-shape (:id b1))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b1'            (cths/get-shape file' :frame-b1)
+               copied-yellow' (find-copied-shape yellow page' (:id b1'))
+               copied-blue1'  (find-copied-shape blue1 page' (:id copied-yellow'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-remove-swap-slot-cut-paste-yellow-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file-blue1-in-yellow)
+          store    (ths/setup-store file)
+          blue1    (cths/get-shape file :blue1)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          yellow   (cths/get-shape file :frame-yellow)
+          b2       (cths/get-shape file :frame-b2)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id yellow))
+           (dw/delete-selected)
+           (dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b2'            (cths/get-shape file' :frame-b2)
+               copied-yellow' (find-copied-shape yellow page' (:id b2'))
+               copied-blue1'  (find-copied-shape blue1 page' (:id copied-yellow'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has not swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          green    (cths/get-shape file :green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               blue2'         (cths/get-shape file' :blue-copy-in-green-copy)
+               copied-green'  (find-copied-shape green page' uuid/zero)
+               copied-blue2'  (find-copied-shape blue2' page' (:id copied-green'))]
+
+          ;; ==== Check
+
+          ;; blue2 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue2')))
+
+          ;; copied-blue2 also has swap-id
+           (t/is (some? copied-blue2'))
+           (t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
+
+(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-b1
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b1       (cths/get-shape file :frame-b1)
+          green    (cths/get-shape file :green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b1))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b1'            (cths/get-shape file' :frame-b1)
+               blue2'         (cths/get-shape file' :blue-copy-in-green-copy)
+               copied-green'  (find-copied-shape green page' (:id b1'))
+               copied-blue2'  (find-copied-shape blue2' page' (:id copied-green'))]
+
+          ;; ==== Check
+
+          ;; blue1 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue2')))
+
+          ;; copied-blue1 also has swap-id
+           (t/is (some? copied-blue2'))
+           (t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
+
+(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b2       (cths/get-shape file :frame-b2)
+          green    (cths/get-shape file :green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b2'            (cths/get-shape file' :frame-b2)
+               blue2'         (cths/get-shape file' :blue-copy-in-green-copy)
+               copied-green'  (find-copied-shape green page' (:id b2'))
+               copied-blue2'  (find-copied-shape blue2' page' (:id copied-green'))]
+
+          ;; ==== Check
+
+          ;; blue2 has swap-id
+           (t/is (some? (ctk/get-swap-slot blue2')))
+
+          ;; copied-blue1 also has swap-id
+           (t/is (some? copied-blue2'))
+           (t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
+
+
+(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-root
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          green    (cths/get-shape file :green-copy)
+          blue2    (cths/get-shape file :blue-copy-in-green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id green))
+           (dw/delete-selected)
+           (dws/select-shape uuid/zero)
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               copied-green'  (find-copied-shape green page' uuid/zero)
+               copied-blue1'  (find-copied-shape blue2 page' (:id copied-green'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has swap-id
+           (t/is (some? copied-blue1'))
+           (t/is (some? (ctk/get-swap-slot copied-blue1')))))))))
+
+(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-b1
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b1       (cths/get-shape file :frame-b1)
+          green    (cths/get-shape file :green-copy)
+          blue2    (cths/get-shape file :blue-copy-in-green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id green))
+           (dw/delete-selected)
+           (dws/select-shape (:id b1))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b1'            (cths/get-shape file' :frame-b1)
+               copied-green'  (find-copied-shape green page' (:id b1'))
+               copied-blue2'  (find-copied-shape blue2 page' (:id copied-green'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has swap-id
+           (t/is (some? copied-blue2'))
+           (t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
+
+(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-b2
+  (t/async
+    done
+    (let [;; ==== Setup
+          file     (setup-file)
+          store    (ths/setup-store file)
+
+         ;; ==== Action
+          page     (cthf/current-page file)
+          b2       (cths/get-shape file :frame-b2)
+          blue2    (cths/get-shape file :blue-copy-in-green-copy)
+          green    (cths/get-shape file :green-copy)
+          features #{"components/v2"}
+          version  46
+
+          pdata    (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id  file) file} page file features version)
+
+          events
+          [(dws/select-shape (:id green))
+           (dw/delete-selected)
+           (dws/select-shape (:id b2))
+           (dw/paste-shapes pdata)]]
+
+      (ths/run-store
+       store done events
+       (fn [new-state]
+         (let [;; ==== Get
+               file'          (ths/get-file-from-store new-state)
+               page'          (cthf/current-page file')
+               b2'            (cths/get-shape file' :frame-b2)
+               copied-green'  (find-copied-shape green page' (:id b2'))
+               copied-blue2'  (find-copied-shape blue2 page' (:id copied-green'))]
+
+          ;; ==== Check
+          ;; copied-blue1 has swap-id
+           (t/is (some? copied-blue2'))
+           (t/is (some? (ctk/get-swap-slot copied-blue2')))))))))