From 9803cb8a472e1c73af307c2d3506845443121a73 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 9 Oct 2026 16:45:15 +0200 Subject: [PATCH] :sparkles: 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. --- .serena/memories/common/testing.md | 9 + .../files/comp_processors_test.cljc | 1 - .../files/repair_components_test.cljc | 365 ++++++++++++++++++ .../logic/comp_detach_with_nested_test.cljc | 1 - .../logic/library_asset_sync_test.cljc | 147 +++++++ .../logic/variants_switch_test.cljc | 29 -- common/test/common_tests/runner.cljc | 4 + .../common_tests/types/components_test.cljc | 1 - .../logic/copying_and_duplicating_test.cljs | 61 ++- 9 files changed, 555 insertions(+), 63 deletions(-) create mode 100644 common/test/common_tests/files/repair_components_test.cljc create mode 100644 common/test/common_tests/logic/library_asset_sync_test.cljc diff --git a/.serena/memories/common/testing.md b/.serena/memories/common/testing.md index 4eda8e871f..a9876e673f 100644 --- a/.serena/memories/common/testing.md +++ b/.serena/memories/common/testing.md @@ -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. diff --git a/common/test/common_tests/files/comp_processors_test.cljc b/common/test/common_tests/files/comp_processors_test.cljc index 6eb30ad82b..4c6590e376 100644 --- a/common/test/common_tests/files/comp_processors_test.cljc +++ b/common/test/common_tests/files/comp_processors_test.cljc @@ -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))))) diff --git a/common/test/common_tests/files/repair_components_test.cljc b/common/test/common_tests/files/repair_components_test.cljc new file mode 100644 index 0000000000..8a21495621 --- /dev/null +++ b/common/test/common_tests/files/repair_components_test.cljc @@ -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)))))) diff --git a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc index 3e06167e35..520955d42f 100644 --- a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc +++ b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc @@ -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))) diff --git a/common/test/common_tests/logic/library_asset_sync_test.cljc b/common/test/common_tests/logic/library_asset_sync_test.cljc new file mode 100644 index 0000000000..854cfcde09 --- /dev/null +++ b/common/test/common_tests/logic/library_asset_sync_test.cljc @@ -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))))) diff --git a/common/test/common_tests/logic/variants_switch_test.cljc b/common/test/common_tests/logic/variants_switch_test.cljc index 06e8b5ac34..de8fb76cb9 100644 --- a/common/test/common_tests/logic/variants_switch_test.cljc +++ b/common/test/common_tests/logic/variants_switch_test.cljc @@ -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 diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 79a74e14b2..fe84173113 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -23,6 +23,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] @@ -66,6 +67,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] @@ -128,6 +130,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 @@ -171,6 +174,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 diff --git a/common/test/common_tests/types/components_test.cljc b/common/test/common_tests/types/components_test.cljc index 4d42df92f8..3a3d30e87d 100644 --- a/common/test/common_tests/types/components_test.cljc +++ b/common/test/common_tests/types/components_test.cljc @@ -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)) diff --git a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs index 7939b903df..ca7811bea6 100644 --- a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs +++ b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs @@ -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