penpot/common/test/common_tests/files/helpers_test.cljc
Andrey Antukh 10ccfd2218
🐛 Gate pastes on page load and harden base-shape lookup (#11674)
* 🐛 Gate pastes on page load and harden base-shape lookup

Pasting while the workspace is still opening crashed the
session: the layer-order lookup called rseq on a missing
root children list.

Ignore paste events until the page objects are loaded (the
clipboard keeps its content, so retrying works), return empty
instead of throwing from the shared layer-order helpers, and
fall back to pasting at the pointer position when the selection
is detached from the shape tree. Selecting the page root keeps
working as before through the frame branches.

Closes #11666

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Cover props paste and root-plus-other in paste guards

Address review follow-ups on the paste-before-init fix: gate
props pasting on page readiness like the shape entries, and
route root-plus-other selections without a base shape to the
pointer fallback instead of the unguarded else branch. Pin
single-root selection to the frame path with a regression test.

Closes #11666

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Hoist page lookup out of paste-shapes gate

Bind page and page-objects once in an outer let instead of
calling lookup-page twice (once for the readiness gate and
once inside the body). No behavior change.

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Reuse bound ids in paste-shapes page lookup

Bind file-id and page-id once and pass them to the lookup-page
arity that takes both, instead of resolving the page twice and
rebinding file-id in the inner let. No behavior change.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-30 07:24:03 +02:00

98 lines
4.8 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.files.helpers-test
(:require
[app.common.files.helpers :as cfh]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/deftest test-generate-unique-name
(t/testing "Test unique name generation"
(let [suffix-fn #(str "-copy-" %)]
(t/is (cfh/generate-unique-name "base-name"
#{"base-name" "base-name-copy-1"}
:suffix-fn suffix-fn)
"base-name-copy-2")
(t/is (cfh/generate-unique-name "base-name"
#{"base-name-copy-2"}
:suffix-fn suffix-fn)
"base-name-copy-1")
(t/is (cfh/generate-unique-name "base-name"
#{"base-namec-copy"}
:suffix-fn suffix-fn)
"base-name-copy-1")
(t/is (cfh/generate-unique-name "base-name"
#{"base-name"}
:suffix-fn suffix-fn)
"base-name-copy-1")))
(t/testing "Test unique name generation with immidate suffix and default suffix-fn"
(t/is (cfh/generate-unique-name "base-name" #{} :immediate-suffix? true)
"base-name 1")
(t/is (cfh/generate-unique-name "base-name"
#{"base-name 1" "base-name 2"}
:immediate-suffix? true)
"base-name 3")))
(t/deftest test-get-base-shape-with-missing-data
(let [root-id uuid/zero
shape-a {:id (uuid/custom 1 1) :parent-id root-id :frame-id root-id}
shape-b {:id (uuid/custom 1 2) :parent-id root-id :frame-id root-id}
selected #{(:id shape-a) (:id shape-b)}]
(t/testing "Returns nil instead of throwing with nil objects"
(t/is (nil? (cfh/get-base-shape nil selected))))
(t/testing "Returns nil instead of throwing with empty objects"
(t/is (nil? (cfh/get-base-shape {} selected))))
(t/testing "Returns nil instead of throwing when the root has no shapes"
(t/is (nil? (cfh/get-base-shape {root-id {:id root-id}} selected))))
(t/testing "Returns nil when the selection is not present in the objects"
(let [objects {root-id {:id root-id :shapes [(:id shape-a)]}
(:id shape-a) shape-a}]
(t/is (nil? (cfh/get-base-shape objects #{(:id shape-b)})))))
(t/testing "Selecting the root itself never yields a base shape (callers handle it)"
(let [objects {root-id {:id root-id :shapes [(:id shape-a)]}
(:id shape-a) shape-a}]
(t/is (nil? (cfh/get-base-shape objects #{root-id})))))))
(t/deftest test-order-by-indexed-shapes
(let [root-id uuid/zero
shape-a {:id (uuid/custom 1 1) :parent-id root-id :frame-id root-id}
shape-b {:id (uuid/custom 1 2) :parent-id root-id :frame-id root-id}
shape-c {:id (uuid/custom 1 3) :parent-id root-id :frame-id root-id}
objects {root-id {:id root-id :shapes [(:id shape-a) (:id shape-b) (:id shape-c)]}
(:id shape-a) shape-a
(:id shape-b) shape-b
(:id shape-c) shape-c}]
(t/testing "Orders selection top-most first on healthy inputs"
(t/is (= [(:id shape-c) (:id shape-a)]
(cfh/order-by-indexed-shapes objects #{(:id shape-a) (:id shape-c)})))
(t/is (= shape-c
(cfh/get-base-shape objects #{(:id shape-a) (:id shape-c)}))))
(t/testing "Returns empty instead of throwing with nil objects"
(t/is (= [] (cfh/order-by-indexed-shapes nil #{(:id shape-a)})))
(t/is (nil? (cfh/get-base-shape nil #{(:id shape-a)}))))))
(t/deftest test-get-position-on-parent-with-missing-data
(t/testing "Returns nil instead of throwing with missing data"
(t/is (nil? (cfh/get-position-on-parent nil nil)))
(t/is (nil? (cfh/get-position-on-parent {} (uuid/custom 1 9))))))
(t/deftest test-get-prev-sibling
(let [parent-id (uuid/custom 1 1)
child-a (uuid/custom 1 2)
child-b (uuid/custom 1 3)
orphan-id (uuid/custom 1 4)
objects {parent-id {:id parent-id :shapes [child-a child-b]}
child-a {:id child-a :parent-id parent-id}
child-b {:id child-b :parent-id parent-id}
orphan-id {:id orphan-id :parent-id parent-id}}]
(t/testing "Returns previous sibling when present in parent ordering"
(t/is (= child-a
(cfh/get-prev-sibling objects child-b))))
(t/testing "Returns nil when the shape is missing from parent ordering"
(t/is (nil? (cfh/get-prev-sibling objects orphan-id))))))