penpot/common/test/common_tests/files_migrations_test.cljc
2026-09-16 10:17:26 +02:00

209 lines
11 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-migrations-test
(:require
[app.common.data :as d]
[app.common.files.migrations :as cfm]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(defmethod cfm/migrate-data "test/1" [data _] (update data :sum inc))
(defmethod cfm/migrate-data "test/2" [data _] (update data :sum inc))
(defmethod cfm/migrate-data "test/3" [data _] (update data :sum inc))
(t/deftest generic-migration-subsystem-1
(let [migrations (into (d/ordered-set) ["test/1" "test/2" "test/3"])]
(with-redefs [cfm/available-migrations migrations
ctf/check-file-data identity]
(let [file {:data {:sum 1}
:id 1
:migrations (d/ordered-set "test/1")}
file' (cfm/migrate file nil)]
(t/is (= cfm/available-migrations (:migrations file')))
(t/is (= 3 (:sum (:data file'))))))))
(t/deftest migration-0024b-fix-stroke-cap-placement
(let [shape-id (uuid/next)
page-id (uuid/next)
data {:pages-index
{page-id
{:objects
{shape-id {:id shape-id
:type :path
:stroke-cap-start "round"
:stroke-cap-end "round"
:strokes [{:stroke-color "#000000"
:stroke-opacity 1
:stroke-style :svg
:stroke-width 2}
{:stroke-color "#000000"
:stroke-cap-start "round"
:stroke-cap-end "round"
:stroke-opacity 1
:stroke-style :svg
:stroke-width 2}]}}}}}
data' (cfm/migrate-data data "0024b-fix-stroke-cap-placement")]
(let [shape (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (nil? (:stroke-cap-start shape)) "top-level cap removed")
(t/is (nil? (:stroke-cap-end shape)) "top-level cap removed")
(t/is (= :round (get-in shape [:strokes 0 :stroke-cap-start])) "cap moved into stroke")
(t/is (= :round (get-in shape [:strokes 0 :stroke-cap-end])) "cap moved into stroke")
(t/is (= :round (get-in shape [:strokes 1 :stroke-cap-start])) "correct cap type")
(t/is (= :round (get-in shape [:strokes 1 :stroke-cap-end])) "correct cap type"))))
(t/deftest migration-0024-fix-stroke-cap-no-strokes
(let [shape-id (uuid/next)
page-id (uuid/next)
data {:pages-index
{page-id
{:objects
{shape-id {:id shape-id
:type :path
:stroke-cap-start :round
:stroke-cap-end :round
:strokes []}}}}}
data' (cfm/migrate-data data "0024b-fix-stroke-cap-placement")]
(let [shape (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (nil? (:stroke-cap-start shape)) "top-level cap removed even with no strokes")
(t/is (nil? (:stroke-cap-end shape)) "top-level cap removed even with no strokes"))))
(t/deftest migration-0027-normalizes-constrained-shape-values
(let [file-id (uuid/next)
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :frame})
(assoc :r1 -1
:r2 -2
:r3 -3
:r4 -4
:layout-gap {:row-gap -5 :column-gap -6}
:layout-padding {:p1 -7 :p2 -8 :p3 -9 :p4 -10}
:layout-grid-rows [{:type :fixed :value -11}]
:layout-grid-columns [{:type :percent :value -12}]
:layout-item-min-w -13
:layout-item-max-w -14
:layout-item-min-h -15
:layout-item-max-h -16
:strokes [{:stroke-color "#000000"
:stroke-width -17
:stroke-width-top -18
:stroke-width-right -19
:stroke-width-bottom -20
:stroke-width-left -21}]
:shadow [{:id nil
:style :drop-shadow
:offset-x 0
:offset-y 0
:blur -22
:spread 0
:hidden false
:color {:color "#000000" :opacity 1}}]
:blur {:id (uuid/next)
:type :layer-blur
:value -23
:hidden false}
:background-blur {:id (uuid/next)
:type :background-blur
:value -24
:hidden false}
:exports [{:type :png :scale 0 :suffix ""}]
:grids [{:type :square
:display true
:params {:size 0
:color {:color "#000000" :opacity 1}}}
{:type :column
:display true
:params {:size -25
:color {:color "#000000" :opacity 1}}}]))
data (-> (ctf/make-file-data file-id page-id)
(assoc-in [:pages-index page-id :objects shape-id] shape))]
(t/is (thrown? #?(:clj Exception :cljs js/Error)
(ctf/check-file-data data))
"new schemas reject legacy negative values")
(let [data' (cfm/migrate-data data "0027-normalize-constrained-values")
shape' (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (= data' (ctf/check-file-data data')) "migrated file data passes the schema")
(t/is (every? zero? (map #(get shape' %) [:r1 :r2 :r3 :r4])) "corner radii clamped")
(t/is (= {:row-gap 0 :column-gap 0} (:layout-gap shape')) "layout gaps clamped")
(t/is (= {:p1 0 :p2 0 :p3 0 :p4 0} (:layout-padding shape')) "layout padding clamped")
(t/is (= [0] (mapv :value (:layout-grid-rows shape'))) "row tracks clamped")
(t/is (= [0] (mapv :value (:layout-grid-columns shape'))) "column tracks clamped")
(t/is (every? zero?
(map #(get shape' %)
[:layout-item-min-w :layout-item-max-w
:layout-item-min-h :layout-item-max-h]))
"layout item bounds clamped")
(t/is (every? zero?
(map (first (:strokes shape'))
[:stroke-width :stroke-width-top :stroke-width-right
:stroke-width-bottom :stroke-width-left]))
"stroke widths clamped")
(t/is (zero? (get-in shape' [:shadow 0 :blur])) "shadow blur clamped")
(t/is (zero? (get-in shape' [:blur :value])) "layer blur clamped")
(t/is (zero? (get-in shape' [:background-blur :value])) "background blur clamped")
(t/is (= 1 (get-in shape' [:exports 0 :scale])) "export scale reset to default")
(t/is (= 0.01 (get-in shape' [:grids 0 :params :size])) "square grid size clamped")
(t/is (= 1 (get-in shape' [:grids 1 :params :size])) "column grid count clamped"))))
(t/deftest migration-0027-normalizes-page-grids-and-variant-properties
(let [file-id (uuid/next)
page-id (uuid/next)
component-id (uuid/next)
long-name (apply str (repeat 61 "n"))
long-value (apply str (repeat 61 "v"))
data (-> (ctf/make-file-data file-id page-id)
(assoc-in [:pages-index page-id :default-grids]
{:square {:size -1
:color {:color "#000000" :opacity 1}}
:row {:size 0.5
:color {:color "#000000" :opacity 1}}
:column {:size -2
:color {:color "#000000" :opacity 1}}})
(assoc-in [:components component-id]
{:id component-id
:name "Variant"
:variant-properties [{:name long-name
:value long-value}]}))
data' (cfm/migrate-data data "0027-normalize-constrained-values")]
(t/is (= 0.01 (get-in data' [:pages-index page-id :default-grids :square :size]))
"default square grid size clamped")
(t/is (= 1 (get-in data' [:pages-index page-id :default-grids :row :size]))
"default row grid count reset")
(t/is (= 1 (get-in data' [:pages-index page-id :default-grids :column :size]))
"default column grid count reset")
(t/is (= 60 (count (get-in data' [:components component-id :variant-properties 0 :name])))
"variant property name truncated")
(t/is (= 60 (count (get-in data' [:components component-id :variant-properties 0 :value])))
"variant property value truncated")
(t/is (= data' (cfm/migrate-data data' "0027-normalize-constrained-values"))
"migration is idempotent")))
(t/deftest migration-0027-runs-through-file-migration
(let [migration-id "0027-normalize-constrained-values"
shape-id (uuid/next)
file (ctf/make-file {:name "Legacy constrained values"})
page-id (first (get-in file [:data :pages]))
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :r1 -1))
file (-> file
(assoc :migrations (disj cfm/available-migrations migration-id))
(assoc-in [:data :pages-index page-id :objects shape-id] shape))
file' (cfm/migrate-file file {})]
(t/is (cfm/need-migration? file) "new migration detected")
(t/is (not (cfm/need-migration? file')) "new migration recorded")
(t/is (contains? (:migrations file') migration-id) "migration id persisted")
(t/is (zero? (get-in file' [:data :pages-index page-id :objects shape-id :r1]))
"migration repaired file data before schema validation")))