mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Test component validation, repair and library asset sync
Fill the unit test gaps in common and frontend component code: - One test per component error code: break a file in one way, check the codes validation reports, repair it as the backend does (until valid, at most ten passes) and check what the repair changed. Pinned: F46 (a dangling ref in a copy child is never reported), F47 and F48 (two repairs that never converge). :component-not-main, :should-be-component-root and :not-component-not-allowed have no test, as no file can produce them. - Library color and typography sync must not touch copies. Both do (F3): the color sync touches :fill-group, the typography sync :content-group and :text-content-attribute, and a later main edit of that attribute then no longer reaches the copy. Pinned. - Twenty live thf/dump-file calls in common tests go; they printed whole file trees on every run and asserted nothing. - The disabled copy-nested-3 frontend test runs again: a stray backquote quoted its run-store call, so it never called done.
This commit is contained in:
parent
2cae81f588
commit
7400ada8b4
@ -44,6 +44,15 @@ For shape mutations, prefer production-path helpers such as `cls/generate-update
|
||||
|
||||
`thf/apply-changes` validates by default and usually gives the most useful invariant failure. Pass `:validate? false` only for intentionally malformed intermediate state.
|
||||
|
||||
## Validation and repair tests
|
||||
|
||||
`common-tests.files.repair-components-test` has one test per component error code: break a file with
|
||||
the helpers (`ths/update-shape`, `thc/update-component`), assert the exact set of codes, then repair
|
||||
as the backend does (validate + `cfr/repair-file` until valid, at most 10 passes, like
|
||||
`app.srepl.procs.file-repair`) and assert the result. Repairs that never converge are pinned with
|
||||
`tm/known-failure`. A deleted component's objects are reached with `ctkl/get-component data id true`
|
||||
(`thc/get-component` skips deleted ones).
|
||||
|
||||
## Geometry setup caution
|
||||
|
||||
For geometry-sensitive tests, read `mem:common/geometry-invariants` before positioning shapes. Use geometry-preserving helpers or production change helpers rather than direct single-field edits.
|
||||
|
||||
@ -860,6 +860,5 @@
|
||||
(:id copy2-root-child2)
|
||||
{:component-id [(thi/id :component1) (thi/id :wrong-comp3)]}}}}}}]
|
||||
|
||||
(thf/dump-file file') ;; Uncomment to debug
|
||||
(t/is (= expected-diff diff)))))
|
||||
|
||||
|
||||
365
common/test/common_tests/files/repair_components_test.cljc
Normal file
365
common/test/common_tests/files/repair_components_test.cljc
Normal file
@ -0,0 +1,365 @@
|
||||
;; 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.repair-components-test
|
||||
"One test per component error code of `app.common.files.validate`: each
|
||||
builds a file broken in one way, checks the codes validation reports,
|
||||
repairs it as the backend does (validate and repair until valid, at most
|
||||
ten passes) and checks the result.
|
||||
|
||||
Three codes have no test because no file can produce them: the checks that
|
||||
report `:component-not-main`, `:should-be-component-root` and
|
||||
`:not-component-not-allowed` only run on shapes that already satisfy them."
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.repair :as cfr]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[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.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
;; The backend repairs a file at most this many times (`app.srepl.procs.file-repair`).
|
||||
(def ^:private max-passes 10)
|
||||
|
||||
(defn- codes
|
||||
[errors]
|
||||
(into #{} (map :code) errors))
|
||||
|
||||
(defn- repair
|
||||
"Validate `file` and repair it until it validates, as the backend does.
|
||||
Returns `[codes file' errors']`: the codes found first, the repaired file
|
||||
and the errors left after the last pass (nil when it validates)."
|
||||
([file] (repair file {}))
|
||||
([file libraries]
|
||||
(let [errors (cfv/validate-file file libraries)]
|
||||
(loop [file file
|
||||
errs errors
|
||||
passes 0]
|
||||
(if (or (nil? errs) (= passes max-passes))
|
||||
[(codes errors) file errs]
|
||||
(let [changes (cfr/repair-file file libraries errs)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)]
|
||||
(recur file' (cfv/validate-file file' libraries) (inc passes))))))))
|
||||
|
||||
;; ---- Configurations
|
||||
|
||||
(defn- simple
|
||||
"A component (:main-root with :main-child) and a copy (:copy-root with
|
||||
:copy-child)."
|
||||
[& {:as params}]
|
||||
(-> (thf/sample-file :file1 :page-label :page1)
|
||||
(tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root
|
||||
(merge {:copy-root-params {:children-labels [:copy-child]}}
|
||||
params))))
|
||||
|
||||
(defn- nested
|
||||
"Component2 (:main2-root) nests a copy of component1 (:main2-nested-head);
|
||||
its copy :copy2-root holds :copy2-nested-head."
|
||||
[]
|
||||
(-> (thf/sample-file :file1 :page-label :page1)
|
||||
(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-nested-head :copy2-child]})))
|
||||
|
||||
(defn- two-nested
|
||||
"Component2 (:main2-root) nests two copies of component1 (:nested-a,
|
||||
:nested-b); its copy :copy2-root holds their copies :copy-a and :copy-b."
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(tho/add-simple-component :component1 :main1-root :main1-child)
|
||||
(tho/add-frame :main2-root)
|
||||
(thc/instantiate-component :component1 :nested-a :parent-label :main2-root)
|
||||
(thc/instantiate-component :component1 :nested-b :parent-label :main2-root)
|
||||
(thc/make-component :component2 :main2-root)
|
||||
(thc/instantiate-component :component2 :copy2-root))
|
||||
objects (:objects (thf/current-page file))]
|
||||
(doseq [[main-label copy-label] [[:nested-a :copy-a] [:nested-b :copy-b]]]
|
||||
(thi/set-id! copy-label
|
||||
(->> (get-in objects [(thi/id :copy2-root) :shapes])
|
||||
(filter #(= (thi/id main-label) (get-in objects [% :shape-ref])))
|
||||
(first))))
|
||||
file))
|
||||
|
||||
(defn- reparent
|
||||
"Move the shape `label` under the shape `parent-id`, keeping the links
|
||||
consistent."
|
||||
[file label parent-id]
|
||||
(let [objects (:objects (thf/current-page file))
|
||||
shape (get objects (thi/id label))
|
||||
old-id (:parent-id shape)
|
||||
parent (get objects parent-id)]
|
||||
(-> file
|
||||
(ths/update-shape-by-id old-id :shapes (filterv #(not= (:id shape) %) (:shapes (get objects old-id))))
|
||||
(ths/update-shape-by-id parent-id :shapes (conj (vec (:shapes parent)) (:id shape)))
|
||||
(ths/update-shape label :parent-id parent-id)
|
||||
(ths/update-shape label :frame-id (if (= :frame (:type parent)) parent-id (:frame-id parent))))))
|
||||
|
||||
(defn- delete-main
|
||||
"Delete the main `label`, which deletes its component."
|
||||
[file label]
|
||||
(let [page (thf/current-page file)
|
||||
[_ changes] (cls/generate-delete-shapes (pcb/empty-changes nil (:id page))
|
||||
file page (:objects page) #{(thi/id label)} {})]
|
||||
(thf/apply-changes file changes :validate? false)))
|
||||
|
||||
(defn- update-deleted-object
|
||||
"Apply `f` to the shape `label` stored in the deleted component
|
||||
`component-label`."
|
||||
[file component-label label f]
|
||||
(ctf/update-file-data file
|
||||
(fn [data]
|
||||
(ctkl/update-component data (thi/id component-label)
|
||||
#(update-in % [:objects (thi/id label)] f)))))
|
||||
|
||||
(defn- deleted-object
|
||||
[file component-label label]
|
||||
(get-in (ctkl/get-component (:data file) (thi/id component-label) true)
|
||||
[:objects (thi/id label)]))
|
||||
|
||||
(defn- slot-group
|
||||
[id]
|
||||
#{(ctk/build-swap-slot-group id)})
|
||||
|
||||
;; ---- Main heads
|
||||
|
||||
(t/deftest component-main-external
|
||||
;; the main points at a library that also has its component
|
||||
(let [file (ths/update-shape (simple) :main-root :component-file (thi/new-id! :library))
|
||||
library (assoc file :id (thi/id :library))
|
||||
[found file' errors] (repair file {(:id library) library})]
|
||||
(t/is (= #{:component-main-external} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :file1) (:component-file (ths/get-shape file' :main-root))))))
|
||||
|
||||
(t/deftest component-not-found
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :component-id (uuid/next)))]
|
||||
(t/is (= #{:component-not-found :ref-shape-not-found} found))
|
||||
(t/is (nil? errors))
|
||||
;; the copy is detached
|
||||
(t/is (nil? (:component-id (ths/get-shape file' :copy-root))))))
|
||||
|
||||
(t/deftest invalid-main-instance-id
|
||||
(let [[found file' errors] (repair (thc/update-component (simple) :component1 :main-instance-id (uuid/next)))]
|
||||
(t/is (= #{:invalid-main-instance-id :ref-shape-not-found} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :main-root) (:main-instance-id (thc/get-component file' :component1))))))
|
||||
|
||||
(t/deftest invalid-main-instance-page
|
||||
(let [file (-> (simple)
|
||||
(thf/add-sample-page :page2)
|
||||
(thf/switch-to-page :page1)
|
||||
(thc/update-component :component1 :main-instance-page (thi/id :page2)))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:invalid-main-instance-page :ref-shape-not-found} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :page1) (:main-instance-page (thc/get-component file' :component1))))))
|
||||
|
||||
(t/deftest invalid-main-instance
|
||||
;; the component names the copy as its main; the repair of
|
||||
;; :invalid-main-instance does nothing, the main's own error fixes it
|
||||
(let [[found file' errors] (repair (thc/update-component (simple) :component1 :main-instance-id (thi/id :copy-root)))]
|
||||
(t/is (= #{:invalid-main-instance :invalid-main-instance-id :ref-shape-not-found} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :main-root) (:main-instance-id (thc/get-component file' :component1))))))
|
||||
|
||||
(t/deftest shape-ref-in-main
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :main-root :shape-ref (thi/id :copy-root)))]
|
||||
(t/is (= #{:shape-ref-in-main} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (:shape-ref (ths/get-shape file' :main-root))))))
|
||||
|
||||
;; ---- Shapes inside mains
|
||||
|
||||
(t/deftest component-main-flag-on-a-child
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :main-child :main-instance true))]
|
||||
(t/is (= #{:component-main} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (:main-instance (ths/get-shape file' :main-child))))))
|
||||
|
||||
(t/deftest component-main-file-on-a-child
|
||||
;; the repair only removes :main-instance, so the error never goes away
|
||||
(let [[found _ errors] (repair (ths/update-shape (simple) :main-child :component-file (thi/id :file1)))]
|
||||
(t/is (= #{:component-main} found))
|
||||
(tm/known-failure "F47" nil #{:component-main} (codes errors))))
|
||||
|
||||
(t/deftest should-not-be-component-root
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :main-child :component-root true))]
|
||||
(t/is (= #{:should-not-be-component-root} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (:component-root (ths/get-shape file' :main-child))))))
|
||||
|
||||
(t/deftest not-head-main-not-allowed
|
||||
;; a main root without component is no longer a head; repair detaches the
|
||||
;; main and then its copy
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :main-root :component-id nil))]
|
||||
(t/is (= #{:not-head-main-not-allowed :ref-shape-is-not-head} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (:main-instance (ths/get-shape file' :main-root))))
|
||||
(t/is (nil? (:shape-ref (ths/get-shape file' :copy-root))))))
|
||||
|
||||
;; ---- Copies
|
||||
|
||||
(t/deftest ref-shape-not-found
|
||||
(let [[found file' errors] (repair (ths/update-shape (nested) :copy2-nested-head :shape-ref (uuid/next)))]
|
||||
(t/is (= #{:ref-shape-not-found :missing-slot} found))
|
||||
(t/is (nil? errors))
|
||||
;; no matching shape: the nested copy is detached
|
||||
(t/is (nil? (:shape-ref (ths/get-shape file' :copy2-nested-head))))))
|
||||
|
||||
(t/deftest ref-shape-not-found-in-a-copy-child
|
||||
;; the reference of a non-head copy shape is never checked: the check reads
|
||||
;; the library from the shape itself, and only heads have one
|
||||
(let [errors (cfv/validate-file (ths/update-shape (simple) :copy-child :shape-ref (uuid/next)) {})]
|
||||
(tm/known-failure "F46" #{:ref-shape-not-found} #{} (codes errors))))
|
||||
|
||||
(t/deftest ref-shape-is-head
|
||||
;; the check reports no component, so the repair cannot make the shape a
|
||||
;; head again
|
||||
(let [file (-> (nested)
|
||||
(ths/update-shape :copy2-nested-head :component-id nil)
|
||||
(ths/update-shape :copy2-nested-head :component-file nil))
|
||||
[found _ errors] (repair file)]
|
||||
(t/is (= #{:ref-shape-is-head} found))
|
||||
(tm/known-failure "F48" nil #{:ref-shape-is-head} (codes errors))))
|
||||
|
||||
(t/deftest ref-shape-is-not-head
|
||||
;; a copy child made a head while the shape it references is not one
|
||||
(let [file (-> (simple :main-child-params {:type :frame})
|
||||
(ths/update-shape :copy-child :component-id (thi/id :component1))
|
||||
(ths/update-shape :copy-child :component-file (thi/id :file1)))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:ref-shape-is-not-head :component-id-mismatch} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (not (ctk/instance-head? (ths/get-shape file' :copy-child))))))
|
||||
|
||||
(t/deftest component-id-mismatch
|
||||
(let [[found file' errors] (repair (ths/update-shape (nested) :copy2-nested-head :component-id (thi/id :component2)))]
|
||||
(t/is (= #{:component-id-mismatch} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :component1) (:component-id (ths/get-shape file' :copy2-nested-head))))))
|
||||
|
||||
(t/deftest not-head-copy-not-allowed
|
||||
(let [[found file' errors] (repair (reparent (simple) :copy-child uuid/zero))]
|
||||
(t/is (= #{:not-head-copy-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (:shape-ref (ths/get-shape file' :copy-child))))))
|
||||
|
||||
(t/deftest instance-head-not-frame
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :type :rect))]
|
||||
(t/is (= #{:instance-head-not-frame} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= :frame (:type (ths/get-shape file' :copy-root))))))
|
||||
|
||||
(t/deftest invalid-text-touched
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :copy-child :touched #{:text-content-text}))]
|
||||
(t/is (= #{:invalid-text-touched} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (contains? (:touched (ths/get-shape file' :copy-child)) :content-group))))
|
||||
|
||||
;; ---- Nesting
|
||||
|
||||
(t/deftest root-main-not-allowed
|
||||
(let [file (-> (simple)
|
||||
(tho/add-simple-component :component2 :main2-root :main2-child)
|
||||
(reparent :main-root (thi/id :main2-root)))
|
||||
[found file' errors] (repair file)
|
||||
main (ths/get-shape file' :main-root)]
|
||||
(t/is (= #{:root-main-not-allowed :nested-main-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
;; moved back to the page root, as a top main
|
||||
(t/is (= uuid/zero (:parent-id main)))
|
||||
(t/is (true? (:component-root main)))))
|
||||
|
||||
(t/deftest nested-main-not-allowed
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :main-root :component-root nil))]
|
||||
(t/is (= #{:nested-main-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (true? (:component-root (ths/get-shape file' :main-root))))))
|
||||
|
||||
(t/deftest root-copy-not-allowed
|
||||
(let [[found file' errors] (repair (reparent (simple) :copy-root (thi/id :main-root)))]
|
||||
(t/is (= #{:root-copy-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
;; it stays in the main, as a nested copy
|
||||
(t/is (nil? (:component-root (ths/get-shape file' :copy-root))))))
|
||||
|
||||
(t/deftest nested-copy-not-allowed
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :component-root nil))]
|
||||
(t/is (= #{:nested-copy-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (true? (:component-root (ths/get-shape file' :copy-root))))))
|
||||
|
||||
;; ---- Components
|
||||
|
||||
(t/deftest component-nil-objects-not-allowed
|
||||
(let [[found file' errors] (repair (thc/update-component (simple) :component1 :objects nil))]
|
||||
(t/is (= #{:component-nil-objects-not-allowed} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (not (contains? (thc/get-component file' :component1) :objects)))))
|
||||
|
||||
(t/deftest non-deleted-component-cannot-have-objects
|
||||
(let [[found file' errors] (repair (thc/update-component (simple) :component1 :objects {(uuid/next) {}}))]
|
||||
(t/is (= #{:non-deleted-component-cannot-have-objects} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (not (contains? (thc/get-component file' :component1) :objects)))))
|
||||
|
||||
(t/deftest shape-ref-cycle
|
||||
(let [file (-> (two-nested)
|
||||
(delete-main :main2-root)
|
||||
(update-deleted-object :component2 :nested-a #(assoc % :shape-ref (:id %))))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:shape-ref-cycle} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (not= (thi/id :nested-a) (:shape-ref (deleted-object file' :component2 :nested-a))))))
|
||||
|
||||
;; ---- Swap slots
|
||||
|
||||
(t/deftest misplaced-slot
|
||||
(let [[found file' errors] (repair (ths/update-shape (simple) :copy-root :touched (slot-group (uuid/next))))]
|
||||
(t/is (= #{:misplaced-slot} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (ctk/get-swap-slot (ths/get-shape file' :copy-root))))))
|
||||
|
||||
(t/deftest duplicate-slot
|
||||
(let [file (-> (two-nested)
|
||||
(ths/update-shape :copy-a :touched (slot-group (thi/id :nested-a)))
|
||||
(ths/update-shape :copy-b :touched (slot-group (thi/id :nested-a))))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:duplicate-slot} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (ctk/get-swap-slot (ths/get-shape file' :copy-b))))))
|
||||
|
||||
(t/deftest component-duplicate-slot
|
||||
(let [file (-> (two-nested)
|
||||
(delete-main :main2-root)
|
||||
(update-deleted-object :component2 :nested-a #(assoc % :touched (slot-group (thi/id :nested-a))))
|
||||
(update-deleted-object :component2 :nested-b #(assoc % :touched (slot-group (thi/id :nested-a)))))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:component-duplicate-slot} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (nil? (ctk/get-swap-slot (deleted-object file' :component2 :nested-b))))))
|
||||
|
||||
(t/deftest missing-slot
|
||||
(let [file (-> (two-nested)
|
||||
(tho/add-simple-component :component3 :main3-root :main3-child)
|
||||
(tho/swap-component-in-shape :copy-a :component3 :new-shape-label :swapped)
|
||||
(ths/update-shape :swapped :touched nil))
|
||||
[found file' errors] (repair file)]
|
||||
(t/is (= #{:missing-slot} found))
|
||||
(t/is (nil? errors))
|
||||
(t/is (= (thi/id :nested-a) (ctk/get-swap-slot (ths/get-shape file' :swapped))))))
|
||||
@ -133,7 +133,6 @@
|
||||
;; ==== Check
|
||||
|
||||
;; It should the same as above, but in an external library.
|
||||
(thf/dump-file file)
|
||||
(t/is (ctk/instance-root? copy-h-board-with-ellipse))
|
||||
(t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse)))
|
||||
(t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse)))
|
||||
|
||||
147
common/test/common_tests/logic/library_asset_sync_test.cljc
Normal file
147
common/test/common_tests/logic/library_asset_sync_test.cljc
Normal file
@ -0,0 +1,147 @@
|
||||
;; 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.library-asset-sync-test
|
||||
"Library color and typography sync in component copies. The sync writes
|
||||
the asset's new value into every shape that uses it, copies included.
|
||||
That write is derived from the library, not authored on the copy, so it
|
||||
must not touch the copy (`mem:common/component-sync-contract`), and later
|
||||
main edits must still reach it."
|
||||
(: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.composable.core :as tm]
|
||||
[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.text :as txt]
|
||||
[app.common.types.typographies-list :as cttl]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(defn- sync-file
|
||||
"Apply the sync of `asset-type` (`:colors`, `:typographies` or
|
||||
`:components`) for `asset-id` from the file to itself."
|
||||
[file asset-type asset-id]
|
||||
(let [changes (cll/generate-sync-file-changes (pcb/empty-changes)
|
||||
nil
|
||||
asset-type
|
||||
(:id file)
|
||||
asset-id
|
||||
(:id file)
|
||||
{(:id file) file}
|
||||
(:id file))]
|
||||
(thf/apply-changes file changes)))
|
||||
|
||||
(defn- update-shape
|
||||
"Change the shape `label` with `f` through the production change path."
|
||||
[file label f]
|
||||
(let [page (thf/current-page file)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id label)}
|
||||
f
|
||||
(:objects page)
|
||||
{})]
|
||||
(thf/apply-changes file changes)))
|
||||
|
||||
;; ---- Colors
|
||||
|
||||
(defn- color-fill
|
||||
[color]
|
||||
{:fill-color color
|
||||
:fill-opacity 1
|
||||
:fill-color-ref-id (thi/id :color1)
|
||||
:fill-color-ref-file (thi/id :file1)})
|
||||
|
||||
(defn- component-using-color
|
||||
"A component whose child is filled with the library color :color1
|
||||
(#111111), and a copy of it."
|
||||
[]
|
||||
(-> (thf/sample-file :file1)
|
||||
(ths/add-sample-library-color :color1 :color "#111111")
|
||||
(tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root
|
||||
:main-child-params {:fills [(color-fill "#111111")]}
|
||||
:copy-root-params {:children-labels [:copy-child]})))
|
||||
|
||||
(defn- change-library-color
|
||||
[file color]
|
||||
(-> file
|
||||
(assoc-in [:data :colors (thi/id :color1) :color] color)
|
||||
(sync-file :colors (thi/id :color1))))
|
||||
|
||||
(t/deftest library-color-sync-reaches-the-copy-untouched
|
||||
(let [file' (change-library-color (component-using-color) "#222222")
|
||||
copy (ths/get-shape file' :copy-child)]
|
||||
(t/is (= "#222222" (-> copy :fills first :fill-color)))
|
||||
;; the sync writes the fill without ignore-touched
|
||||
(tm/known-failure "F3" nil #{:fill-group} (:touched copy))))
|
||||
|
||||
(t/deftest main-fill-edit-reaches-the-copy-after-a-library-color-sync
|
||||
(let [file' (-> (component-using-color)
|
||||
(change-library-color "#222222")
|
||||
(sync-file :components (thi/id :component1))
|
||||
(update-shape :main-child #(assoc % :fills (ths/sample-fills-color :fill-color "#333333")))
|
||||
(sync-file :components (thi/id :component1)))]
|
||||
;; the touched fill blocks it
|
||||
(tm/known-failure "F3" "#333333" "#222222" (-> (ths/get-shape file' :copy-child) :fills first :fill-color))))
|
||||
|
||||
;; ---- Typographies
|
||||
|
||||
(defn- typography-text
|
||||
"A text \"hello\" styled, on every node, with the library typography
|
||||
:typography1, as applying a typography in the app does."
|
||||
[font-size]
|
||||
(-> (txt/change-text (:content (ths/sample-shape :tmp :type :text)) "hello")
|
||||
(->> (txt/transform-nodes txt/is-content-node?
|
||||
#(assoc %
|
||||
:font-size font-size
|
||||
:typography-ref-id (thi/id :typography1)
|
||||
:typography-ref-file (thi/id :file1))))))
|
||||
|
||||
(defn- component-using-typography
|
||||
"A component whose child is a text styled with the library typography
|
||||
:typography1 (font size 14), and a copy of it."
|
||||
[]
|
||||
(-> (thf/sample-file :file1)
|
||||
(ths/add-sample-typography :typography1 :font-size "14")
|
||||
(tho/add-frame-with-text :main-root :main-child "hello"
|
||||
:child-params {:content (typography-text "14")})
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root :children-labels [:copy-child])))
|
||||
|
||||
(defn- font-size-of
|
||||
"The font size of the first span of the text `shape`."
|
||||
[shape]
|
||||
(get-in shape [:content :children 0 :children 0 :children 0 :font-size]))
|
||||
|
||||
(defn- change-library-typography
|
||||
[file font-size]
|
||||
(-> file
|
||||
(update :data cttl/update-typography (thi/id :typography1) assoc :font-size font-size)
|
||||
(sync-file :typographies (thi/id :typography1))))
|
||||
|
||||
(t/deftest library-typography-sync-reaches-the-copy-untouched
|
||||
(let [file' (change-library-typography (component-using-typography) "24")
|
||||
copy (ths/get-shape file' :copy-child)]
|
||||
(t/is (= "24" (font-size-of copy)))
|
||||
;; the sync writes the content with a raw :set, which touches it
|
||||
(tm/known-failure "F3" nil #{:content-group :text-content-attribute} (:touched copy))))
|
||||
|
||||
(t/deftest main-font-edit-reaches-the-copy-after-a-library-typography-sync
|
||||
(let [file' (-> (component-using-typography)
|
||||
(change-library-typography "24")
|
||||
(sync-file :components (thi/id :component1))
|
||||
(update-shape :main-child
|
||||
(fn [shape]
|
||||
(update shape :content
|
||||
#(txt/transform-nodes txt/is-content-node? (fn [node] (assoc node :font-size "40")) %))))
|
||||
(sync-file :components (thi/id :component1)))]
|
||||
;; the touched text attributes block it
|
||||
(tm/known-failure "F3" "40" "24" (font-size-of (ths/get-shape file' :copy-child)))))
|
||||
@ -43,7 +43,6 @@
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
(thf/dump-file file :keys [:width])
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width copy01) 5))
|
||||
;; The copy has width 15 after the switch
|
||||
@ -108,7 +107,6 @@
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
(thf/dump-file file :keys [:width])
|
||||
;; The copy had width 25 before the switch
|
||||
(t/is (= (:width copy01) 25))
|
||||
;; The override is keept: The copy still has width 25 after the switch
|
||||
@ -222,8 +220,6 @@
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width copy01) 5))
|
||||
;; The copy has width 15 after the switch
|
||||
@ -257,8 +253,6 @@
|
||||
|
||||
child01' (ths/get-shape file' :child01)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width child01) 5))
|
||||
;; The copy has width 15 after the switch
|
||||
@ -305,8 +299,6 @@
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
|
||||
(thf/dump-file file :keys [:width :touched])
|
||||
|
||||
;; The copy had fill color before the switch
|
||||
(t/is (= (:fills copy01) fills))
|
||||
;; The copy still has fill color after the switch
|
||||
@ -358,8 +350,6 @@
|
||||
|
||||
nested02' (ths/get-shape file' :nested02)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had fill color before the switch
|
||||
(t/is (= (:fills nested02) fills))
|
||||
;; The copy still has fill color after the switch
|
||||
@ -475,8 +465,6 @@
|
||||
copy-both' (ths/get-shape file' :copy-both-2)
|
||||
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
|
||||
;;;;;;;;;;; Clean copy
|
||||
;; Before the switch:
|
||||
@ -599,8 +587,6 @@
|
||||
copy-both' (ths/get-shape file' :copy-both-2)
|
||||
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
|
||||
;;;;;;;;;;; Clean copy
|
||||
;; Before the switch:
|
||||
@ -721,8 +707,6 @@
|
||||
copy-both' (ths/get-shape file' :copy-both-2)
|
||||
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
|
||||
;;;;;;;;;;; Clean copy
|
||||
;; Before the switch:
|
||||
@ -845,8 +829,6 @@
|
||||
copy-both' (ths/get-shape file' :copy-both-2)
|
||||
copy-both-t' (get-in page' [:objects (-> copy-both' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
|
||||
;;;;;;;;;;; Clean copy
|
||||
;; Before the switch:
|
||||
@ -964,8 +946,6 @@
|
||||
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
|
||||
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
;;;;;;;;;;; Copy structure clean
|
||||
;; Before the switch, first line:
|
||||
;; * font size 14
|
||||
@ -1095,8 +1075,6 @@
|
||||
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
|
||||
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
;;;;;;;;;;; Copy structure clean
|
||||
;; Before the switch, first line:
|
||||
;; * font size 14
|
||||
@ -1222,8 +1200,6 @@
|
||||
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
|
||||
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
;;;;;;;;;;; Copy structure clean
|
||||
;; Before the switch, first line:
|
||||
;; * font size 14
|
||||
@ -1350,8 +1326,6 @@
|
||||
copy-structure-mixed' (ths/get-shape file' :copy-structure-mixed-2)
|
||||
copy-structure-mixed-t' (get-in page' [:objects (-> copy-structure-mixed' :shapes first)])]
|
||||
|
||||
(thf/dump-file file' {:keys [:name #_:content]})
|
||||
|
||||
;;;;;;;;;;; Copy structure clean
|
||||
;; Before the switch, first line:
|
||||
;; * font size 14
|
||||
@ -1494,7 +1468,6 @@
|
||||
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
copy-cp02' (ths/get-shape file' :copy-cp02)]
|
||||
(thf/dump-file file')
|
||||
;;copy-cp02 is a copy of external02
|
||||
(t/is (= (:component-id copy-cp02) (:id external02)))
|
||||
;;copy-01 had copy-cp02 as child
|
||||
@ -1543,7 +1516,6 @@
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
copy-cp02' (ths/get-shape file' :copy-cp02)
|
||||
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
|
||||
(thf/dump-file file')
|
||||
;;copy-cp02 is a copy of external02
|
||||
(t/is (= (:component-id copy-cp02) (:id external02)))
|
||||
;;copy-01 had copy-cp02 as child
|
||||
@ -1611,7 +1583,6 @@
|
||||
c02-in-copy' (ths/get-shape file' :c02-in-copy)
|
||||
rect02' (get-in page' [:objects (-> c02-in-copy' :shapes first)])]
|
||||
|
||||
(thf/dump-file file :keys [:width :touched])
|
||||
;; The rect had width 25 before the switch
|
||||
(t/is (= (:width rect01) 25))
|
||||
;; The rect still has width 25 after the switch
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
[common-tests.files-migrations-test]
|
||||
[common-tests.files.comp-processors-test]
|
||||
[common-tests.files.helpers-test]
|
||||
[common-tests.files.repair-components-test]
|
||||
[common-tests.files.repair-test]
|
||||
[common-tests.files.shapes-builder-test]
|
||||
[common-tests.files.tokens-test]
|
||||
@ -69,6 +70,7 @@
|
||||
[common-tests.logic.composable-sync-test]
|
||||
[common-tests.logic.copying-and-duplicating-test]
|
||||
[common-tests.logic.duplicated-pages-test]
|
||||
[common-tests.logic.library-asset-sync-test]
|
||||
[common-tests.logic.move-shapes-test]
|
||||
[common-tests.logic.multiple-nesting-levels-test]
|
||||
[common-tests.logic.swap-and-reset-test]
|
||||
@ -135,6 +137,7 @@
|
||||
'common-tests.files-migrations-test
|
||||
'common-tests.files.comp-processors-test
|
||||
'common-tests.files.helpers-test
|
||||
'common-tests.files.repair-components-test
|
||||
'common-tests.files.repair-test
|
||||
'common-tests.files.shapes-builder-test
|
||||
'common-tests.files.tokens-test
|
||||
@ -179,6 +182,7 @@
|
||||
'common-tests.logic.composable-sync-test
|
||||
'common-tests.logic.copying-and-duplicating-test
|
||||
'common-tests.logic.duplicated-pages-test
|
||||
'common-tests.logic.library-asset-sync-test
|
||||
'common-tests.logic.move-shapes-test
|
||||
'common-tests.logic.multiple-nesting-levels-test
|
||||
'common-tests.logic.swap-and-reset-test
|
||||
|
||||
@ -367,7 +367,6 @@
|
||||
near-copy2-nested4-head (ctf/find-near-match file page-file libraries copy2-nested4-head)
|
||||
near-copy2-nested4-child (ctf/find-near-match file page-file libraries copy2-nested4-child)]
|
||||
|
||||
(thf/dump-file library1 :keys [:name :swap-slot-label] :show-refs? true)
|
||||
(t/is (some? main1-root))
|
||||
(t/is (some? main1-child))
|
||||
(t/is (some? main2-root))
|
||||
|
||||
@ -384,38 +384,37 @@
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 2)))))))
|
||||
done)))
|
||||
|
||||
;; FIXME: this test does not calls done consistently, so it break all the test suite
|
||||
#_(t/deftest copy-nested-3
|
||||
(t/async done
|
||||
(mock/with-mocks {uuid/next cthi/next-uuid}
|
||||
(fn [done']
|
||||
(let [;; ==== Setup
|
||||
file (setup-file)
|
||||
store (ths/setup-store file)
|
||||
(t/deftest copy-nested-3
|
||||
(t/async done
|
||||
(mock/with-mocks {uuid/next cthi/next-uuid}
|
||||
(fn [done']
|
||||
(let [;; ==== Setup
|
||||
file (setup-file)
|
||||
store (ths/setup-store file)
|
||||
|
||||
;; ==== Action
|
||||
;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3
|
||||
;; main and copy:
|
||||
;; - Duplicate it two times, moving the duplicates to another page
|
||||
events
|
||||
(concat
|
||||
(duplicate-copy-nested-and-group-out-of-the-main file :target-page-label :page-2)
|
||||
;; - Change color of Simple1
|
||||
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||
;; - Change color of the previous main
|
||||
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||
(set-color-bottom-shape :group-3 file {:color "#333333"}))]
|
||||
`(ths/run-store
|
||||
store done' events
|
||||
(fn [new-state]
|
||||
(let [file' (-> (ths/get-file-from-state new-state)
|
||||
(cthf/switch-to-page :page-2))]
|
||||
(cthf/validate-file! file')
|
||||
;; Check that it's NOT PROPAGATED.
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 10))
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 4))
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 0)))))))
|
||||
done)))
|
||||
;; ==== Action
|
||||
;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3
|
||||
;; main and copy:
|
||||
;; - Duplicate it two times, moving the duplicates to another page
|
||||
events
|
||||
(concat
|
||||
(duplicate-copy-nested-and-group-out-of-the-main file :target-page-label :page-2)
|
||||
;; - Change color of Simple1
|
||||
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||
;; - Change color of the previous main
|
||||
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||
(set-color-bottom-shape :group-3 file {:color "#333333"}))]
|
||||
(ths/run-store
|
||||
store done' events
|
||||
(fn [new-state]
|
||||
(let [file' (-> (ths/get-file-from-state new-state)
|
||||
(cthf/switch-to-page :page-2))]
|
||||
(cthf/validate-file! file')
|
||||
;; Check that it's NOT PROPAGATED.
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 10))
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 4))
|
||||
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 0)))))))
|
||||
done)))
|
||||
|
||||
(t/deftest duplicate-page-integrity-frame-group-component
|
||||
;; This test covers the bug fixed in 2.9.0: duplicating a page with a mainInstance inside a group
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user