From 20062c003baa1c8793fcc4972c68691863c4f0d8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Sat, 10 Oct 2026 18:30:55 +0200 Subject: [PATCH] :sparkles: Test the file and container helpers and the library events The component helpers of types/file and types/container and the library events of the workspace had the lowest coverage left. - common-tests.types.file-test: the export detach, the reuse of the library page, the original ref of a swap, deleted components and used-in?. Pinned: F52 (a library copy nested in a local component loses its ref in every copy), F56 (the original ref of a swap is nil when the mains are on different pages) and F57 (library color refs inside :fills survive the export detach). - common-tests.types.container-test: the placement rules for moves and pastes (copies, mains, variant containers, deleted components, component loops) and small helper branches. A plain shape moved into its own child passes the rules (F58); relocate-shapes drops it first. - frontend-tests.data.workspace-libraries-test: the events on several shapes at once, each one undo entry; update of a library main; a blank rename; placing with an id ref. - workspace-library-updates-test: how a file learns that a library changed, and the sync dialog with its update and dismiss, with the RPC mocked. - workspace-library-links-test: linking, unlinking and sharing. --- common/test/common_tests/runner.cljc | 2 + .../common_tests/types/container_test.cljc | 216 +++++++++++++ common/test/common_tests/types/file_test.cljc | 293 ++++++++++++++++++ .../data/workspace_libraries_test.cljs | 220 +++++++++++++ .../data/workspace_library_links_test.cljs | 189 +++++++++++ .../data/workspace_library_updates_test.cljs | 203 ++++++++++++ frontend/test/frontend_tests/runner.cljs | 6 + 7 files changed, 1129 insertions(+) create mode 100644 common/test/common_tests/types/file_test.cljc create mode 100644 frontend/test/frontend_tests/data/workspace_libraries_test.cljs create mode 100644 frontend/test/frontend_tests/data/workspace_library_links_test.cljs create mode 100644 frontend/test/frontend_tests/data/workspace_library_updates_test.cljs diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index a952548d22..d8fd66fc5d 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -96,6 +96,7 @@ [common-tests.types.color-test] [common-tests.types.components-test] [common-tests.types.container-test] + [common-tests.types.file-test] [common-tests.types.fill-test] [common-tests.types.font-test] [common-tests.types.modifiers-test] @@ -203,6 +204,7 @@ 'common-tests.types.color-test 'common-tests.types.components-test 'common-tests.types.container-test + 'common-tests.types.file-test 'common-tests.types.fill-test 'common-tests.types.font-test 'common-tests.types.modifiers-test diff --git a/common/test/common_tests/types/container_test.cljc b/common/test/common_tests/types/container_test.cljc index e00b1fb9c6..55bc0e6459 100644 --- a/common/test/common_tests/types/container_test.cljc +++ b/common/test/common_tests/types/container_test.cljc @@ -6,11 +6,22 @@ (ns common-tests.types.container-test (:require + [app.common.geom.point :as gpt] + [app.common.test-helpers.components :as thc] + [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.test-helpers.variants :as thv] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] [app.common.types.container :as ctc] [app.common.types.shape :as cts] [app.common.uuid :as uuid] [clojure.test :as t])) +(t/use-fixtures :each thi/test-fixture) + ;; --------------------------------------------------------------------------- ;; Helpers ;; --------------------------------------------------------------------------- @@ -154,3 +165,208 @@ shape-b (make-shape id-b :parent-id id-a) objects (objects-map shape-a shape-b)] (t/is (nil? (ctc/find-component-main objects shape-a false)))))) + +;; --------------------------------------------------------------------------- +;; Placement rules: where shapes may go when moved or pasted +;; --------------------------------------------------------------------------- + +(defn- placement-file + "A board with a frame, a group and a rect inside; components :c1 and :c2, + a copy of :c1; and two variant containers, :v01 with mains :vm1 and :vm2, + :v02 with mains :vm3 and :vm4." + [] + (-> (thf/sample-file :file) + (tho/add-frame :board) + (tho/add-frame :inner :parent-label :board) + (tho/add-group :group :parent-label :board) + (tho/add-rect :rect :parent-label :board) + (tho/add-simple-component :c1 :main1 :main1-child) + (tho/add-simple-component :c2 :main2 :main2-child) + (thc/instantiate-component :c1 :copy1 :children-labels [:copy1-child]) + (thv/add-variant :v01 :vc1 :vm1 :vc2 :vm2) + (thv/add-variant :v02 :vc3 :vm3 :vc4 :vm4))) + +(defn- page-objects + [file] + (:objects (thf/current-page file))) + +(defn- label + [id] + (if (= id uuid/zero) :root (thi/label id))) + +(defn- place + "The labels of the parent and the frame that the shapes `labels` go to + when moved, or pasted with `pasting?`, into `target`." + [file target labels & {:keys [pasting? libraries objects]}] + (let [objects (or objects (page-objects file)) + children (map #(ths/get-shape file %) labels)] + (mapv label (ctc/find-valid-parent-and-frame-ids + (thi/id target) objects children pasting? + (or libraries {(:id file) file}))))) + +(defn- delete-component + [file component-label] + (assoc-in file [:data :components (thi/id component-label) :deleted] true)) + +(t/deftest placement-of-plain-shapes + (let [file (placement-file)] + (t/is (= [:board :board] (place file :board [:main1]))) + (t/testing "inside a group the frame is the group's frame" + (t/is (= [:group :board] (place file :group [:rect])))) + (t/testing "the three-argument arity moves, without libraries" + (t/is (= [:inner :inner] + (mapv label (ctc/find-valid-parent-and-frame-ids + (thi/id :inner) (page-objects file) + [(ths/get-shape file :rect)]))))))) + +(t/deftest placement-inside-a-copy + (let [file (placement-file)] + (t/testing "nothing goes into a copy or a shape inside it" + (t/is (= [:root :root] (place file :copy1 [:rect]))) + (t/is (= [:root :root] (place file :copy1-child [:rect])))) + (t/testing "a shape may stay in the copy it already is in" + (t/is (= [:copy1 :copy1] (place file :copy1 [:copy1-child])))) + (t/testing "but a pasted one is a new shape" + (t/is (= [:root :root] (place file :copy1 [:copy1-child] :pasting? true)))))) + +(t/deftest placement-of-a-main + (let [file (placement-file)] + (t/testing "a main is not moved into another main, nor inside one" + (t/is (= [:root :root] (place file :main2 [:main1]))) + (t/is (= [:root :root] (place file :main2-child [:main1])))) + (t/testing "a plain shape may go into a main" + (t/is (= [:main2 :main2] (place file :main2 [:rect])))) + (t/testing "a pasted main becomes a copy, so it may go into a main" + (t/is (= [:main2 :main2] (place file :main2 [:main1] :pasting? true)))) + (t/testing "unless its component is deleted: then it is pasted as a main" + (let [libraries {(:id file) (delete-component file :c1)}] + (t/is (= [:root :root] (place file :main2 [:main1] :pasting? true :libraries libraries))))) + (t/testing "a cut main, no longer in the page, follows the same rule" + (let [objects (dissoc (page-objects file) (thi/id :main1) (thi/id :main1-child)) + libraries {(:id file) (delete-component file :c1)}] + (t/is (= [:main2 :main2] (place file :main2 [:main1] :pasting? true :objects objects))) + (t/is (= [:root :root] (place file :main2 [:main1] :pasting? true + :objects objects :libraries libraries))))))) + +(t/deftest placement-of-a-variant-container + (let [file (placement-file)] + (t/is (= [:board :board] (place file :board [:v01]))) + (t/testing "not inside another variant container" + (t/is (= [:root :root] (place file :v02 [:v01])))) + (t/testing "not inside a main, even pasted" + (t/is (= [:root :root] (place file :main2 [:v01] :pasting? true)))))) + +(t/deftest placement-inside-a-variant-container + (let [file (placement-file)] + (t/testing "only mains go into a variant container" + (t/is (= [:root :root] (place file :v01 [:rect]))) + (t/is (= [:v01 :v01] (place file :v01 [:main1])))) + (t/testing "a pasted variant does not go inside a sibling variant" + (t/is (= [:v01 :v01] (place file :vm2 [:vm1] :pasting? true)))) + (t/testing "but it may go inside a variant of another container" + (t/is (= [:vm3 :vm3] (place file :vm3 [:vm1] :pasting? true)))))) + +(t/deftest placement-that-makes-a-component-loop + (let [file (placement-file)] + (t/testing "a copy does not go inside its own main" + (t/is (= [:root :root] (place file :main1 [:copy1] :pasting? true))) + (t/is (= [:main2 :main2] (place file :main2 [:copy1] :pasting? true)))) + (t/testing "a main does not go inside itself, even pasted" + (t/is (= [:root :root] (place file :main1-child [:main1]))) + (t/is (= [:root :root] (place file :main1-child [:main1] :pasting? true)))) + (t/testing "a plain shape inside its own child is let through (`relocate-shapes` drops it)" + (t/is (= [:inner :inner] (place file :inner [:board])))))) + +(t/deftest placement-with-a-validation-cache + (let [file (placement-file) + objects (page-objects file) + children [(ths/get-shape file :main1)] + libs {(:id file) file} + cache (ctc/parent-validation-cache objects children libs)] + (t/is (= [:root :root] + (mapv label (ctc/find-valid-parent-and-frame-ids + (thi/id :main2) objects children false libs cache)))) + (t/is (= [:board :board] + (mapv label (ctc/find-valid-parent-and-frame-ids + (thi/id :board) objects children false libs cache)))))) + +(t/deftest valid-shape-for-component + (let [file (placement-file) + objects (page-objects file) + valid? #(ctc/valid-shape-for-component? objects (ths/get-shape file %))] + (t/is (valid? :rect)) + (t/is (valid? :board)) + (t/testing "a main can't be inside another main" + (t/is (not (valid? :main1-child)))) + (t/testing "a main can't hold another main" + (t/is (not (valid? :v01)))) + (t/testing "a main can't be inside a copy" + (t/is (not (valid? :copy1-child)))))) + +(t/deftest first-valid-parent + (let [file (placement-file) + objects (page-objects file) + first-valid #(label (:id (ctc/get-first-valid-parent objects (thi/id %))))] + (t/is (= :rect (first-valid :rect))) + (t/is (= :vm1 (first-valid :vm1))) + (t/testing "copies and variant containers are skipped" + (t/is (= :root (first-valid :copy1-child))) + (t/is (= :root (first-valid :v01)))))) + +;; --------------------------------------------------------------------------- +;; Heads, containers and instances +;; --------------------------------------------------------------------------- + +(t/deftest head-shape-of-nothing-and-of-the-root + (let [objects (page-objects (placement-file))] + (t/is (nil? (ctc/get-head-shape objects nil))) + (t/is (nil? (ctc/get-head-shape objects (get objects uuid/zero)))) + (t/is (= [] (ctc/get-child-heads objects (uuid/next)))))) + +(t/deftest children-in-instance-stop-at-a-nested-copy + (let [file (-> (thf/sample-file :file) + (tho/add-nested-component :c1 :main1 :main1-child + :c2 :main2 :nested-head)) + objects (page-objects file) + labels #(set (map (comp label :id) (ctc/get-children-in-instance objects (thi/id %))))] + (t/is (= #{:main1 :main1-child} (labels :main1))) + (t/is (= #{:main2} (labels :main2))))) + +(t/deftest container-of-a-component + (let [file (placement-file) + container (ctc/get-container (:data file) :component (thi/id :c1))] + (t/is (ctc/component? container)) + (t/is (not (ctc/page? container))) + (t/is (= (thi/id :main1) (:main-instance-id container))))) + +(t/deftest container-root + (let [root-id (uuid/next) + root (assoc (make-shape root-id) :parent-id nil) + child (make-shape (uuid/next) :parent-id root-id)] + (t/is (= root (ctc/get-container-root {:objects (objects-map root child)}))))) + +(t/deftest swap-keep-attrs-keep-auto-sizing-of-a-layout + (let [shape (make-shape (uuid/next) + :type :frame + :layout :flex + :layout-item-h-sizing :auto + :layout-item-v-sizing :auto + :layout-item-margin {:m1 10}) + fixed (assoc shape + :layout-item-h-sizing :fix + :layout-item-v-sizing :fix)] + (t/is (= [:auto :auto nil] + ((juxt :layout-item-h-sizing :layout-item-v-sizing :layout-item-margin) + (ctc/remove-swap-keep-attrs shape)))) + (t/is (= [nil nil] + ((juxt :layout-item-h-sizing :layout-item-v-sizing) + (ctc/remove-swap-keep-attrs fixed)))))) + +(t/deftest component-instance-with-default-options + (let [file (placement-file) + component (ctkl/get-component (:data file) (thi/id :c1)) + [root shapes] (ctc/make-component-instance (thf/current-page file) component + (:data file) (gpt/point 0 0))] + (t/is (= (thi/id :c1) (:component-id root))) + (t/is (not (ctk/main-instance? root))) + (t/is (= 2 (count shapes))))) diff --git a/common/test/common_tests/types/file_test.cljc b/common/test/common_tests/types/file_test.cljc new file mode 100644 index 0000000000..4608111a09 --- /dev/null +++ b/common/test/common_tests/types/file_test.cljc @@ -0,0 +1,293 @@ +;; 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.types.file-test + "Component helpers of `app.common.types.file`: the export detach of + external references, the library page that absorbed components go to, + the original ref of a swapped shape, and the lookups on deleted + components." + (:require + [app.common.files.validate :as cfv] + [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.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape-tree :as ctst] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(def ^:private link-attrs + [:component-id :component-file :shape-ref :component-root]) + +(defn- links + [file label] + (select-keys (ths/get-shape file label) link-attrs)) + +(defn- validation-errors + [file] + (set (map :code (cfv/validate-file file {})))) + +(defn- library-with-component + [] + (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :lib-component :lib-main :lib-main-child))) + +;; ----- Export detach + +(t/deftest detach-external-references-drops-library-links + (let [library (library-with-component) + file (-> (thf/sample-file :file) + (tho/add-simple-component-with-copy :component :main :main-child + :copy + :copy-root-params {:children-labels [:copy-child]}) + (thc/instantiate-component :lib-component :lib-copy + :library library + :children-labels [:lib-copy-child])) + file' (ctf/detach-external-references file (:id file))] + (t/testing "a copy of a library component becomes plain shapes" + (t/is (= {} (links file' :lib-copy))) + (t/is (= {} (links file' :lib-copy-child)))) + (t/testing "a copy of a local component keeps its links" + (t/is (= (links file :copy) (links file' :copy))) + (t/is (= (links file :copy-child) (links file' :copy-child)))) + (t/is (= #{} (validation-errors file'))))) + +(t/deftest detach-external-references-keeps-a-nested-library-copy-in-place + (let [library (library-with-component) + file (-> (thf/sample-file :file) + (tho/add-frame :main {:name "Frame2"}) + (thc/instantiate-component :lib-component :nested-head + :library library + :parent-label :main) + (thc/make-component :component :main) + (thc/instantiate-component :component :copy + :children-labels [:copy-nested-head])) + file' (ctf/detach-external-references file (:id file))] + (t/testing "the nested library copy in the main becomes plain shapes" + (t/is (= {} (links file' :nested-head)))) + (t/testing "its copy keeps the ref to the main's shape, which is local" + (tm/known-failure "F52" + {:shape-ref (thi/id :nested-head)} + {} + (links file' :copy-nested-head))) + (t/testing "the validation does not see the lost ref" + (t/is (= #{} (validation-errors file')))))) + +;; Fill and typography refs on the shape and on the text nodes themselves +;; (the older format), next to a fill ref inside `:fills`. +(defn- file-with-color-refs + [library-id] + (let [file (thf/sample-file :file)] + (-> file + (ths/add-sample-shape :rect + :type :rect + :fill-color-ref-id (thi/id :color) + :fill-color-ref-file library-id + :stroke-color-ref-id (thi/id :color) + :stroke-color-ref-file library-id + :fills [{:fill-color "#abcdef" + :fill-opacity 1 + :fill-color-ref-id (thi/id :color) + :fill-color-ref-file library-id}]) + (ths/add-sample-shape :text + :type :text + :content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :children [{:text "library" + :fill-color-ref-id (thi/id :color) + :fill-color-ref-file library-id + :typography-ref-id (thi/id :typography) + :typography-ref-file library-id} + {:text "local" + :typography-ref-id (thi/id :typography) + :typography-ref-file (:id file)}]}]}]})))) + +(defn- text-leaves + [file] + (->> (get-in (ths/get-shape file :text) [:content :children 0 :children 0 :children]) + (mapv #(select-keys % [:fill-color-ref-file :typography-ref-file])))) + +(t/deftest detach-external-references-drops-library-color-refs + (let [library-id (thi/new-id! :library) + file (file-with-color-refs library-id) + file' (ctf/detach-external-references file (:id file)) + rect' (ths/get-shape file' :rect)] + (t/is (= {} (select-keys rect' [:fill-color-ref-file :stroke-color-ref-file]))) + (t/is (= [{} {:typography-ref-file (:id file)}] (text-leaves file'))) + (t/testing "a fill ref inside `:fills`" + (tm/known-failure "F57" nil library-id (-> rect' :fills first :fill-color-ref-file))))) + +;; ----- Asset usage + +(t/deftest component-used-in-a-file + (let [library (library-with-component) + file (-> (thf/sample-file :file) + (thc/instantiate-component :lib-component :lib-copy :library library)) + unused (-> (thf/sample-file :other) + (tho/add-rect :rect)) + lib-c (thc/get-component library :lib-component)] + (t/is (ctf/used-in? (:data file) (:id library) lib-c :component)) + (t/is (not (ctf/used-in? (:data unused) (:id library) lib-c :component))))) + +;; ----- Library page for absorbed components + +(t/deftest absorbed-components-reuse-the-library-page + (let [library1 (-> (thf/sample-file :library1 :is-shared true) + (tho/add-simple-component :component1 :main1 :main1-child)) + library2 (-> (thf/sample-file :library2 :is-shared true) + (tho/add-simple-component :component2 :main2 :main2-child)) + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy1 :library library1) + (thc/instantiate-component :component2 :copy2 :library library2)) + file' (ctf/update-file-data file + #(-> % + (ctf/absorb-assets (:data library1)) + (ctf/absorb-assets (:data library2)))) + data' (ctf/file-data file') + pages' (ctpl/pages-seq data') + roots (->> (ctpl/get-page data' (:id (second pages'))) + (ctn/shapes-seq) + (filter ctk/main-instance?) + (sort-by :y))] + (t/is (= ["Page 1" "Main components"] (map :name pages'))) + (t/is (= 2 (count roots))) + (t/testing "the second component goes below the first, a grid gap apart" + (let [[first-root second-root] roots] + (t/is (<= (+ (:y first-root) (:height first-root) 50) (:y second-root))))) + (t/is (= #{} (validation-errors file'))))) + +;; ----- Original ref of a swapped shape + +(defn- swap-at-second-level + "Swaps, in a copy of :c3, the copy of :c1 nested two levels down + (:c3 > :c2 > :c1) for :c4. Returns the file with the new shape as + :swapped." + [file] + (let [copy-c2 (first (:shapes (ths/get-shape file :copy3))) + copy-c1 (first (:shapes (ths/get-shape-by-id file copy-c2)))] + (thi/set-id! :copy-c1 copy-c1) + (-> file + (tho/add-simple-component :c4 :main4 :main4-child) + (tho/swap-component-in-shape :copy-c1 :c4 :new-shape-label :swapped)))) + +(defn- ref-id-for-swapped + [file] + (ctf/find-ref-id-for-swapped (ths/get-shape file :swapped) + (thf/current-page file) + {(:id file) file})) + +(t/deftest ref-id-for-swapped-goes-up-the-ref-chain + (let [file (-> (thf/sample-file :file) + (tho/add-two-levels-nested-component-with-copy + :c1 :main1 :main1-child + :c2 :main2 :nested-head1 + :c3 :main3 :nested-head2 :nested-subhead2 + :copy3) + (swap-at-second-level))] + (t/is (= (thi/id :nested-head1) (ctk/get-swap-slot (ths/get-shape file :swapped)))) + (t/is (= (thi/id :nested-subhead2) (ref-id-for-swapped file))))) + +(t/deftest ref-id-for-swapped-with-mains-on-other-pages + (let [file (-> (thf/sample-file :file :page-label :page1) + (tho/add-nested-component :c1 :main1 :main1-child + :c2 :main2 :nested-head1) + (thf/add-sample-page :page2) + (tho/add-frame :main3 {:name "Frame3"}) + (thc/instantiate-component :c2 :nested-head2 + :parent-label :main3 + :children-labels [:nested-subhead2]) + (thc/make-component :c3 :main3) + (thc/instantiate-component :c3 :copy3) + (swap-at-second-level))] + (t/is (= (thi/id :nested-head1) (ctk/get-swap-slot (ths/get-shape file :swapped)))) + (tm/known-failure "F56" (thi/id :nested-subhead2) nil (ref-id-for-swapped file)))) + +;; ----- Deleted components + +(defn- file-with-deleted-component + [] + (let [file (-> (thf/sample-file :file) + (tho/add-simple-component :component :main :main-child))] + (ctf/update-file-data file #(ctf/delete-component % (thi/id :component) false nil)))) + +(t/deftest lookups-on-a-deleted-component + (let [file (file-with-deleted-component) + data (ctf/file-data file) + component (thc/get-component file :component :include-deleted? true)] + (t/is (:deleted component)) + (t/is (= (thi/id :main) (:id (ctf/get-component-root data component)))) + (t/is (= #{(thi/id :main) (thi/id :main-child)} + (set (map :id (ctf/get-component-shapes data component))))) + (t/testing "a shape of the component, with the component as its container" + (let [child (ctf/get-component-shape data component (thi/id :main-child) + :with-context? true)] + (t/is (= (thi/id :main-child) (:id child))) + (t/is (= :component (:type (:container (meta child))))))) + (t/testing "loading the objects again leaves the component as it is" + (t/is (= component (ctf/load-component-objects data component)))))) + +;; ----- Containers and ref chains + +(t/deftest update-container-of-a-component + (let [file (file-with-deleted-component) + component (thc/get-component file :component :include-deleted? true) + container (ctn/make-container component :component) + data' (ctf/update-container (ctf/file-data file) container + #(assoc % :name "Renamed"))] + (t/is (= "Renamed" (:name (thc/get-component {:data data'} :component + :include-deleted? true)))))) + +(t/deftest update-objects-tree-updates-and-removes + (let [file (-> (thf/sample-file :file) + (tho/add-frame-with-child :frame :child) + (tho/add-rect :rect)) + container (ctn/make-container (thf/current-page file) :page) + result (ctf/update-objects-tree + container + (fn [shape] + (condp = (:id shape) + (thi/id :frame) {:result :update + :updated-shape (assoc shape :name "Updated")} + (thi/id :rect) {:result :remove} + {:result :keep})))] + (t/is (= "Updated" (:name (ctst/get-shape result (thi/id :frame))))) + (t/is (some? (ctst/get-shape result (thi/id :child)))) + (t/is (nil? (ctst/get-shape result (thi/id :rect)))) + (t/testing "an unknown result" + (t/is (thrown? #?(:clj Exception :cljs :default) + (ctf/update-objects-tree container (constantly {:result :other}))))))) + +(t/deftest ref-chain-without-the-target + (let [file (-> (thf/sample-file :file) + (tho/add-simple-component-with-copy :component :main :main-child + :copy + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + libraries {(:id file) file} + chain (ctf/get-ref-chain-until-target-ref page libraries + (ths/get-shape file :copy-child) + (ths/get-shape file :copy))] + (t/testing "the chain runs to the main and stops there" + (t/is (= [(thi/id :copy-child) (thi/id :main-child)] (map :id chain)))))) + +(t/deftest near-match-with-context + (let [file (-> (thf/sample-file :file) + (tho/add-simple-component-with-copy :component :main :main-child + :copy + :copy-root-params {:children-labels [:copy-child]})) + match (ctf/find-near-match file (thf/current-page file) {(:id file) file} + (ths/get-shape file :copy-child) + :with-context? true)] + (t/is (= (thi/id :main-child) (:id match))) + (t/is (some? (:container (meta match)))))) diff --git a/frontend/test/frontend_tests/data/workspace_libraries_test.cljs b/frontend/test/frontend_tests/data/workspace_libraries_test.cljs new file mode 100644 index 0000000000..f5a62fbcf7 --- /dev/null +++ b/frontend/test/frontend_tests/data/workspace_libraries_test.cljs @@ -0,0 +1,220 @@ +;; 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 frontend-tests.data.workspace-libraries-test + "The library events that act on several shapes or components at once, each + as one undo entry, and the branches of the single-shape events that the + composable cases do not reach: creating or placing a component with an + id ref, a blank rename and updating a main that lives in a linked + library." + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.test-helpers.components :as cthc] + [app.common.test-helpers.composable.core :as tm] + [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.types.components-list :as ctkl] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] + [app.main.data.workspace.undo :as dwu] + [app.main.store :as st] + [cljs.test :as t :include-macros true] + [frontend-tests.composable-tests.interpreter :as ftm])) + +(t/use-fixtures :each ftm/fixture) + +(def ^:private main-color "#111111") +(def ^:private override "#222222") + +(defn- two-components + "Components :c1 and :c2, two copies of :c1 (:copy-a, :copy-b) and two plain + rects." + [] + (-> (cthf/sample-file :file1) + (ctho/add-simple-component :c1 :main1 :main1-child + :child-params {:fills (cths/sample-fills-color :fill-color main-color)}) + (ctho/add-simple-component :c2 :main2 :main2-child) + (cthc/instantiate-component :c1 :copy-a :children-labels [:copy-a-child]) + (cthc/instantiate-component :c1 :copy-b :children-labels [:copy-b-child]) + (ctho/add-rect :rect-a) + (ctho/add-rect :rect-b))) + +(defn- install-file! + [file] + (ftm/install! (tm/make-situation file))) + +(defn- shape + [label] + (cths/get-shape (ftm/current-file) label)) + +(defn- shape-by-id + [id] + (cths/get-shape-by-id (ftm/current-file) id)) + +(defn- data + [] + (:data (ftm/current-file))) + +(defn- undo-entries + [] + (count (get-in @st/state [:workspace-undo :items]))) + +(defn- step + "Run `events` and wait until their work drains." + [& events] + (ftm/await-step (vec events))) + +(defn- select + [& labels] + (dws/select-shapes (into (d/ordered-set) (map cthi/id) labels))) + +(defn- linked? + [label] + (some? (:component-id (shape label)))) + +(defn- fill + [label] + (-> (shape label) :fills first :fill-color)) + +(defn- set-fill + [& labels] + (dwsh/update-shapes (into #{} (map cthi/id) labels) + #(assoc % :fills (cths/sample-fills-color :fill-color override)))) + +(defn- component-count + [] + (count (ctkl/components-seq (data)))) + +(t/deftest ^:async detach-components-is-one-undo-entry + (install-file! (two-components)) + (let [before (undo-entries)] + (t/is (= :settled (await (step (dwl/detach-components [(cthi/id :copy-a) (cthi/id :copy-b)]))))) + (t/is (not (linked? :copy-a))) + (t/is (not (linked? :copy-b))) + (t/is (= (inc before) (undo-entries))) + (await (step dwu/undo)) + (t/is (linked? :copy-a)) + (t/is (linked? :copy-b)))) + +(t/deftest ^:async detach-selected-components + (install-file! (two-components)) + (t/testing "nothing happens when a selected shape is inside a copy" + (await (step (select :copy-a :copy-b-child) dwl/detach-selected-components)) + (t/is (linked? :copy-a)) + (t/is (some? (:shape-ref (shape :copy-b-child))))) + (t/testing "the selected copies are detached" + (await (step (select :copy-a :copy-b) dwl/detach-selected-components)) + (t/is (not (linked? :copy-a))) + (t/is (not (linked? :copy-b))))) + +(t/deftest ^:async reset-components-is-one-undo-entry + (install-file! (two-components)) + (await (step (set-fill :copy-a-child :copy-b-child))) + (t/is (= override (fill :copy-a-child))) + (let [before (undo-entries)] + (await (step (dwl/reset-components [(cthi/id :copy-a) (cthi/id :copy-b)]))) + (t/is (= main-color (fill :copy-a-child))) + (t/is (= main-color (fill :copy-b-child))) + (t/is (= (inc before) (undo-entries))) + (await (step dwu/undo)) + (t/is (= override (fill :copy-a-child))) + (t/is (= override (fill :copy-b-child))))) + +(defn- copies-of + [component-label] + (->> (vals (:objects (cthf/current-page (ftm/current-file)))) + (filter #(and (ctk/instance-head? %) + (not (ctk/main-instance? %)) + (= (cthi/id component-label) (:component-id %)))))) + +(t/deftest ^:async multi-swap-is-one-undo-entry + (install-file! (two-components)) + (let [before (undo-entries)] + (await (step (dwl/component-multi-swap [(shape :copy-a) (shape :copy-b)] + (:id (ftm/current-file)) + (cthi/id :c2)))) + (t/is (= 0 (count (copies-of :c1)))) + (t/is (= 2 (count (copies-of :c2)))) + (t/is (= (inc before) (undo-entries))) + (await (step dwu/undo)) + (t/is (= 2 (count (copies-of :c1)))) + (t/is (= 0 (count (copies-of :c2)))))) + +(t/deftest ^:async add-multiple-components-is-one-undo-entry + (install-file! (two-components)) + (let [before (undo-entries)] + (await (step (select :rect-a :rect-b) (dwl/add-multiple-components))) + (t/is (= 4 (component-count))) + (t/is (= (inc before) (undo-entries))) + (await (step dwu/undo)) + (t/is (= 2 (component-count))))) + +(t/deftest ^:async add-multiple-components-skips-copies + (install-file! (two-components)) + (await (step (select :rect-a :copy-a-child) (dwl/add-multiple-components))) + (t/is (= 2 (component-count)))) + +(t/deftest ^:async add-component-sets-the-id-ref + (install-file! (two-components)) + (let [id-ref (atom nil)] + (await (step (dwl/add-component id-ref [(cthi/id :rect-a)]))) + (t/is (some? (ctkl/get-component (data) @id-ref))))) + +(t/deftest ^:async duplicate-component-makes-a-new-main + (install-file! (two-components)) + (await (step (dwl/duplicate-component (:id (ftm/current-file)) (cthi/id :c1)))) + (let [new-component (->> (ctkl/components-seq (data)) + (remove #(#{(cthi/id :c1) (cthi/id :c2)} (:id %))) + first)] + (t/is (= 3 (component-count))) + (t/is (some? (shape-by-id (:main-instance-id new-component)))))) + +(t/deftest ^:async restore-components-is-one-undo-entry + (install-file! (two-components)) + (let [file-id (:id (ftm/current-file))] + (await (step (dwl/delete-component {:id (cthi/id :c1)}) + (dwl/delete-component {:id (cthi/id :c2)}))) + (t/is (= 0 (component-count))) + (let [before (undo-entries)] + (await (step (dwl/restore-components {(cthi/id :c1) file-id + (cthi/id :c2) file-id}))) + (t/is (= 2 (component-count))) + (t/is (= (inc before) (undo-entries)))))) + +(t/deftest ^:async update-component-of-a-library-main + (let [library (-> (cthf/sample-file :library :is-shared true) + (ctho/add-simple-component :lib-c :lib-main :lib-main-child)) + file (-> (cthf/sample-file :file1) + (cthc/instantiate-component :lib-c :copy + :library library + :children-labels [:copy-child]))] + (ftm/install! (-> (tm/make-situation file) + (tm/with-aux-files {(:id library) library}))) + (await (step (set-fill :copy-child))) + (await (step (dwl/update-component (cthi/id :copy)))) + (let [library' (get-in @st/state [:files (:id library)])] + (t/is (= override (-> (cths/get-shape library' :lib-main-child) :fills first :fill-color)))) + (t/testing "a shape that is not a head is left as it is" + (await (step (dwl/update-component (cthi/id :copy-child)))) + (t/is (= override (fill :copy-child)))))) + +(t/deftest ^:async rename-component-to-a-blank-name + (install-file! (two-components)) + (let [before (:name (ctkl/get-component (data) (cthi/id :c1)))] + (await (step (dwl/rename-component (cthi/id :c1) " "))) + (t/is (= before (:name (ctkl/get-component (data) (cthi/id :c1))))))) + +(t/deftest ^:async instantiate-component-sets-the-id-ref + (install-file! (two-components)) + (let [id-ref (atom nil)] + (await (step (dwl/instantiate-component (:id (ftm/current-file)) (cthi/id :c2) + (gpt/point 0 0) {:id-ref id-ref :origin "plugin"}))) + (t/is (= (cthi/id :c2) (:component-id (shape-by-id @id-ref)))))) diff --git a/frontend/test/frontend_tests/data/workspace_library_links_test.cljs b/frontend/test/frontend_tests/data/workspace_library_links_test.cljs new file mode 100644 index 0000000000..876fa0860f --- /dev/null +++ b/frontend/test/frontend_tests/data/workspace_library_links_test.cljs @@ -0,0 +1,189 @@ +;; 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 frontend-tests.data.workspace-library-links-test + "Linking a library to the file and unlinking it, with the libraries it + brings along, and publishing the file as a shared library. The server is + mocked." + (:require + [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.variants :as cthv] + [app.main.data.workspace.libraries :as dwl] + [app.main.repo :as rp] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock] + [frontend-tests.helpers.state :as ths] + [potok.v2.core :as ptk])) + +(t/use-fixtures :each {:before cthi/reset-idmap!}) + +(defn- rpc + "A mock of `rp/cmd!` that answers each command from `responses` + (command -> value, or fn of the params returning an observable), and + records the calls in `calls`." + [calls responses] + (mock/stub + (fn [cmd params & _] + (swap! calls conj [cmd params]) + (let [response (get responses cmd)] + (if (fn? response) + (response params) + (rx/of response)))))) + +(defn- commands + [calls] + (set (map first @calls))) + +(defn- merge-state + "An event that merges `m` into the state, for what the workspace loads + before these events run." + [m] + (ptk/reify ::merge-state + ptk/UpdateEvent + (update [_ state] + (merge-with merge state m)))) + +(defn- files + [] + {:file (cthf/sample-file :file1) + :library (-> (cthf/sample-file :library :is-shared true) + (ctho/add-simple-component :lib-c :lib-main :lib-main-child)) + :dependency (-> (cthf/sample-file :dependency :is-shared true) + (ctho/add-simple-component :dep-c :dep-main :dep-main-child))}) + +(defn- linked-to + [state file-id] + (->> (vals (:files state)) + (filter #(= file-id (:library-of %))) + (map :id) + set)) + +(t/deftest link-loads-the-library-and-its-dependencies + (t/async + done + (let [{:keys [file library dependency]} (files) + by-id (into {} (map (juxt :id identity)) [library dependency]) + calls (atom [])] + (mock/with-mocks + {rp/cmd! (rpc calls {:link-file-to-library [(:id dependency)] + :get-file #(rx/of (get by-id (:id %))) + :get-file-object-thumbnails {"thumbnail-id" "thumbnail-uri"} + :get-library-usage {:used-in 3}})} + (fn [inner-done] + (ths/run-store + (ths/setup-store file) inner-done + [(merge-state {:shared-files {(:id library) {:library-summary {:variants [{} {}]}}}}) + (dwl/link-file-to-library (:id file) (:id library))] + (fn [state] + (t/is (= #{(:id library) (:id dependency)} (linked-to state (:id file)))) + (t/is (= "thumbnail-uri" (get-in state [:thumbnails "thumbnail-id" :uri]))) + (t/testing "a library with variants asks how much it is used" + (t/is (contains? (commands calls) :get-library-usage)))))) + done)))) + +(t/deftest link-skips-libraries-already-loaded + (t/async + done + (let [{:keys [file library dependency]} (files) + calls (atom [])] + (mock/with-mocks + {rp/cmd! (rpc calls {:link-file-to-library [(:id dependency)] + :get-file #(rx/of library) + :get-file-object-thumbnails {}})} + (fn [inner-done] + (ths/run-store + (ths/setup-store file {:libraries [dependency]}) inner-done + [(dwl/link-file-to-library (:id file) (:id library))] + (fn [_] + (t/is (= [(:id library)] + (->> @calls + (filter #(= :get-file (first %))) + (map (comp :id second))))) + (t/is (not (contains? (commands calls) :get-library-usage)))))) + done)))) + +(t/deftest link-that-would-make-a-loop + (t/async + done + (let [{:keys [file library]} (files) + calls (atom [])] + (mock/with-mocks + {rp/cmd! (rpc calls {:link-file-to-library + (fn [_] (rx/throw (ex-info "loop" {:code :circular-library-reference})))})} + (fn [inner-done] + (ths/run-store + (ths/setup-store file) inner-done + [(dwl/link-file-to-library (:id file) (:id library))] + (fn [state] + (t/is (= :error (get-in state [:notification :level]))) + (t/is (empty? (linked-to state (:id file))))))) + done)))) + +(t/deftest unlink-drops-the-library-and-what-it-brought + (t/async + done + (let [{:keys [file library dependency]} (files) + calls (atom [])] + (mock/with-mocks + {rp/cmd! (rpc calls {:unlink-file-from-library nil + :get-file-libraries []})} + (fn [inner-done] + (ths/run-store + (ths/setup-store file) inner-done + [(dwl/libraries-fetched (:id file) [library dependency]) + (dwl/unlink-file-from-library (:id file) (:id library))] + (fn [state] + (t/is (empty? (linked-to state (:id file)))) + (t/is (some? (get-in state [:files (:id file)])))))) + done)))) + +(t/deftest cleanup-keeps-the-libraries-still-linked + (t/async + done + (let [{:keys [file library dependency]} (files)] + (ths/run-store + (ths/setup-store file) done + [(dwl/libraries-fetched (:id file) [library dependency]) + (dwl/cleanup-unlinked-libraries (:id file) [library])] + (fn [state] + (t/is (= #{(:id library)} (linked-to state (:id file))))))))) + +(defn- share + [file shared? check] + (fn [done] + (let [calls (atom [])] + (mock/with-mocks + {rp/cmd! (rpc calls {:set-file-shared nil})} + (fn [inner-done] + (ths/run-store + (ths/setup-store file) inner-done + [(dwl/set-file-shared (:id file) shared?)] + #(check % @calls))) + done)))) + +(t/deftest share-a-file-with-variants + (t/async + done + (let [file (-> (cthf/sample-file :file1) + (cthv/add-variant :v01 :c01 :m01 :c02 :m02))] + ((share file true + (fn [state calls] + (t/is (true? (get-in state [:files (:id file) :is-shared]))) + (t/is (= [[:set-file-shared {:id (:id file) :is-shared true}]] calls)))) + done)))) + +(t/deftest unshare-a-file + (t/async + done + (let [file (cthf/sample-file :file1)] + ((share file false + (fn [state calls] + (t/is (false? (get-in state [:files (:id file) :is-shared]))) + (t/is (= [[:set-file-shared {:id (:id file) :is-shared false}]] calls)))) + done)))) diff --git a/frontend/test/frontend_tests/data/workspace_library_updates_test.cljs b/frontend/test/frontend_tests/data/workspace_library_updates_test.cljs new file mode 100644 index 0000000000..96c22d96a2 --- /dev/null +++ b/frontend/test/frontend_tests/data/workspace_library_updates_test.cljs @@ -0,0 +1,203 @@ +;; 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 frontend-tests.data.workspace-library-updates-test + "How a file learns that a linked library changed and offers to sync: the + library change pushed by the server, the check of which libraries need a + sync, the sync dialog and its update and dismiss actions, and the sync + itself from an external library." + (:require + [app.common.test-helpers.components :as cthc] + [app.common.test-helpers.composable.core :as tm] + [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.file :as ctf] + [app.main.data.workspace.libraries :as dwl] + [app.main.repo :as rp] + [app.main.store :as st] + [cljs.test :as t :include-macros true] + [frontend-tests.composable-tests.interpreter :as ftm] + [frontend-tests.helpers.mock :as mock] + [potok.v2.core :as ptk])) + +(t/use-fixtures :each ftm/fixture) + +(def ^:private old-color "#111111") +(def ^:private new-color "#222222") + +(def ^:private synced-at (js/Date. 1000)) +(def ^:private component-changed-at (js/Date. 2000)) +(def ^:private library-changed-at (js/Date. 3000)) + +(defn- library-and-file + "A library whose component :lib-c changed after the file last synced + with it, and a file with a copy of :lib-c. With `changed?`, the main + child got a fill after the copy was made, so a sync changes the copy." + [changed?] + (let [library (-> (cthf/sample-file :library :is-shared true) + (ctho/add-simple-component :lib-c :lib-main :lib-main-child + :child-params {:fills (cths/sample-fills-color + :fill-color old-color)}) + ;; `ctkl/update-component` would stamp the current date + (ctf/update-file-data + #(assoc-in % [:components (cthi/id :lib-c) :modified-at] component-changed-at)) + (assoc :synced-at synced-at + :modified-at library-changed-at)) + file (-> (cthf/sample-file :file1) + (cthc/instantiate-component :lib-c :copy + :library library + :children-labels [:copy-child]) + (ctho/add-rect :rect))] + [(cond-> library + changed? + (cths/update-shape :lib-main-child :fills (cths/sample-fills-color :fill-color new-color))) + file])) + +(defn- install-with-library! + [changed?] + (let [[library file] (library-and-file changed?)] + (ftm/install! (-> (tm/make-situation file) + (tm/with-aux-files {(:id library) library}))) + library)) + +(defn- live-library + [] + (get-in @st/state [:files (cthi/id :library)])) + +(defn- copy-fill + [] + (-> (cths/get-shape (ftm/current-file) :copy-child) :fills first :fill-color)) + +(defn- sleep + [ms] + (js/Promise. (fn [resolve] (js/setTimeout resolve ms)))) + +(defn- await-until + "Wait up to a second for `ready?`." + [ready?] + (ftm/await-step [] 1000 ready?)) + +(defn- notification + [] + (:notification @st/state)) + +(defn- rpc-commands + [] + (set (map :cmd @mock/rpc-calls))) + +;; ----- Which libraries need a sync + +(t/deftest assets-need-sync + (let [[library file] (library-and-file true) + data (:data file)] + (t/testing "a library changed after the last sync, with a used component changed too" + (t/is (= [{:shape-id (cthi/id :copy) :asset-id (cthi/id :lib-c) + :asset-type :component :container-id (cthf/current-page-id file)}] + (vec (dwl/assets-need-sync library data))))) + (t/testing "the file itself" + (t/is (nil? (dwl/assets-need-sync library (assoc data :id (:id library)))))) + (t/testing "a library not changed since the last sync" + (t/is (nil? (dwl/assets-need-sync (assoc library :synced-at (js/Date. 4000)) data)))) + (t/testing "changes before the date the user dismissed the updates" + (t/is (nil? (dwl/assets-need-sync library data (js/Date. 4000))))) + (t/testing "a used component that did not change" + (t/is (empty? (dwl/assets-need-sync (assoc library :synced-at (js/Date. 2500)) data)))))) + +;; ----- The sync dialog + +(defn- sync-dialog-shown? + [] + (= :sync-dialog (:tag (notification)))) + +(t/deftest ^:async sync-dialog-updates-the-copies + (await + (mock/with-mocks* + {rp/cmd! mock/rpc-cmd-mock} + (install-with-library! true) + (t/is (= :settled (await (ftm/await-step [(dwl/notify-sync-file)] 1000 sync-dialog-shown?)))) + ((-> (notification) :accept :callback)) + (await (await-until #(= new-color (copy-fill)))) + (t/is (= new-color (copy-fill))) + (t/is (not= :visible (:status (notification)))) + (t/is (not= synced-at (:synced-at (live-library)))) + (t/is (contains? (rpc-commands) :update-file-library-sync-status))))) + +(t/deftest ^:async sync-dialog-dismiss-ignores-the-changes + (await + (mock/with-mocks* + {rp/cmd! mock/rpc-cmd-mock} + (install-with-library! true) + (await (ftm/await-step [(dwl/notify-sync-file)] 1000 sync-dialog-shown?)) + ((-> (notification) :cancel :callback)) + (t/is (some? (:ignore-sync-until (ftm/current-file)))) + (t/is (contains? (rpc-commands) :ignore-file-library-sync-status)) + (t/is (= old-color (copy-fill))) + (t/testing "no dialog for the same changes afterwards" + (st/emit! (dwl/notify-sync-file)) + (await (sleep 50)) + (t/is (not (sync-dialog-shown?))))))) + +(t/deftest ^:async no-sync-dialog-when-the-sync-changes-nothing + (install-with-library! false) + (st/emit! (dwl/notify-sync-file)) + (await (sleep 50)) + (t/is (not (sync-dialog-shown?)))) + +(defn- view-only + [] + (ptk/reify ::view-only + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:permissions :can-edit] false)))) + +(t/deftest ^:async no-sync-dialog-without-edit-permission + (install-with-library! true) + (st/emit! (view-only) (dwl/notify-sync-file)) + (await (sleep 50)) + (t/is (not (sync-dialog-shown?)))) + +;; ----- Changes pushed by the server + +(defn- rename-change + [library] + {:type :mod-obj + :page-id (cthf/current-page-id library) + :id (cthi/id :lib-main-child) + :operations [{:type :set :attr :name :val "Renamed"}]}) + +(t/deftest ^:async library-change-from-the-server + (await + (mock/with-mocks* + {dwl/fetch-library-thumbnails (mock/stub (fn [_] (ptk/data-event ::no-thumbnails)))} + (let [library (install-with-library! true)] + (await (ftm/await-step [(dwl/ext-library-changed (:id library) (js/Date. 5000) 7 + [(rename-change library)])])) + (let [library' (live-library)] + (t/is (= 7 (:revn library'))) + (t/is (= (js/Date. 5000) (:modified-at library'))) + (t/is (= "Renamed" (:name (cths/get-shape library' :lib-main-child))))))))) + +;; ----- Sync from an external library + +(t/deftest ^:async sync-from-an-external-library + (await + (mock/with-mocks* + {rp/cmd! mock/rpc-cmd-mock} + (let [library (install-with-library! true)] + (await (ftm/await-step [(dwl/set-updating-library true) + (dwl/sync-file (:id (ftm/current-file)) (:id library))])) + (t/is (= new-color (copy-fill))) + (t/is (not (:updating-library @st/state))) + (t/is (not= synced-at (:synced-at (live-library)))) + (t/is (contains? (rpc-commands) :update-file-library-sync-status)))))) + +(t/deftest ^:async set-updating-library + (await (ftm/await-step [(dwl/set-updating-library true)])) + (t/is (true? (:updating-library @st/state))) + (await (ftm/await-step [(dwl/set-updating-library false)])) + (t/is (nil? (:updating-library @st/state)))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 16833b0965..1e9adfb2da 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -32,6 +32,9 @@ [frontend-tests.data.workspace-context-menu-test] [frontend-tests.data.workspace-edit-watchers-test] [frontend-tests.data.workspace-interactions-test] + [frontend-tests.data.workspace-libraries-test] + [frontend-tests.data.workspace-library-links-test] + [frontend-tests.data.workspace-library-updates-test] [frontend-tests.data.workspace-mcp-test] [frontend-tests.data.workspace-media-test] [frontend-tests.data.workspace-pages-test] @@ -194,6 +197,9 @@ 'frontend-tests.data.workspace-context-menu-test 'frontend-tests.data.workspace-edit-watchers-test 'frontend-tests.data.workspace-interactions-test + 'frontend-tests.data.workspace-libraries-test + 'frontend-tests.data.workspace-library-links-test + 'frontend-tests.data.workspace-library-updates-test 'frontend-tests.data.workspace-mcp-test 'frontend-tests.data.workspace-media-test 'frontend-tests.data.workspace-pages-test